diff --git a/.eslintrc b/.eslintrc index 44af7b458f..ade1623262 100644 --- a/.eslintrc +++ b/.eslintrc @@ -118,6 +118,8 @@ "getCookie": true, "getCookies": true, "get_url_arg": true, - "QUnit": true + "QUnit": true, + "Snap": true, + "mina": true } } diff --git a/.travis.yml b/.travis.yml index 1551f17ec5..ab46e06c3d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,6 +15,8 @@ services: - mysql install: + - pip install flake8==3.3.0 + - flake8 . --count --select=E901,E999,F821,F822,F823 --show-source --statistics - sudo rm /etc/apt/sources.list.d/docker.list - sudo apt-get purge -y mysql-common mysql-server mysql-client - nvm install v7.10.0 diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md index 4b9a7477c6..ceadd31319 100644 --- a/ISSUE_TEMPLATE.md +++ b/ISSUE_TEMPLATE.md @@ -6,4 +6,4 @@ 1. -Frappe version: +Frappé version: diff --git a/README.md b/README.md index 4b0ee23808..9621b58b46 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -## Frappe Framework +## Frappé Framework [![Build Status](https://travis-ci.org/frappe/frappe.png)](https://travis-ci.org/frappe/frappe) @@ -6,7 +6,7 @@ Full-stack web application framework that uses Python and MariaDB on the server ### Installation -[Install via Frappe Bench](https://github.com/frappe/bench) +[Install via Frappé Bench](https://github.com/frappe/bench) ## Contributing diff --git a/attributions.md b/attributions.md index b7c72912e4..5964c45ea4 100644 --- a/attributions.md +++ b/attributions.md @@ -1,4 +1,4 @@ -## Frappe framework includes these public works +## Frappé framework includes these public works ### Javascript / CSS diff --git a/frappe/__init__.py b/frappe/__init__.py index 914f0ece83..746d5164ea 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -6,7 +6,7 @@ globals attached to frappe module """ from __future__ import unicode_literals, print_function -from six import iteritems +from six import iteritems, text_type from werkzeug.local import Local, release_local import os, sys, importlib, inspect, json @@ -57,14 +57,14 @@ def _(msg, lang=None): def as_unicode(text, encoding='utf-8'): '''Convert to unicode if required''' - if isinstance(text, unicode): + if isinstance(text, text_type): return text elif text==None: return '' elif isinstance(text, basestring): - return unicode(text, encoding) + return text_type(text, encoding) else: - return unicode(text) + return text_type(text) def get_lang_dict(fortype, name=None): """Returns the translated language dict for the given type and name. @@ -880,7 +880,7 @@ def get_file_json(path): def read_file(path, raise_not_found=False): """Open a file and return its content as Unicode.""" - if isinstance(path, unicode): + if isinstance(path, text_type): path = path.encode("utf-8") if os.path.exists(path): diff --git a/frappe/api.py b/frappe/api.py index 4200e1f22b..38c4598273 100644 --- a/frappe/api.py +++ b/frappe/api.py @@ -8,7 +8,7 @@ import frappe.handler import frappe.client from frappe.utils.response import build_response from frappe import _ -from urlparse import urlparse +from six.moves.urllib.parse import urlparse from urllib import urlencode def handle(): diff --git a/frappe/async.py b/frappe/async.py index 70dad31636..622a1ad805 100644 --- a/frappe/async.py +++ b/frappe/async.py @@ -9,6 +9,7 @@ import frappe import os import time import redis +from io import FileIO from frappe.utils import get_site_path from frappe import conf @@ -138,7 +139,7 @@ def get_redis_server(): return redis_server -class FileAndRedisStream(file): +class FileAndRedisStream(FileIO): def __init__(self, *args, **kwargs): ret = super(FileAndRedisStream, self).__init__(*args, **kwargs) self.count = 0 diff --git a/frappe/auth.py b/frappe/auth.py index 3612b98f04..8845b3e790 100644 --- a/frappe/auth.py +++ b/frappe/auth.py @@ -17,7 +17,7 @@ from frappe.translate import get_lang_code from frappe.utils.password import check_password from frappe.core.doctype.authentication_log.authentication_log import add_authentication_log -from urllib import quote +from six.moves.urllib.parse import quote class HTTPRequest: def __init__(self): diff --git a/frappe/boot.py b/frappe/boot.py index d413d03fb6..5d042f82ce 100644 --- a/frappe/boot.py +++ b/frappe/boot.py @@ -3,7 +3,7 @@ from __future__ import unicode_literals -from six import iteritems +from six import iteritems, text_type """ bootstrap client session @@ -17,6 +17,7 @@ from frappe.utils.change_log import get_versions from frappe.translate import get_lang_dict from frappe.email.inbox import get_email_accounts from frappe.core.doctype.feedback_trigger.feedback_trigger import get_enabled_feedback_trigger +from frappe.core.doctype.user_permission.user_permission import get_user_permissions def get_bootinfo(): """build and return boot info""" @@ -30,6 +31,7 @@ def get_bootinfo(): # system info bootinfo.sysdefaults = frappe.defaults.get_defaults() + bootinfo.user_permissions = get_user_permissions() bootinfo.server_date = frappe.utils.nowdate() if frappe.session['user'] != 'Guest': @@ -66,7 +68,7 @@ def get_bootinfo(): frappe.get_attr(method)(bootinfo) if bootinfo.lang: - bootinfo.lang = unicode(bootinfo.lang) + bootinfo.lang = text_type(bootinfo.lang) bootinfo.versions = {k: v['version'] for k, v in get_versions().items()} bootinfo.error_report_email = frappe.get_hooks("error_report_email") diff --git a/frappe/build.js b/frappe/build.js index be24a6b0cd..707708236b 100644 --- a/frappe/build.js +++ b/frappe/build.js @@ -1,3 +1,4 @@ +/*eslint-disable no-console */ const path = require('path'); const fs = require('fs'); const babel = require('babel-core'); @@ -17,7 +18,7 @@ const apps_contents = fs.readFileSync(path_join(sites_path, 'apps.txt'), 'utf8') const apps = apps_contents.split('\n'); const app_paths = apps.map(app => path_join(apps_path, app, app)) // base_path of each app const assets_path = path_join(sites_path, 'assets'); -const build_map = make_build_map(); +let build_map = make_build_map(); const file_watcher_port = get_conf().file_watcher_port; // command line args @@ -62,6 +63,7 @@ function watch() { io.emit('reload_js', filename); } }); + watch_build_json(); }); io.on('connection', function (socket) { @@ -225,11 +227,7 @@ function compile_less_file(file, less_path, public_path) { function watch_less(ondirty) { const less_paths = app_paths.map(path => path_join(path, 'public', 'less')); - const to_watch = []; - for (const less_path of less_paths) { - if (!fs.existsSync(less_path)) continue; - to_watch.push(less_path); - } + const to_watch = filter_valid_paths(less_paths); chokidar.watch(to_watch).on('change', (filename, stats) => { console.log(filename, 'dirty'); var last_index = filename.lastIndexOf('/'); @@ -255,17 +253,9 @@ function watch_less(ondirty) { function watch_js(ondirty) { const js_paths = app_paths.map(path => path_join(path, 'public', 'js')); - const to_watch = []; - for (const js_path of js_paths) { - if (!fs.existsSync(js_path)) continue; - to_watch.push(js_path); - } + const to_watch = filter_valid_paths(js_paths); chokidar.watch(to_watch).on('change', (filename, stats) => { console.log(filename, 'dirty'); - var last_index = filename.lastIndexOf('/'); - const js_path = filename.slice(0, last_index); - const public_path = path_join(js_path, '..'); - // build the target js file for which this js/html file is input for (const target in build_map) { const sources = build_map[target]; @@ -278,6 +268,19 @@ function watch_js(ondirty) { }); } +function watch_build_json() { + const build_json_paths = app_paths.map(path => path_join(path, 'public', 'build.json')); + const to_watch = filter_valid_paths(build_json_paths); + chokidar.watch(to_watch).on('change', (filename) => { + console.log(filename, 'updated'); + build_map = make_build_map(); + }); +} + +function filter_valid_paths(paths) { + return paths.filter(path => fs.existsSync(path)); +} + function html_to_js_template(path, content) { let key = path.split('/'); key = key[key.length - 1]; diff --git a/frappe/build.py b/frappe/build.py index 6c86518f18..2a201fee6f 100644 --- a/frappe/build.py +++ b/frappe/build.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals, print_function from frappe.utils.minify import JavascriptMinify import subprocess -from six import iteritems +from six import iteritems, text_type """ Build the `public` folders and setup languages @@ -121,7 +121,7 @@ def get_build_maps(): timestamps = {} def pack(target, sources, no_compress, verbose): - from cStringIO import StringIO + from six import StringIO outtype, outtxt = target.split(".")[-1], '' jsm = JavascriptMinify() @@ -135,7 +135,7 @@ def pack(target, sources, no_compress, verbose): timestamps[f] = os.path.getmtime(f) try: with open(f, 'r') as sourcefile: - data = unicode(sourcefile.read(), 'utf-8', errors='ignore') + data = text_type(sourcefile.read(), 'utf-8', errors='ignore') extn = f.rsplit(".", 1)[1] @@ -144,7 +144,7 @@ def pack(target, sources, no_compress, verbose): jsm.minify(tmpin, tmpout) minified = tmpout.getvalue() if minified: - outtxt += unicode(minified or '', 'utf-8').strip('\n') + ';' + outtxt += text_type(minified or '', 'utf-8').strip('\n') + ';' if verbose: print("{0}: {1}k".format(f, int(len(minified) / 1024))) diff --git a/frappe/change_log/v6/v6_16_4.md b/frappe/change_log/v6/v6_16_4.md index fc901c3248..63ae316f22 100644 --- a/frappe/change_log/v6/v6_16_4.md +++ b/frappe/change_log/v6/v6_16_4.md @@ -1,2 +1 @@ -- Developer Tutorial [Videos](http://frappe.github.io/frappe/user/videos/) - Increased uploaded file size limit upto 10MB \ No newline at end of file diff --git a/frappe/change_log/v7/v7_2_0.md b/frappe/change_log/v7/v7_2_0.md index 1042b5db7e..d907a9df38 100644 --- a/frappe/change_log/v7/v7_2_0.md +++ b/frappe/change_log/v7/v7_2_0.md @@ -7,7 +7,7 @@ - Delete selected rows - Map selected rows from one document to another. - Show Totals button in report -- OpenID Connect for Frappe +- OpenID Connect for Frappé - Threading based on message id in Email Queue - New control object daterangepicker for filtering - Orientation selection in PDF diff --git a/frappe/client.py b/frappe/client.py index 8489542943..bc51d4c54f 100644 --- a/frappe/client.py +++ b/frappe/client.py @@ -61,16 +61,22 @@ def get_value(doctype, fieldname, filters=None, as_dict=True, debug=False): try: filters = json.loads(filters) - except ValueError: - # name passed, not json + except (TypeError, ValueError): + # filters are not passesd, not json pass try: fieldname = json.loads(fieldname) - except ValueError: + except (TypeError, ValueError): # name passed, not json + fieldname = "name" pass + # check whether the used filters were really parseable and usable + # and did not just result in an empty string or dict + if not filters: + filters = None + return frappe.db.get_value(doctype, filters, fieldname, as_dict=as_dict, debug=debug) @frappe.whitelist() diff --git a/frappe/commands/__init__.py b/frappe/commands/__init__.py index 51cb08fbae..579da214c5 100644 --- a/frappe/commands/__init__.py +++ b/frappe/commands/__init__.py @@ -5,11 +5,11 @@ from __future__ import unicode_literals, absolute_import, print_function import sys import click import cProfile -import StringIO import pstats import frappe import frappe.utils from functools import wraps +from six import StringIO click.disable_unicode_literals_warning = True @@ -25,7 +25,7 @@ def pass_context(f): if profile: pr.disable() - s = StringIO.StringIO() + s = StringIO() ps = pstats.Stats(pr, stream=s)\ .sort_stats('cumtime', 'tottime', 'ncalls') ps.print_stats() diff --git a/frappe/commands/docs.py b/frappe/commands/docs.py index 38d4a22bea..6c73739478 100644 --- a/frappe/commands/docs.py +++ b/frappe/commands/docs.py @@ -1,31 +1,9 @@ from __future__ import unicode_literals, absolute_import import click -import os +import os, shutil import frappe from frappe.commands import pass_context - -@click.command('write-docs') -@pass_context -@click.argument('app') -@click.option('--target', default=None) -@click.option('--local', default=False, is_flag=True, help='Run app locally') -def write_docs(context, app, target=None, local=False): - "Setup docs in target folder of target app" - from frappe.utils.setup_docs import setup_docs - - if not target: - target = os.path.abspath(os.path.join("..", "docs", app)) - - for site in context.sites: - try: - frappe.init(site=site) - frappe.connect() - make = setup_docs(app) - make.make_docs(target, local) - finally: - frappe.destroy() - @click.command('build-docs') @pass_context @click.argument('app') @@ -36,23 +14,25 @@ def write_docs(context, app, target=None, local=False): def build_docs(context, app, docs_version="current", target=None, local=False, watch=False): "Setup docs in target folder of target app" from frappe.utils import watch as start_watch - if not target: - target = os.path.abspath(os.path.join("..", "docs", app)) + from frappe.utils.setup_docs import add_breadcrumbs_tag for site in context.sites: _build_docs_once(site, app, docs_version, target, local) if watch: def trigger_make(source_path, event_type): - if "/templates/autodoc/" in source_path: - _build_docs_once(site, app, docs_version, target, local) - - elif ("/docs.css" in source_path - or "/docs/" in source_path - 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(app, "..", "..") + if "/docs/user/" in source_path: + # user file + target_path = frappe.get_app_path(target, 'www', 'docs', 'user', + os.path.relpath(source_path, start=frappe.get_app_path(app, 'docs', 'user'))) + shutil.copy(source_path, target_path) + add_breadcrumbs_tag(target_path) + + if source_path.endswith('/docs/index.md'): + target_path = frappe.get_app_path(target, 'www', 'docs', 'index.md') + shutil.copy(source_path, target_path) + + 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): @@ -62,17 +42,16 @@ def _build_docs_once(site, app, docs_version, target, local, only_content_update frappe.init(site=site) frappe.connect() - make = setup_docs(app) + make = setup_docs(app, target) if not only_content_updated: make.build(docs_version) - make.make_docs(target, local) + #make.make_docs(target, local) finally: frappe.destroy() commands = [ build_docs, - write_docs, ] diff --git a/frappe/commands/site.py b/frappe/commands/site.py index 5773278835..3716a0b0bd 100755 --- a/frappe/commands/site.py +++ b/frappe/commands/site.py @@ -9,6 +9,7 @@ from frappe.commands.scheduler import _is_scheduler_enabled from frappe.limits import update_limits, get_limits from frappe.installer import update_site_config from frappe.utils import touch_file, get_site_path +from six import text_type @click.command('new-site') @click.argument('site') @@ -428,7 +429,7 @@ def set_limit(context, site, limit, value): @click.command('set-limits') @click.option('--site', help='site name') -@click.option('--limit', 'limits', type=(unicode, unicode), multiple=True) +@click.option('--limit', 'limits', type=(text_type, text_type), multiple=True) @pass_context def set_limits(context, site, limits): _set_limits(context, site, limits) diff --git a/frappe/config/docs.py b/frappe/config/docs.py index 029149d7c0..4d3178e3fd 100644 --- a/frappe/config/docs.py +++ b/frappe/config/docs.py @@ -2,38 +2,5 @@ from __future__ import unicode_literals -docs_version = "7.x.x" - source_link = "https://github.com/frappe/frappe" -docs_base_url = "https://frappe.github.io/frappe" -headline = "Superhero Web Framework" -sub_heading = "Build extensions to ERPNext or make your own app" -hide_install = True -long_description = """Frappe is a full stack web application framework written in Python, -Javascript, HTML/CSS with MySQL as the backend. It was built for ERPNext -but is pretty generic and can be used to build database driven apps. - -The key differece in Frappe compared to other frameworks is that Frappe -is that meta-data is also treated as data and is used to build front-ends -very easily. Frappe comes with a full blown admin UI called the **Desk** -that handles forms, navigation, lists, menus, permissions, file attachment -and much more out of the box. - -Frappe also has a plug-in architecture that can be used to build plugins -to ERPNext. - -Frappe Framework was designed to build [ERPNext](https://erpnext.com), open source -ERP for managing small and medium sized businesses. - -[Get started with the Tutorial](https://frappe.github.io/frappe/user/) -""" -google_analytics_id = 'UA-8911157-23' - -def get_context(context): - context.brand_html = (' Frappé Framework') - context.top_bar_items = [ - {"label": "Tutorials", "url": context.docs_base_url + "/user", "right": 1}, - {"label": "API", "url": context.docs_base_url + "/current", "right": 1}, - {"label": "Forum", "url": 'https://discuss.erpnext.com', "right": 1} - ] +docs_base_url = "/docs" diff --git a/frappe/config/setup.py b/frappe/config/setup.py index c1604e90f9..cd2f58fcc4 100644 --- a/frappe/config/setup.py +++ b/frappe/config/setup.py @@ -31,13 +31,6 @@ def get_data(): "icon": "fa fa-lock", "description": _("Set Permissions on Document Types and Roles") }, - { - "type": "page", - "name": "user-permissions", - "label": _("User Permissions Manager"), - "icon": "fa fa-shield", - "description": _("Set Permissions per User") - }, { "type": "page", "name": "modules_setup", diff --git a/frappe/core/doctype/communication/comment.py b/frappe/core/doctype/communication/comment.py index 96c3e59d84..b5f0f141bc 100644 --- a/frappe/core/doctype/communication/comment.py +++ b/frappe/core/doctype/communication/comment.py @@ -82,12 +82,7 @@ def notify_mentions(doc): sender_fullname = get_fullname(frappe.session.user) parent_doc_label = "{0} {1}".format(_(doc.reference_doctype), doc.reference_name) - subject = _("{0} mentioned you in a comment in {1}").format(sender_fullname, parent_doc_label) - message = frappe.get_template("templates/emails/mentioned_in_comment.html").render({ - "sender_fullname": sender_fullname, - "comment": doc, - "link": get_link_to_form(doc.reference_doctype, doc.reference_name, label=parent_doc_label) - }) + subject = _("{0} mentioned you in a comment").format(sender_fullname) recipients = [frappe.db.get_value("User", {"enabled": 1, "username": username, "user_type": "System User"}) for username in mentions] @@ -96,7 +91,13 @@ def notify_mentions(doc): recipients=recipients, sender=frappe.session.user, subject=subject, - message=message + template="mentioned_in_comment", + args={ + "sender_fullname": sender_fullname, + "comment": doc, + "link": get_link_to_form(doc.reference_doctype, doc.reference_name, label=parent_doc_label) + }, + header=[_('New Mention'), 'orange'] ) def get_comments_from_parent(doc): diff --git a/frappe/core/doctype/communication/feed.py b/frappe/core/doctype/communication/feed.py index ded84a469c..2d939447cd 100644 --- a/frappe/core/doctype/communication/feed.py +++ b/frappe/core/doctype/communication/feed.py @@ -3,7 +3,6 @@ from __future__ import unicode_literals import frappe -import frappe.defaults import frappe.permissions from frappe.model.document import Document from frappe.utils import get_fullname @@ -68,7 +67,7 @@ def get_feed_match_conditions(user=None, force=True): conditions = ['`tabCommunication`.owner="{user}" or `tabCommunication`.reference_owner="{user}"'.format(user=frappe.db.escape(user))] - user_permissions = frappe.defaults.get_user_permissions(user) + user_permissions = frappe.permissions.get_user_permissions(user) can_read = frappe.get_user().get_can_read() can_read_doctypes = ['"{}"'.format(doctype) for doctype in diff --git a/frappe/core/doctype/doctype/boilerplate/controller.py b/frappe/core/doctype/doctype/boilerplate/controller._py similarity index 100% rename from frappe/core/doctype/doctype/boilerplate/controller.py rename to frappe/core/doctype/doctype/boilerplate/controller._py diff --git a/frappe/core/doctype/doctype/boilerplate/test_controller.py b/frappe/core/doctype/doctype/boilerplate/test_controller._py similarity index 100% rename from frappe/core/doctype/doctype/boilerplate/test_controller.py rename to frappe/core/doctype/doctype/boilerplate/test_controller._py diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index 2e13e64f57..40207a24dd 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -330,10 +330,10 @@ class DocType(Document): def make_controller_template(self): """Make boilerplate controller template.""" - make_boilerplate("controller.py", self) + make_boilerplate("controller._py", self) if not (self.istable or self.issingle): - make_boilerplate("test_controller.py", self.as_dict()) + make_boilerplate("test_controller._py", self.as_dict()) if not self.istable: make_boilerplate("controller.js", self.as_dict()) diff --git a/frappe/core/doctype/domain/domain.json b/frappe/core/doctype/domain/domain.json index 35b68d0714..f257d02ae8 100644 --- a/frappe/core/doctype/domain/domain.json +++ b/frappe/core/doctype/domain/domain.json @@ -26,7 +26,7 @@ "ignore_xss_filter": 0, "in_filter": 0, "in_global_search": 0, - "in_list_view": 0, + "in_list_view": 1, "in_standard_filter": 0, "label": "Domain", "length": 0, @@ -54,7 +54,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2017-06-16 13:03:25.430679", + "modified": "2017-07-26 21:29:00.353105", "modified_by": "Administrator", "module": "Core", "name": "Domain", diff --git a/frappe/core/doctype/domain/test_domain.js b/frappe/core/doctype/domain/test_domain.js new file mode 100644 index 0000000000..6d8bd8039d --- /dev/null +++ b/frappe/core/doctype/domain/test_domain.js @@ -0,0 +1,23 @@ +/* eslint-disable */ +// rename this file from _test_[name] to test_[name] to activate +// and remove above this line + +QUnit.test("test: Domain", function (assert) { + let done = assert.async(); + + // number of asserts + assert.expect(1); + + frappe.run_serially('Domain', [ + // insert a new Domain + () => frappe.tests.make([ + // values to be set + {key: 'value'} + ]), + () => { + assert.equal(cur_frm.doc.key, 'value'); + }, + () => done() + ]); + +}); diff --git a/frappe/core/doctype/feedback_trigger/feedback_trigger.py b/frappe/core/doctype/feedback_trigger/feedback_trigger.py index 6b9ca3e46a..e40f668d58 100644 --- a/frappe/core/doctype/feedback_trigger/feedback_trigger.py +++ b/frappe/core/doctype/feedback_trigger/feedback_trigger.py @@ -66,12 +66,16 @@ def send_feedback_request(reference_doctype, reference_name, trigger="Manual", d feedback_request, url = get_feedback_request_url(reference_doctype, reference_name, details.get("recipients"), trigger) - feedback_url = frappe.render_template("templates/emails/feedback_request_url.html", { "url": url }) + feedback_msg = frappe.render_template("templates/emails/feedback_request_url.html", { "url": url }) # appending feedback url to message body - details.update({ "message": "{message}{feedback_url}".format( + message = "{message}{feedback_msg}".format( message=details.get("message"), - feedback_url=feedback_url) + feedback_msg=feedback_msg + ) + details.update({ + "message": message, + "header": [details.get('subject'), 'blue'] }) if details: diff --git a/frappe/core/doctype/file/file.py b/frappe/core/doctype/file/file.py index 324353c882..f9b65e6556 100755 --- a/frappe/core/doctype/file/file.py +++ b/frappe/core/doctype/file/file.py @@ -10,12 +10,10 @@ naming for same name files: file.gif, file-1.gif, file-2.gif etc import frappe import json -import urllib import os import shutil import requests import requests.exceptions -import StringIO import mimetypes, imghdr from frappe.utils.file_manager import delete_file_data_content, get_content_hash, get_random_filename @@ -23,6 +21,8 @@ from frappe import _ from frappe.utils.nestedset import NestedSet from frappe.utils import strip, get_files_path from PIL import Image, ImageOps +from six import StringIO +from six.moves.urllib.parse import unquote import zipfile class FolderNotEmpty(frappe.ValidationError): pass @@ -372,7 +372,7 @@ def get_web_image(file_url): frappe.msgprint(_("Unable to read file format for {0}").format(file_url)) raise - image = Image.open(StringIO.StringIO(r.content)) + image = Image.open(StringIO(r.content)) try: filename, extn = file_url.rsplit("/", 1)[1].rsplit(".", 1) @@ -383,7 +383,7 @@ def get_web_image(file_url): extn = None extn = get_extension(filename, extn, r.content) - filename = "/files/" + strip(urllib.unquote(filename)) + filename = "/files/" + strip(unquote(filename)) return image, filename, extn diff --git a/frappe/core/doctype/has_role/has_role.py b/frappe/core/doctype/has_role/has_role.py index 44c27098d9..45e76c85a1 100644 --- a/frappe/core/doctype/has_role/has_role.py +++ b/frappe/core/doctype/has_role/has_role.py @@ -7,7 +7,6 @@ import frappe from frappe.model.document import Document class HasRole(Document): - def validate(self): - if cint(self.get("__islocal")) and frappe.db.exists("Has Role", { - "parent": self.parent, "role": self.role}): + def before_insert(self): + if frappe.db.exists("Has Role", {"parent": self.parent, "role": self.role}): frappe.throw(frappe._("User '{0}' already has the role '{1}'").format(self.parent, self.role)) diff --git a/frappe/core/doctype/page/page.py b/frappe/core/doctype/page/page.py index 720ff563a8..865bba2b1f 100644 --- a/frappe/core/doctype/page/page.py +++ b/frappe/core/doctype/page/page.py @@ -10,6 +10,7 @@ from frappe.model.utils import render_include from frappe import conf, _ from frappe.desk.form.meta import get_code_files_via_hooks, get_js from frappe.core.doctype.custom_role.custom_role import get_custom_allowed_roles +from six import text_type class Page(Document): def autoname(self): @@ -111,13 +112,13 @@ class Page(Document): fpath = os.path.join(path, page_name + '.css') if os.path.exists(fpath): with open(fpath, 'r') as f: - self.style = unicode(f.read(), "utf-8") + self.style = text_type(f.read(), "utf-8") # html as js template for fname in os.listdir(path): if fname.endswith(".html"): with open(os.path.join(path, fname), 'r') as f: - template = unicode(f.read(), "utf-8") + template = text_type(f.read(), "utf-8") if "" in template: context = frappe._dict({}) try: diff --git a/frappe/core/doctype/report/test_query_report.js b/frappe/core/doctype/report/test_query_report.js new file mode 100644 index 0000000000..c51884cd21 --- /dev/null +++ b/frappe/core/doctype/report/test_query_report.js @@ -0,0 +1,33 @@ +// Test for creating query report +QUnit.test("Test Query Report", function(assert){ + assert.expect(2); + let done = assert.async(); + let random = frappe.utils.get_random(10); + frappe.run_serially([ + () => frappe.set_route('List', 'ToDo'), + () => frappe.new_doc('ToDo'), + () => frappe.quick_entry.dialog.set_value('description', random), + () => frappe.quick_entry.insert(), + () => { + return frappe.tests.make('Report', [ + {report_name: 'ToDo List Report'}, + {report_type: 'Query Report'}, + {ref_doctype: 'ToDo'} + ]); + }, + () => frappe.set_route('Form','Report', 'ToDo List Report'), + + //Query + () => cur_frm.set_value('query','select description,owner,status from `tabToDo`'), + () => cur_frm.save(), + () => frappe.set_route('query-report','ToDo List Report'), + () => frappe.timeout(5), + () => { + assert.ok($('div.slick-header-column').length == 4,'Correct numbers of columns visible'); + //To check if the result is present + assert.ok($('div.r1:contains('+random+')').is(':visible'),'Result is visible in report'); + frappe.timeout(3); + }, + () => done() + ]); +}); diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json index 87d85449ba..bbdf75c085 100644 --- a/frappe/core/doctype/system_settings/system_settings.json +++ b/frappe/core/doctype/system_settings/system_settings.json @@ -527,7 +527,7 @@ "bold": 0, "collapsible": 1, "columns": 0, - "fieldname": "security", + "fieldname": "permissions", "fieldtype": "Section Break", "hidden": 0, "ignore_user_permissions": 0, @@ -536,10 +536,11 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, - "label": "Security", + "label": "Permissions", "length": 0, "no_copy": 0, "permlevel": 0, + "precision": "", "print_hide": 0, "print_hide_if_no_value": 0, "read_only": 0, @@ -556,10 +557,9 @@ "bold": 0, "collapsible": 0, "columns": 0, - "default": "06:00", - "description": "Session Expiry in Hours e.g. 06:00", - "fieldname": "session_expiry", - "fieldtype": "Data", + "description": "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, @@ -567,11 +567,11 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, - "label": "Session Expiry", + "label": "Ignore User Permissions If Missing", "length": 0, "no_copy": 0, - "options": "", "permlevel": 0, + "precision": "", "print_hide": 0, "print_hide_if_no_value": 0, "read_only": 0, @@ -588,10 +588,10 @@ "bold": 0, "collapsible": 0, "columns": 0, - "default": "720:00", - "description": "In Hours", - "fieldname": "session_expiry_mobile", - "fieldtype": "Data", + "default": "0", + "description": "If Apply Strict User Permission is checked and User Permission is defined for a DocType for a User, then all the documents where value of the link is blank, will not be shown to that User", + "fieldname": "apply_strict_user_permissions", + "fieldtype": "Check", "hidden": 0, "ignore_user_permissions": 0, "ignore_xss_filter": 0, @@ -599,7 +599,7 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, - "label": "Session Expiry Mobile", + "label": "Apply Strict User Permissions", "length": 0, "no_copy": 0, "permlevel": 0, @@ -614,16 +614,45 @@ "set_only_once": 0, "unique": 0 }, + { + "allow_bulk_edit": 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_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Security", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, { "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, "columns": 0, - "default": "0", - "description": "If enabled, the password strength will be enforced based on the Minimum Password Score value. A value of 2 being medium strong and 4 being very strong.", - "fieldname": "enable_password_policy", - "fieldtype": "Check", + "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, @@ -631,11 +660,11 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, - "label": "Enable Password Policy", + "label": "Session Expiry", "length": 0, "no_copy": 0, + "options": "", "permlevel": 0, - "precision": "", "print_hide": 0, "print_hide_if_no_value": 0, "read_only": 0, @@ -652,10 +681,10 @@ "bold": 0, "collapsible": 0, "columns": 0, - "default": "2", - "depends_on": "eval:doc.enable_password_policy==1", - "fieldname": "minimum_password_score", - "fieldtype": "Select", + "default": "720:00", + "description": "In Hours", + "fieldname": "session_expiry_mobile", + "fieldtype": "Data", "hidden": 0, "ignore_user_permissions": 0, "ignore_xss_filter": 0, @@ -663,10 +692,9 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, - "label": "Minimum Password Score", + "label": "Session Expiry Mobile", "length": 0, "no_copy": 0, - "options": "2\n4", "permlevel": 0, "precision": "", "print_hide": 0, @@ -685,8 +713,10 @@ "bold": 0, "collapsible": 0, "columns": 0, - "fieldname": "column_break_13", - "fieldtype": "Column Break", + "default": "0", + "description": "If enabled, the password strength will be enforced based on the Minimum Password Score value. A value of 2 being medium strong and 4 being very strong.", + "fieldname": "enable_password_policy", + "fieldtype": "Check", "hidden": 0, "ignore_user_permissions": 0, "ignore_xss_filter": 0, @@ -694,6 +724,7 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, + "label": "Enable Password Policy", "length": 0, "no_copy": 0, "permlevel": 0, @@ -714,9 +745,10 @@ "bold": 0, "collapsible": 0, "columns": 0, - "description": "Note: Multiple sessions will be allowed in case of mobile device", - "fieldname": "deny_multiple_sessions", - "fieldtype": "Check", + "default": "2", + "depends_on": "eval:doc.enable_password_policy==1", + "fieldname": "minimum_password_score", + "fieldtype": "Select", "hidden": 0, "ignore_user_permissions": 0, "ignore_xss_filter": 0, @@ -724,9 +756,10 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, - "label": "Allow only one session per user", + "label": "Minimum Password Score", "length": 0, "no_copy": 0, + "options": "2\n4", "permlevel": 0, "precision": "", "print_hide": 0, @@ -745,9 +778,8 @@ "bold": 0, "collapsible": 0, "columns": 0, - "description": "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", + "fieldname": "column_break_13", + "fieldtype": "Column Break", "hidden": 0, "ignore_user_permissions": 0, "ignore_xss_filter": 0, @@ -755,7 +787,6 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, - "label": "Ignore User Permissions If Missing", "length": 0, "no_copy": 0, "permlevel": 0, @@ -776,9 +807,8 @@ "bold": 0, "collapsible": 0, "columns": 0, - "default": "0", - "description": "If Apply Strict User Permission is checked and User Permission is defined for a DocType for a User, then all the documents where value of the link is blank, will not be shown to that User", - "fieldname": "apply_strict_user_permissions", + "description": "Note: Multiple sessions will be allowed in case of mobile device", + "fieldname": "deny_multiple_sessions", "fieldtype": "Check", "hidden": 0, "ignore_user_permissions": 0, @@ -787,7 +817,7 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, - "label": "Apply Strict User Permissions", + "label": "Allow only one session per user", "length": 0, "no_copy": 0, "permlevel": 0, @@ -997,7 +1027,7 @@ "issingle": 1, "istable": 0, "max_attachments": 0, - "modified": "2017-06-23 07:48:10.453011", + "modified": "2017-07-20 22:57:56.466867", "modified_by": "Administrator", "module": "Core", "name": "System Settings", @@ -1032,4 +1062,4 @@ "sort_order": "ASC", "track_changes": 1, "track_seen": 0 -} +} \ No newline at end of file diff --git a/frappe/core/doctype/system_settings/system_settings.py b/frappe/core/doctype/system_settings/system_settings.py index 8f64feda9f..f7ecfc00bb 100644 --- a/frappe/core/doctype/system_settings/system_settings.py +++ b/frappe/core/doctype/system_settings/system_settings.py @@ -35,6 +35,7 @@ class SystemSettings(Document): frappe.cache().delete_value('system_settings') frappe.cache().delete_value('time_zone') + frappe.local.system_settings = {} @frappe.whitelist() def load(): diff --git a/frappe/core/doctype/system_settings/test_system_settings.js b/frappe/core/doctype/system_settings/test_system_settings.js new file mode 100644 index 0000000000..53edaba99d --- /dev/null +++ b/frappe/core/doctype/system_settings/test_system_settings.js @@ -0,0 +1,23 @@ +/* eslint-disable */ +// rename this file from _test_[name] to test_[name] to activate +// and remove above this line + +QUnit.test("test: System Settings", function (assert) { + let done = assert.async(); + + // number of asserts + assert.expect(1); + + frappe.run_serially('System Settings', [ + // insert a new System Settings + () => frappe.tests.make([ + // values to be set + {key: 'value'} + ]), + () => { + assert.equal(cur_frm.doc.key, 'value'); + }, + () => done() + ]); + +}); diff --git a/frappe/core/doctype/user/user.js b/frappe/core/doctype/user/user.js index aa7f7940e3..49c1f8b437 100644 --- a/frappe/core/doctype/user/user.js +++ b/frappe/core/doctype/user/user.js @@ -59,9 +59,13 @@ frappe.ui.form.on('User', { frappe.route_options = { "user": doc.name }; - frappe.set_route("user-permissions"); + frappe.set_route('List', 'User Permission'); }, null, "btn-default") + frm.add_custom_button(__('View Permitted Documents'), + () => frappe.set_route('query-report', 'Permitted Documents For User', + {user: frm.doc.name})); + frm.toggle_display(['sb1', 'sb3', 'modules_access'], true); } diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index da08997456..c5dfbd0e2a 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -1,7 +1,7 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # MIT License. See license.txt -from __future__ import unicode_literals +from __future__ import unicode_literals, print_function import frappe from frappe.model.document import Document from frappe.utils import cint, has_gravatar, format_datetime, now_datetime, get_formatted_email @@ -191,7 +191,7 @@ class User(Document): self.email_new_password(new_password) except frappe.OutgoingEmailError: - print frappe.get_traceback() + print(frappe.get_traceback()) pass # email server not set, don't send email @Document.hook @@ -858,25 +858,21 @@ def notify_admin_access_to_system_manager(login_manager=None): and login_manager.user == "Administrator" and frappe.local.conf.notify_admin_access_to_system_manager): - message = """

- {dear_system_manager}

- {access_message}

- {is_it_unauthorized} -

""".format( - dear_system_manager=_("Dear System Manager,"), + site = '{0}'.format(frappe.local.request.host_url) + date_and_time = '{0}'.format(format_datetime(now_datetime(), format_string="medium")) + ip_address = frappe.local.request_ip - access_message=_("""Administrator accessed {0} on {1} via IP Address {2}.""").format( - """{site}""".format(site=frappe.local.request.host_url), - """{date_and_time}""".format(date_and_time=format_datetime(now_datetime(), format_string="medium")), - frappe.local.request_ip - ), + access_message = _('Administrator accessed {0} on {1} via IP Address {2}.').format( + site, date_and_time, ip_address) - is_it_unauthorized=_("If you think this is unauthorized, please change the Administrator password.") + frappe.sendmail( + recipients=get_system_managers(), + subject=_("Administrator Logged In"), + template="administrator_logged_in", + args={'access_message': access_message}, + header=['Access Notification', 'orange'] ) - frappe.sendmail(recipients=get_system_managers(), subject=_("Administrator Logged In"), - message=message) - def extract_mentions(txt): """Find all instances of @username in the string. The mentions will be separated by non-word characters or may appear at the start of the string""" diff --git a/frappe/core/doctype/user_permission/__init__.py b/frappe/core/doctype/user_permission/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/core/doctype/user_permission/test_user_permission.js b/frappe/core/doctype/user_permission/test_user_permission.js new file mode 100644 index 0000000000..1770dddf81 --- /dev/null +++ b/frappe/core/doctype/user_permission/test_user_permission.js @@ -0,0 +1,23 @@ +/* eslint-disable */ +// rename this file from _test_[name] to test_[name] to activate +// and remove above this line + +QUnit.test("test: User Permission", function (assert) { + let done = assert.async(); + + // number of asserts + assert.expect(1); + + frappe.run_serially('User Permission', [ + // insert a new User Permission + () => frappe.tests.make([ + // values to be set + {key: 'value'} + ]), + () => { + assert.equal(cur_frm.doc.key, 'value'); + }, + () => done() + ]); + +}); diff --git a/frappe/core/doctype/user_permission/test_user_permission.py b/frappe/core/doctype/user_permission/test_user_permission.py new file mode 100644 index 0000000000..157fa44ae2 --- /dev/null +++ b/frappe/core/doctype/user_permission/test_user_permission.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2017, Frappe Technologies and Contributors +# See license.txt +from __future__ import unicode_literals + +#import frappe +import unittest + +class TestUserPermission(unittest.TestCase): + pass diff --git a/frappe/core/doctype/user_permission/user_permission.js b/frappe/core/doctype/user_permission/user_permission.js new file mode 100644 index 0000000000..fbd6ed5616 --- /dev/null +++ b/frappe/core/doctype/user_permission/user_permission.js @@ -0,0 +1,10 @@ +// Copyright (c) 2017, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on('User Permission', { + refresh: function(frm) { + frm.add_custom_button(__('View Permitted Documents'), + () => frappe.set_route('query-report', 'Permitted Documents For User', + {user: frm.doc.user})); + } +}); diff --git a/frappe/core/doctype/user_permission/user_permission.json b/frappe/core/doctype/user_permission/user_permission.json new file mode 100644 index 0000000000..2e67de5ce0 --- /dev/null +++ b/frappe/core/doctype/user_permission/user_permission.json @@ -0,0 +1,188 @@ +{ + "allow_copy": 0, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 0, + "beta": 0, + "creation": "2017-07-17 14:25:27.881871", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "", + "editable_grid": 1, + "engine": "InnoDB", + "fields": [ + { + "allow_bulk_edit": 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_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 1, + "label": "User", + "length": 0, + "no_copy": 0, + "options": "User", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "allow", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Allow", + "length": 0, + "no_copy": 0, + "options": "DocType", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "for_value", + "fieldtype": "Dynamic Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 1, + "label": "For Value", + "length": 0, + "no_copy": 0, + "options": "allow", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 1, + "collapsible": 0, + "columns": 0, + "default": "1", + "description": "If you un-check this, you will have to apply manually for each Role + Document Type combination", + "fieldname": "apply_for_all_roles", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Apply for all Roles for this User", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + } + ], + "has_web_view": 0, + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 0, + "image_view": 0, + "in_create": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 0, + "max_attachments": 0, + "modified": "2017-07-27 22:55:58.647315", + "modified_by": "Administrator", + "module": "Core", + "name": "User Permission", + "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 + } + ], + "quick_entry": 1, + "read_only": 0, + "read_only_onload": 0, + "show_name_in_global_search": 0, + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "user", + "track_changes": 1, + "track_seen": 0 +} \ No newline at end of file diff --git a/frappe/core/doctype/user_permission/user_permission.py b/frappe/core/doctype/user_permission/user_permission.py new file mode 100644 index 0000000000..a8bd0c0799 --- /dev/null +++ b/frappe/core/doctype/user_permission/user_permission.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2017, 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 +from frappe.permissions import (get_valid_perms, update_permission_property) +from frappe import _ + +class UserPermission(Document): + def on_update(self): + frappe.cache().delete_value('user_permissions') + + if self.apply_for_all_roles: + self.apply_user_permissions_to_all_roles() + + def apply_user_permissions_to_all_roles(self): + # add apply user permissions for all roles that + # for this doctype + def show_progress(i, l): + if l > 2: + frappe.publish_realtime("progress", + dict(progress=[i, l], title=_('Updating...')), + user=frappe.session.user) + + + roles = frappe.get_roles(self.user) + linked = frappe.db.sql('''select distinct parent from tabDocField + where fieldtype="Link" and options=%s''', self.allow) + for i, link in enumerate(linked): + doctype = link[0] + for perm in get_valid_perms(doctype, self.user): + # if the role is applicable to the user + show_progress(i+1, len(linked)) + if perm.role in roles: + if not perm.apply_user_permissions: + update_permission_property(doctype, perm.role, 0, + 'apply_user_permissions', '1') + + try: + user_permission_doctypes = json.loads(perm.user_permission_doctypes or '[]') + except ValueError: + user_permission_doctypes = [] + + if self.allow not in user_permission_doctypes: + user_permission_doctypes.append(self.allow) + update_permission_property(doctype, perm.role, 0, + 'user_permission_doctypes', json.dumps(user_permission_doctypes), validate=False) + + show_progress(len(linked), len(linked)) + + def on_trash(self): # pylint: disable=no-self-use + frappe.cache().delete_value('user_permissions') + +def get_user_permissions(user=None): + '''Get all users permissions for the user as a dict of doctype''' + if not user: + user = frappe.session.user + + out = frappe.cache().hget("user_permissions", user) + + if not out: + out = {} + try: + for perm in frappe.get_all('User Permission', + fields=['allow', 'for_value'], filters=dict(user=user)): + out.setdefault(perm.allow, []).append(perm.for_value) + + # add profile match + if user not in out.get("User", []): + out.setdefault("User", []).append(user) + + frappe.cache().hset("user_permissions", user, out) + except frappe.SQLError as e: + if e.args[0]==1146: + # called from patch + pass + + return out \ No newline at end of file diff --git a/frappe/core/page/data_import_tool/exporter.py b/frappe/core/page/data_import_tool/exporter.py index d76ba01bff..caa6ae90ef 100644 --- a/frappe/core/page/data_import_tool/exporter.py +++ b/frappe/core/page/data_import_tool/exporter.py @@ -284,7 +284,7 @@ def get_template(doctype=None, parent_doctype=None, all_doctypes="No", with_data if from_data_import == "Yes" and excel_format == "Yes": filename = frappe.generate_hash("", 10) with open(filename, 'wb') as f: - f.write(cstr(w.getvalue()).encode("utf-8")) + f.write(cstr(w.getvalue()).encode("utf-8")) f = open(filename) reader = csv.reader(f) diff --git a/frappe/core/page/data_import_tool/importer.py b/frappe/core/page/data_import_tool/importer.py index dd4a872111..308cef8f55 100644 --- a/frappe/core/page/data_import_tool/importer.py +++ b/frappe/core/page/data_import_tool/importer.py @@ -17,6 +17,7 @@ from frappe.utils.file_manager import save_url from frappe.utils import cint, cstr, flt, getdate, get_datetime, get_url from frappe.core.page.data_import_tool.data_import_tool import get_data_keys +from six import text_type @frappe.whitelist() def upload(rows = None, submit_after_import=None, ignore_encoding_errors=False, no_email=True, overwrite=None, @@ -308,7 +309,7 @@ def upload(rows = None, submit_after_import=None, ignore_encoding_errors=False, doc = parent.append(parentfield, doc) parent.save() log('Inserted row for %s at #%s' % (as_link(parenttype, - doc.parent), unicode(doc.idx))) + doc.parent),text_type(doc.idx))) else: if overwrite and doc["name"] and frappe.db.exists(doctype, doc["name"]): original = frappe.get_doc(doctype, doc["name"]) diff --git a/frappe/core/page/desktop/desktop.js b/frappe/core/page/desktop/desktop.js index bfa5575963..ad706f6c18 100644 --- a/frappe/core/page/desktop/desktop.js +++ b/frappe/core/page/desktop/desktop.js @@ -50,7 +50,6 @@ $.extend(frappe.desktop, { desktop_items: all_icons, })); - frappe.desktop.setup_help_messages(); frappe.desktop.setup_module_click(); // notifications @@ -63,30 +62,6 @@ $.extend(frappe.desktop, { }, - setup_help_messages: function() { - // { - // title: 'Sign up for a Premium Plan', - // description: 'Sign up for a premium plan and add users, get more disk space and priority support', - // action: 'Select Plan', - // route: 'usage-info' - // } - - // TEMP: test activiation without this message. - return; - - // if(!frappe.user.has_role('System Manager')) { - // return; - // } - - // frappe.call({ - // method: 'frappe.core.page.desktop.desktop.get_help_messages', - // callback: function(r) { - // frappe.desktop.render_help_messages(r.message); - // } - // }); - - }, - render_help_messages: function(help_messages) { var wrapper = frappe.desktop.wrapper.find('.help-message-wrapper'); var $help_messages = wrapper.find('.help-messages'); diff --git a/frappe/core/page/permission_manager/permission_manager.js b/frappe/core/page/permission_manager/permission_manager.js index 400043a5fd..020db1d16b 100644 --- a/frappe/core/page/permission_manager/permission_manager.js +++ b/frappe/core/page/permission_manager/permission_manager.js @@ -21,6 +21,7 @@ frappe.pages['permission-manager'].refresh = function(wrapper) { frappe.PermissionEngine = Class.extend({ init: function(wrapper) { this.wrapper = wrapper; + this.page = wrapper.page; this.body = $(this.wrapper).find(".perm-engine"); this.make(); this.refresh(); @@ -55,6 +56,10 @@ frappe.PermissionEngine = Class.extend({ .change(function() { me.refresh(); }); + + this.page.add_inner_button(__('Set User Permissions'), () => { + return frappe.set_route('List', 'User Permission'); + }); this.set_from_route(); }, set_from_route: function() { @@ -133,11 +138,11 @@ frappe.PermissionEngine = Class.extend({ refresh: function() { var me = this; if(!me.doctype_select) { - this.body.html("

" + __("Loading") + "..."); + this.body.html("

" + __("Loading") + "...

"); return; } if(!me.get_doctype() && !me.get_role()) { - this.body.html("

"+__("Select Document Type or Role to start.")+""); + this.body.html("

"+__("Select Document Type or Role to start.")+"

"); return; } // get permissions @@ -247,10 +252,13 @@ frappe.PermissionEngine = Class.extend({ setup_user_permissions: function(d, role_cell) { var me = this; - d.help = frappe.render('', {}); + d.help = ``; var checkbox = this.add_check(role_cell, d, "apply_user_permissions") .removeClass("col-md-4") @@ -336,8 +344,8 @@ frappe.PermissionEngine = Class.extend({ var me = this; this.body.on("click", ".show-user-permissions", function() { - frappe.route_options = { doctype: me.get_doctype() || "" }; - frappe.set_route("user-permissions"); + frappe.route_options = { allow: me.get_doctype() || "" }; + frappe.set_route('List', 'User Permission'); }); this.body.on("click", "input[type='checkbox']", function() { diff --git a/frappe/core/page/permission_manager/permission_manager.py b/frappe/core/page/permission_manager/permission_manager.py index 626bb1c20d..ae3a3971e6 100644 --- a/frappe/core/page/permission_manager/permission_manager.py +++ b/frappe/core/page/permission_manager/permission_manager.py @@ -7,7 +7,7 @@ import frappe.defaults from frappe.modules.import_file import get_file_path, read_doc_from_file from frappe.translate import send_translations from frappe.permissions import (reset_perms, get_linked_doctypes, get_all_perms, - setup_custom_perms, add_permission) + setup_custom_perms, add_permission, update_permission_property) from frappe.core.doctype.doctype.doctype import (clear_permissions_cache, validate_permissions_for_doctype) from frappe import _ @@ -68,18 +68,8 @@ def add(parent, role, permlevel): @frappe.whitelist() def update(doctype, role, permlevel, ptype, value=None): frappe.only_for("System Manager") - - out = None - if setup_custom_perms(doctype): - out = 'refresh' - - name = frappe.get_value('Custom DocPerm', dict(parent=doctype, role=role, permlevel=permlevel)) - - frappe.db.sql("""update `tabCustom DocPerm` set `%s`=%s where name=%s"""\ - % (frappe.db.escape(ptype), '%s', '%s'), (value, name)) - validate_permissions_for_doctype(doctype) - - return out + out = update_permission_property(doctype, role, permlevel, ptype, value) + return 'refresh' if out else None @frappe.whitelist() def remove(doctype, role, permlevel): diff --git a/frappe/core/page/user_permissions/README.md b/frappe/core/page/user_permissions/README.md deleted file mode 100644 index e4fc779c6c..0000000000 --- a/frappe/core/page/user_permissions/README.md +++ /dev/null @@ -1 +0,0 @@ -Interface to set user defaults (DefaultValue). \ No newline at end of file diff --git a/frappe/core/page/user_permissions/__init__.py b/frappe/core/page/user_permissions/__init__.py deleted file mode 100644 index 0e57cb68c3..0000000000 --- a/frappe/core/page/user_permissions/__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/page/user_permissions/user_permissions.js b/frappe/core/page/user_permissions/user_permissions.js deleted file mode 100644 index 5cb81900eb..0000000000 --- a/frappe/core/page/user_permissions/user_permissions.js +++ /dev/null @@ -1,365 +0,0 @@ -frappe.pages['user-permissions'].on_page_load = function(wrapper) { - var page = frappe.ui.make_app_page({ - parent: wrapper, - title: __("User Permissions Manager"), - icon: "fa fa-shield", - single_column: true - }); - - frappe.breadcrumbs.add("Setup"); - - $("
\ -

\ - " + __("Edit Role Permissions") + "\ -


\ -

"+__("Help for User Permissions")+":

\ -
    \ -
  1. " - + __("Apart from Role based Permission Rules, you can apply User Permissions based on DocTypes.") - + "
  2. " - - + "
  3. " - + __("These permissions will apply for all transactions where the permitted record is linked. For example, if Company C is added to User Permissions of user X, user X will only be able to see transactions that has company C as a linked value.") - + "
  4. " - - + "
  5. " - + __("These will also be set as default values for those links, if only one such permission record is defined.") - + "
  6. " - - + "
  7. " - + __("A user can be permitted to multiple records of the same DocType.") - + "
  8. \ -
").appendTo(page.main); - wrapper.user_permissions = new frappe.UserPermissions(wrapper); -} - -frappe.pages['user-permissions'].refresh = function(wrapper) { - wrapper.user_permissions.set_from_route(); -} - -frappe.UserPermissions = Class.extend({ - init: function(wrapper) { - this.wrapper = wrapper; - this.body = $(this.wrapper).find(".user-settings"); - this.filters = {}; - this.make(); - this.refresh(); - }, - make: function() { - var me = this; - - $(this.wrapper).find(".view-role-permissions").on("click", function() { - frappe.route_options = { doctype: me.get_doctype() || "" }; - frappe.set_route("permission-manager"); - }) - - return frappe.call({ - module:"frappe.core", - page:"user_permissions", - method: "get_users_and_links", - callback: function(r) { - me.options = r.message; - - me.filters.user = me.wrapper.page.add_field({ - fieldname: "user", - label: __("User"), - fieldtype: "Select", - options: ([__("Select User") + "..."].concat(r.message.users)).join("\n") - }); - - me.filters.doctype = me.wrapper.page.add_field({ - fieldname: "doctype", - label: __("DocType"), - fieldtype: "Select", - options: ([__("Select DocType") + "..."].concat(me.get_link_names())).join("\n") - }); - - me.filters.user_permission = me.wrapper.page.add_field({ - fieldname: "user_permission", - label: __("Name"), - fieldtype: "Link", - options: "[Select]" - }); - - if(frappe.user_roles.includes("System Manager")) { - me.download = me.wrapper.page.add_field({ - fieldname: "download", - label: __("Download"), - fieldtype: "Button", - icon: "fa fa-download" - }); - - me.upload = me.wrapper.page.add_field({ - fieldname: "upload", - label: __("Upload"), - fieldtype: "Button", - icon: "fa fa-upload" - }); - } - - // bind change event - $.each(me.filters, function(k, f) { - f.$input.on("change", function() { - me.refresh(); - }); - }); - - // change options in user_permission link - me.filters.doctype.$input.on("change", function() { - me.filters.user_permission.df.options = me.get_doctype(); - }); - - me.set_from_route(); - me.setup_download_upload(); - } - }); - }, - setup_download_upload: function() { - var me = this; - me.download.$input.on("click", function() { - window.location.href = frappe.urllib.get_base_url() - + "/api/method/frappe.core.page.user_permissions.user_permissions.get_user_permissions_csv"; - }); - - me.upload.$input.on("click", function() { - var d = new frappe.ui.Dialog({ - title: __("Upload User Permissions"), - fields: [ - { - fieldtype:"HTML", - options: '

    '+ - "
  1. "+__("Upload CSV file containing all user permissions in the same format as Download.")+"
  2. "+ - "
  3. "+__("Any existing permission will be deleted / overwritten.")+"
  4. "+ - '

    ' - }, - { - fieldtype:"Attach", fieldname:"attach", - } - ], - primary_action_label: __("Upload and Sync"), - primary_action: function() { - var filedata = d.fields_dict.attach.get_value(); - if(!filedata) { - frappe.msgprint(__("Please attach a file")); - return; - } - frappe.call({ - method:"frappe.core.page.user_permissions.user_permissions.import_user_permissions", - args: { - filedata: filedata - }, - callback: function(r) { - if(!r.exc) { - frappe.msgprint(__("Permissions Updated")); - d.hide(); - } - } - }); - } - }); - d.show(); - }) - }, - get_link_names: function() { - return this.options.link_fields; - }, - set_from_route: function() { - var me = this; - if(frappe.route_options && this.filters && !$.isEmptyObject(this.filters)) { - $.each(frappe.route_options, function(key, value) { - if(me.filters[key] && frappe.route_options[key]!=null) - me.set_filter(key, value); - }); - frappe.route_options = null; - } - this.refresh(); - }, - set_filter: function(key, value) { - this.filters[key].$input.val(value); - }, - get_user: function() { - var user = this.filters.user.$input.val(); - return user== __("Select User") + "..." ? null : user; - }, - get_doctype: function() { - var doctype = this.filters.doctype.$input.val(); - return doctype== __("Select DocType") + "..." ? null : doctype; - }, - get_user_permission: function() { - // autosuggest hack! - var user_permission = this.filters.user_permission.$input.val(); - return (user_permission === "%") ? null : user_permission; - }, - render: function(prop_list) { - var me = this; - this.body.empty(); - this.prop_list = prop_list; - if(!prop_list || !prop_list.length) { - this.add_message(__("No User Restrictions found.")); - } else { - this.show_user_permissions_table(); - } - this.show_add_user_permission(); - if(this.get_user() && this.get_doctype()) { - $('').appendTo(this.body.find(".btn-area")).on("click", function() { - frappe.route_options = {doctype: me.get_doctype(), user:me.get_user() }; - frappe.set_route("query-report/Permitted Documents For User"); - }); - } - }, - add_message: function(txt) { - $('

    ' + txt + '

    ').appendTo(this.body); - }, - refresh: function() { - var me = this; - if(!me.filters.user) { - this.body.html("

    "+__("Loading")+"...

    "); - return; - } - if(!me.get_user() && !me.get_doctype()) { - this.body.html("

    "+__("Select User or DocType to start.")+"

    "); - return; - } - // get permissions - return frappe.call({ - module: "frappe.core", - page: "user_permissions", - method: "get_permissions", - args: { - parent: me.get_user(), - defkey: me.get_doctype(), - defvalue: me.get_user_permission() - }, - callback: function(r) { - me.render(r.message); - } - }); - }, - show_user_permissions_table: function() { - var me = this; - this.table = $("\ - \ - \ -
    ").appendTo(this.body); - - $('

    ' - +__("These restrictions will apply for Document Types where 'Apply User Permissions' is checked for the permission rule and a field with this value is present.") - +'

    ').appendTo(this.body); - - $.each([[__("Allow User"), 150], [__("If Document Type"), 150], [__("Is"),150], ["", 50]], - function(i, col) { - $("") - .html(col[0]) - .css("width", col[1]+"px") - .appendTo(me.table.find("thead tr")); - }); - - - $.each(this.prop_list, function(i, d) { - var row = $("").appendTo(me.table.find("tbody")); - - $("").html('' - +d.parent+'').appendTo(row); - $("").html(d.defkey).appendTo(row); - $("").html(d.defvalue).appendTo(row); - - me.add_delete_button(row, d); - }); - - }, - add_delete_button: function(row, d) { - var me = this; - $("") - .appendTo($("").appendTo(row)) - .attr("data-name", d.name) - .attr("data-user", d.parent) - .attr("data-defkey", d.defkey) - .attr("data-defvalue", d.defvalue) - .click(function() { - return frappe.call({ - module: "frappe.core", - page: "user_permissions", - method: "remove", - args: { - name: $(this).attr("data-name"), - user: $(this).attr("data-user"), - defkey: $(this).attr("data-defkey"), - defvalue: $(this).attr("data-defvalue") - }, - callback: function(r) { - if(r.exc) { - frappe.msgprint(__("Did not remove")); - } else { - me.refresh(); - } - } - }) - }); - }, - - show_add_user_permission: function() { - var me = this; - $("") - .appendTo($('

    ').appendTo(this.body)) - .click(function() { - var d = new frappe.ui.Dialog({ - title: __("Add A New Restriction"), - fields: [ - {fieldtype:"Select", label:__("Allow User"), - options:me.options.users, reqd:1, fieldname:"user"}, - {fieldtype:"Select", label: __("If Document Type"), fieldname:"defkey", - options:me.get_link_names(), reqd:1}, - {fieldtype:"Link", label:__("Is"), fieldname:"defvalue", - options:'[Select]', reqd:1}, - {fieldtype:"Button", label: __("Add"), fieldname:"add"}, - ] - }); - if(me.get_user()) { - d.set_value("user", me.get_user()); - d.get_input("user").prop("disabled", true); - } - if(me.get_doctype()) { - d.set_value("defkey", me.get_doctype()); - d.get_input("defkey").prop("disabled", true); - } - if(me.get_user_permission()) { - d.set_value("defvalue", me.get_user_permission()); - d.get_input("defvalue").prop("disabled", true); - } - - d.fields_dict["defvalue"].get_query = function(txt) { - if(!d.get_value("defkey")) { - frappe.throw(__("Please select Document Type")); - } - - return { - doctype: d.get_value("defkey") - } - }; - - d.get_input("add").click(function() { - var args = d.get_values(); - if(!args) { - return; - } - frappe.call({ - module: "frappe.core", - page: "user_permissions", - method: "add", - args: args, - callback: function(r) { - if(r.exc) { - frappe.msgprint(__("Did not add")); - } else { - me.refresh(); - } - } - }) - d.hide(); - }); - d.show(); - }); - } -}) diff --git a/frappe/core/page/user_permissions/user_permissions.json b/frappe/core/page/user_permissions/user_permissions.json deleted file mode 100644 index ab831e1919..0000000000 --- a/frappe/core/page/user_permissions/user_permissions.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "content": null, - "creation": "2013-01-01 18:50:55", - "docstatus": 0, - "doctype": "Page", - "icon": "fa fa-user", - "idx": 1, - "modified": "2014-05-28 16:53:43.103533", - "modified_by": "Administrator", - "module": "Core", - "name": "user-permissions", - "owner": "Administrator", - "page_name": "user-permissions", - "roles": [], - "script": null, - "standard": "Yes", - "style": null, - "title": "User Permissions Manager" -} \ No newline at end of file diff --git a/frappe/core/page/user_permissions/user_permissions.py b/frappe/core/page/user_permissions/user_permissions.py deleted file mode 100644 index 98d7e90095..0000000000 --- a/frappe/core/page/user_permissions/user_permissions.py +++ /dev/null @@ -1,109 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# MIT License. See license.txt - -from __future__ import unicode_literals -import frappe -from frappe import _ -import frappe.defaults -from frappe.permissions import (can_set_user_permissions, add_user_permission, - remove_user_permission, get_valid_perms) -from frappe.core.doctype.user.user import get_system_users -from frappe.utils.csvutils import UnicodeWriter, read_csv_content_from_uploaded_file -from frappe.defaults import clear_default - -@frappe.whitelist() -def get_users_and_links(): - return { - "users": get_system_users(), - "link_fields": get_doctypes_for_user_permissions() - } - -@frappe.whitelist() -def get_permissions(parent=None, defkey=None, defvalue=None): - if defkey and not can_set_user_permissions(defkey, defvalue): - raise frappe.PermissionError - - conditions, values = _build_conditions(locals()) - - permissions = frappe.db.sql("""select name, parent, defkey, defvalue - from tabDefaultValue - where parent not in ('__default', '__global') - and substr(defkey,1,1)!='_' - and parenttype='User Permission' - {conditions} - order by parent, defkey""".format(conditions=conditions), values, as_dict=True) - - if not defkey: - out = [] - doctypes = get_doctypes_for_user_permissions() - for p in permissions: - if p.defkey in doctypes: - out.append(p) - permissions = out - - return permissions - -def _build_conditions(filters): - conditions = [] - values = {} - for key, value in filters.items(): - if filters.get(key): - conditions.append("and `{key}`=%({key})s".format(key=key)) - values[key] = value - - return "\n".join(conditions), values - -@frappe.whitelist() -def remove(user, name, defkey, defvalue): - if not can_set_user_permissions(defkey, defvalue): - frappe.throw(_("Cannot remove permission for DocType: {0} and Name: {1}").format( - defkey, defvalue), frappe.PermissionError) - - remove_user_permission(defkey, defvalue, user, name) - -@frappe.whitelist() -def add(user, defkey, defvalue): - if not can_set_user_permissions(defkey, defvalue): - frappe.throw(_("Cannot set permission for DocType: {0} and Name: {1}").format( - defkey, defvalue), frappe.PermissionError) - - add_user_permission(defkey, defvalue, user, with_message=True) - -def get_doctypes_for_user_permissions(): - '''Get doctypes for the current user where user permissions are applicable''' - user_roles = frappe.get_roles() - - if "System Manager" in user_roles: - doctypes = set([p.parent for p in get_valid_perms()]) - else: - doctypes = set([p.parent for p in get_valid_perms() if p.set_user_permissions]) - - single_doctypes = set([d.name for d in frappe.get_all("DocType", {"issingle": 1})]) - - return sorted(doctypes.difference(single_doctypes)) - - -@frappe.whitelist() -def get_user_permissions_csv(): - out = [["User Permissions"], ["User", "Document Type", "Value"]] - out += [[a.parent, a.defkey, a.defvalue] for a in get_permissions()] - - csv = UnicodeWriter() - for row in out: - csv.writerow(row) - - frappe.response['result'] = str(csv.getvalue()) - frappe.response['type'] = 'csv' - frappe.response['doctype'] = "User Permissions" - -@frappe.whitelist() -def import_user_permissions(): - frappe.only_for("System Manager") - rows = read_csv_content_from_uploaded_file(ignore_encoding=True) - clear_default(parenttype="User Permission") - - if rows[0][0]!="User Permissions" and rows[1][0] != "User": - frappe.throw(frappe._("Please upload using the same template as download.")) - - for row in rows[2:]: - add_user_permission(row[1], row[2], row[0]) diff --git a/frappe/core/report/permitted_documents_for_user/permitted_documents_for_user.py b/frappe/core/report/permitted_documents_for_user/permitted_documents_for_user.py index 377e2c73bb..632a1f2e45 100644 --- a/frappe/core/report/permitted_documents_for_user/permitted_documents_for_user.py +++ b/frappe/core/report/permitted_documents_for_user/permitted_documents_for_user.py @@ -16,12 +16,12 @@ def execute(filters=None): data = frappe.get_list(doctype, fields=fields, as_list=True, user=user) if show_permissions: - columns = columns + ["Read", "Write", "Create", "Delete", "Submit", "Cancel", "Amend", "Print", "Email", - "Report", "Import", "Export", "Share"] - data = list(data) - for i,item in enumerate(data): - temp = frappe.permissions.get_doc_permissions(frappe.get_doc(doctype, item[0]), False,user) - data[i] = item+(temp.get("read"),temp.get("write"),temp.get("create"),temp.get("delete"),temp.get("submit"),temp.get("cancel"),temp.get("amend"),temp.get("print"),temp.get("email"),temp.get("report"),temp.get("import"),temp.get("export"),temp.get("share"),) + columns = columns + ["Read", "Write", "Create", "Delete", "Submit", "Cancel", "Amend", "Print", "Email", + "Report", "Import", "Export", "Share"] + data = list(data) + for i,item in enumerate(data): + temp = frappe.permissions.get_doc_permissions(frappe.get_doc(doctype, item[0]), False,user) + data[i] = item+(temp.get("read"),temp.get("write"),temp.get("create"),temp.get("delete"),temp.get("submit"),temp.get("cancel"),temp.get("amend"),temp.get("print"),temp.get("email"),temp.get("report"),temp.get("import"),temp.get("export"),temp.get("share"),) return columns, data diff --git a/frappe/custom/doctype/customize_form/test_customize_form.js b/frappe/custom/doctype/customize_form/test_customize_form.js index 144d11a9ae..cac3cc15e6 100644 --- a/frappe/custom/doctype/customize_form/test_customize_form.js +++ b/frappe/custom/doctype/customize_form/test_customize_form.js @@ -7,11 +7,11 @@ QUnit.test("test customize form", function(assert) { let done = assert.async(); frappe.run_serially([ () => frappe.set_route('Form', 'Customize Form'), + () => frappe.timeout(2), () => cur_frm.set_value('doc_type', 'ToDo'), - () => frappe.timeout(2), - () => assert.equal(cur_frm.doc.fields[1].fieldname, 'status'), + () => assert.equal(cur_frm.doc.fields[1].fieldname, 'status', "Status Field"), // open "status" row () => cur_frm.fields_dict.fields.grid.grid_rows[1].toggle_view(), @@ -25,7 +25,7 @@ QUnit.test("test customize form", function(assert) { () => frappe.timeout(0.5), // status still exists - () => assert.equal(cur_frm.doc.fields[1].fieldname, 'status'), + () => assert.equal(cur_frm.doc.fields[1].fieldname, 'status', "Status Field Still Exists"), () => done() ]); }); diff --git a/frappe/database.py b/frappe/database.py index 9b9e9e1446..af265b5857 100644 --- a/frappe/database.py +++ b/frappe/database.py @@ -18,10 +18,10 @@ import redis import frappe.model.meta from frappe.utils import now, get_datetime, cstr from frappe import _ -from types import StringType, UnicodeType +from six import text_type, binary_type from frappe.utils.global_search import sync_global_search from frappe.model.utils.link_count import flush_local_link_count -from six import iteritems +from six import iteritems, text_type class Database: @@ -69,8 +69,8 @@ class Database: use_unicode=True, charset='utf8mb4') self._conn.converter[246]=float self._conn.converter[12]=get_datetime - self._conn.encoders[UnicodeWithAttrs] = self._conn.encoders[UnicodeType] - self._conn.encoders[DateTimeDeltaType] = self._conn.encoders[StringType] + self._conn.encoders[UnicodeWithAttrs] = self._conn.encoders[text_type] + self._conn.encoders[DateTimeDeltaType] = self._conn.encoders[binary_type] MYSQL_OPTION_MULTI_STATEMENTS_OFF = 1 self._conn.set_server_option(MYSQL_OPTION_MULTI_STATEMENTS_OFF) @@ -260,7 +260,7 @@ class Database: else: val = r[i] - if as_utf8 and type(val) is unicode: + if as_utf8 and type(val) is text_type: val = val.encode('utf-8') row_dict[self._cursor.description[i][0]] = val ret.append(row_dict) @@ -289,13 +289,13 @@ class Database: if isinstance(v, (datetime.date, datetime.timedelta, datetime.datetime, long)): if isinstance(v, datetime.date): - v = unicode(v) + v = text_type(v) if formatted: v = formatdate(v) # time elif isinstance(v, (datetime.timedelta, datetime.datetime)): - v = unicode(v) + v = text_type(v) # long elif isinstance(v, long): @@ -306,7 +306,7 @@ class Database: if isinstance(v, float): v=fmt_money(v) elif isinstance(v, int): - v = unicode(v) + v = text_type(v) return v @@ -321,7 +321,7 @@ class Database: val = self.convert_to_simple_type(c, formatted) else: val = c - if as_utf8 and type(val) is unicode: + if as_utf8 and type(val) is text_type: val = val.encode('utf-8') nr.append(val) nres.append(nr) @@ -333,7 +333,7 @@ class Database: for r in res: nr = [] for c in r: - if type(c) is unicode: + if type(c) is text_type: c = c.encode('utf-8') nr.append(self.convert_to_simple_type(c, formatted)) nres.append(nr) @@ -880,10 +880,10 @@ class Database: def escape(self, s, percent=True): """Excape quotes and percent in given string.""" - if isinstance(s, unicode): + if isinstance(s, text_type): s = (s or "").encode("utf-8") - s = unicode(MySQLdb.escape_string(s), "utf-8").replace("`", "\\`") + s = text_type(MySQLdb.escape_string(s), "utf-8").replace("`", "\\`") # NOTE separating % escape, because % escape should only be done when using LIKE operator # or when you use python format string to generate query that already has a %s diff --git a/frappe/defaults.py b/frappe/defaults.py index b4c7ff6452..0e6e23ffdc 100644 --- a/frappe/defaults.py +++ b/frappe/defaults.py @@ -48,25 +48,10 @@ def is_a_user_permission_key(key): return ":" not in key and key != frappe.scrub(key) def get_user_permissions(user=None): - if not user: - user = frappe.session.user - - return build_user_permissions(user) - -def build_user_permissions(user): - out = frappe.cache().hget("user_permissions", user) - if out==None: - out = {} - for key, value in frappe.db.sql("""select defkey, ifnull(defvalue, '') as defvalue - from tabDefaultValue where parent=%s and parenttype='User Permission'""", (user,)): - out.setdefault(key, []).append(value) - - # add profile match - if user not in out.get("User", []): - out.setdefault("User", []).append(user) - - frappe.cache().hset("user_permissions", user, out) - return out + from frappe.core.doctype.user_permission.user_permission \ + import get_user_permissions as _get_user_permissions + '''Return frappe.core.doctype.user_permissions.user_permissions._get_user_permissions (kept for backward compatibility)''' + return _get_user_permissions(user) def get_defaults(user=None): globald = get_defaults_for() diff --git a/frappe/desk/doctype/event/event.py b/frappe/desk/doctype/event/event.py index 66701c9c5a..b77b708ae6 100644 --- a/frappe/desk/doctype/event/event.py +++ b/frappe/desk/doctype/event/event.py @@ -7,7 +7,7 @@ import frappe import json from frappe.utils import (getdate, cint, add_months, date_diff, add_days, - nowdate, get_datetime_str, cstr, get_datetime, now_datetime) + nowdate, get_datetime_str, cstr, get_datetime, now_datetime, format_datetime) from frappe.model.document import Document from frappe.utils.user import get_enabled_system_users from frappe.desk.reportview import get_filters_cond @@ -48,20 +48,22 @@ def send_event_digest(): for user in get_enabled_system_users(): events = get_events(today, today, user.name, for_reminder=True) if events: - text = "" frappe.set_user_lang(user.name, user.language) - text = "

    " + frappe._("Events In Today's Calendar") + "

    " for e in events: + e.starts_on = format_datetime(e.starts_on, 'hh:mm a') if e.all_day: e.starts_on = "All Day" - text += "

    %(starts_on)s: %(subject)s

    %(description)s

    " % e - text += '

    '\ - + frappe._("Daily Event Digest is sent for Calendar Events where reminders are set.")+'

    ' - - frappe.sendmail(recipients=user.email, subject=frappe._("Upcoming Events for Today"), - content = text) + frappe.sendmail( + recipients=user.email, + subject=frappe._("Upcoming Events for Today"), + template="upcoming_events", + args={ + 'events': events, + }, + header=[frappe._("Events in Today's Calendar"), 'blue'] + ) @frappe.whitelist() def get_events(start, end, user=None, for_reminder=False, filters=None): diff --git a/frappe/desk/form/load.py b/frappe/desk/form/load.py index 87e48b0450..ee7fffa242 100644 --- a/frappe/desk/form/load.py +++ b/frappe/desk/form/load.py @@ -70,7 +70,6 @@ def getdoctype(doctype, with_parent=False, cached_timestamp=None): if not docs: docs = get_meta_bundle(doctype) - frappe.response['user_permissions'] = get_user_permissions(docs) frappe.response['user_settings'] = get_user_settings(parent_dt or doctype) if cached_timestamp and docs[0].modified==cached_timestamp: @@ -102,16 +101,6 @@ def get_docinfo(doc=None, doctype=None, name=None): "rating": get_feedback_rating(doc.doctype, doc.name) } -def get_user_permissions(meta): - out = {} - all_user_permissions = frappe.defaults.get_user_permissions() - - for m in meta: - for df in m.get_fields_to_check_permissions(all_user_permissions): - out[df.options] = list(set(all_user_permissions[df.options])) - - return out - def get_attachments(dt, dn): return frappe.get_all("File", fields=["name", "file_name", "file_url", "is_private"], filters = {"attached_to_name": dn, "attached_to_doctype": dt}) diff --git a/frappe/desk/form/meta.py b/frappe/desk/form/meta.py index 4efcf30a7a..00ef2f403d 100644 --- a/frappe/desk/form/meta.py +++ b/frappe/desk/form/meta.py @@ -14,7 +14,7 @@ from frappe.model.utils import render_include from frappe.build import scrub_html_template ###### -from six import iteritems +from six import iteritems, text_type def get_meta(doctype, cached=True): @@ -99,7 +99,7 @@ class FormMeta(Meta): for fname in os.listdir(path): if fname.endswith(".html"): with open(os.path.join(path, fname), 'r') as f: - templates[fname.split('.')[0]] = scrub_html_template(unicode(f.read(), "utf-8")) + templates[fname.split('.')[0]] = scrub_html_template(text_type(f.read(), "utf-8")) self.set("__templates", templates or None) diff --git a/frappe/desk/form/run_method.py b/frappe/desk/form/run_method.py index dc1f267b66..1253cc49b3 100644 --- a/frappe/desk/form/run_method.py +++ b/frappe/desk/form/run_method.py @@ -6,6 +6,7 @@ import json, inspect import frappe from frappe import _ from frappe.utils import cint +from six import text_type @frappe.whitelist() def runserverobj(method, docs=None, dt=None, dn=None, arg=None, args=None): @@ -53,7 +54,7 @@ def make_csv_output(res, dt): """send method response as downloadable CSV file""" import frappe - from cStringIO import StringIO + from six import StringIO import csv f = StringIO() @@ -68,6 +69,6 @@ def make_csv_output(res, dt): f.seek(0) - frappe.response['result'] = unicode(f.read(), 'utf-8') + frappe.response['result'] = text_type(f.read(), 'utf-8') frappe.response['type'] = 'csv' frappe.response['doctype'] = dt.replace(' ','') diff --git a/frappe/desk/page/backups/backups.py b/frappe/desk/page/backups/backups.py index f3517c6953..2d493515b7 100644 --- a/frappe/desk/page/backups/backups.py +++ b/frappe/desk/page/backups/backups.py @@ -76,9 +76,14 @@ def backup_files_and_notify_user(user_email=None): backup_files = backup(with_files=True) get_downloadable_links(backup_files) - subject = "File backup is ready" - message = frappe.render_template('frappe/templates/emails/file_backup_notification.html', backup_files, is_path=True) - frappe.sendmail(recipients=[user_email], subject=subject, message=message) + subject = _("File backup is ready") + frappe.sendmail( + recipients=[user_email], + subject=subject, + template="file_backup_notification", + args=backup_files, + header=[subject, 'green'] + ) def get_downloadable_links(backup_files): for key in ['backup_path_files', 'backup_path_private_files']: diff --git a/frappe/desk/page/chat/chat.py b/frappe/desk/page/chat/chat.py index a0ea8b7393..19f4d4cae4 100644 --- a/frappe/desk/page/chat/chat.py +++ b/frappe/desk/page/chat/chat.py @@ -6,6 +6,7 @@ import frappe from frappe.desk.notifications import delete_notification_count_for from frappe.core.doctype.user.user import STANDARD_USERS from frappe.utils import cint +from frappe import _ @frappe.whitelist() def get_list(arg=None): @@ -132,11 +133,13 @@ def _notify(contact, txt, subject=None): frappe.sendmail(\ recipients=contact, sender= frappe.db.get_value("User", frappe.session.user, "email"), - subject=subject or "New Message from " + get_fullname(frappe.session.user), - message=frappe.get_template("templates/emails/new_message.html").render({ + subject=subject or _("New Message from {0}").format(get_fullname(frappe.session.user)), + template="new_message", + args={ "from": get_fullname(frappe.session.user), "message": txt, "link": get_url() - })) + }, + header=[_('New Message'), 'orange']) except frappe.OutgoingEmailError: pass diff --git a/frappe/desk/page/setup_wizard/install_fixtures.py b/frappe/desk/page/setup_wizard/install_fixtures.py index 7e67907a3b..5da25932ca 100644 --- a/frappe/desk/page/setup_wizard/install_fixtures.py +++ b/frappe/desk/page/setup_wizard/install_fixtures.py @@ -22,7 +22,7 @@ def update_genders_and_salutations(): try: doc.insert(ignore_permissions=True) - except frappe.DuplicateEntryError, e: + except frappe.DuplicateEntryError as e: # pass DuplicateEntryError and continue if e.args and e.args[0]==doc.doctype and e.args[1]==doc.name: # make sure DuplicateEntryError is for the exact same doc and not a related doc diff --git a/frappe/desk/query_builder.py b/frappe/desk/query_builder.py index 4e23318d07..0c52bd8beb 100644 --- a/frappe/desk/query_builder.py +++ b/frappe/desk/query_builder.py @@ -8,6 +8,7 @@ out = frappe.response from frappe.utils import cint import frappe.defaults +from six import text_type def get_sql_tables(q): if q.find('WHERE') != -1: @@ -239,17 +240,17 @@ def runquery_csv(): rows = [[rep_name], out['colnames']] + out['values'] - from cStringIO import StringIO + from six import StringIO import csv f = StringIO() writer = csv.writer(f) for r in rows: # encode only unicode type strings and not int, floats etc. - writer.writerow(map(lambda v: isinstance(v, unicode) and v.encode('utf-8') or v, r)) + writer.writerow(map(lambda v: isinstance(v, text_type) and v.encode('utf-8') or v, r)) f.seek(0) - out['result'] = unicode(f.read(), 'utf-8') + out['result'] = text_type(f.read(), 'utf-8') out['type'] = 'csv' out['doctype'] = rep_name diff --git a/frappe/desk/reportview.py b/frappe/desk/reportview.py index fbb363300f..f0b156f673 100644 --- a/frappe/desk/reportview.py +++ b/frappe/desk/reportview.py @@ -10,6 +10,7 @@ import frappe.permissions import MySQLdb from frappe.model.db_query import DatabaseQuery from frappe import _ +from six import text_type @frappe.whitelist() def get(): @@ -145,16 +146,16 @@ def export_query(): # convert to csv import csv - from cStringIO import StringIO + from six import StringIO f = StringIO() writer = csv.writer(f) for r in data: # encode only unicode type strings and not int, floats etc. - writer.writerow(map(lambda v: isinstance(v, unicode) and v.encode('utf-8') or v, r)) + writer.writerow(map(lambda v: isinstance(v, text_type) and v.encode('utf-8') or v, r)) f.seek(0) - frappe.response['result'] = unicode(f.read(), 'utf-8') + frappe.response['result'] = text_type(f.read(), 'utf-8') frappe.response['type'] = 'csv' frappe.response['doctype'] = doctype diff --git a/frappe/docs/assets/img/desk/animated_line_graph.gif b/frappe/docs/assets/img/desk/animated_line_graph.gif new file mode 100644 index 0000000000..0b0e7b212c Binary files /dev/null and b/frappe/docs/assets/img/desk/animated_line_graph.gif differ diff --git a/frappe/docs/assets/img/desk/bar_graph.png b/frappe/docs/assets/img/desk/bar_graph.png index d25254af6d..b3bd89cc88 100644 Binary files a/frappe/docs/assets/img/desk/bar_graph.png and b/frappe/docs/assets/img/desk/bar_graph.png differ diff --git a/frappe/docs/assets/img/desk/line_graph.png b/frappe/docs/assets/img/desk/line_graph.png deleted file mode 100644 index 02c60c7c18..0000000000 Binary files a/frappe/docs/assets/img/desk/line_graph.png and /dev/null differ diff --git a/frappe/docs/assets/img/desk/line_graph_sales.png b/frappe/docs/assets/img/desk/line_graph_sales.png new file mode 100644 index 0000000000..0e70ae0031 Binary files /dev/null and b/frappe/docs/assets/img/desk/line_graph_sales.png differ diff --git a/frappe/docs/assets/img/desk/percentage_graph.png b/frappe/docs/assets/img/desk/percentage_graph.png new file mode 100644 index 0000000000..3a25d59479 Binary files /dev/null and b/frappe/docs/assets/img/desk/percentage_graph.png differ diff --git a/frappe/docs/contents.html b/frappe/docs/contents.html deleted file mode 100644 index 59d0b4459d..0000000000 --- a/frappe/docs/contents.html +++ /dev/null @@ -1,10 +0,0 @@ - - -

    Table of Contents

    -
    - -{% include "templates/includes/full_index.html" %} - - - - diff --git a/frappe/docs/contents.py b/frappe/docs/contents.py deleted file mode 100644 index c23737a3c0..0000000000 --- a/frappe/docs/contents.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt - -from __future__ import unicode_literals -import frappe -from frappe.website.utils import get_full_index - -def get_context(context): - context.full_index = get_full_index() diff --git a/frappe/docs/index.html b/frappe/docs/index.html deleted file mode 100644 index a70cba3109..0000000000 --- a/frappe/docs/index.html +++ /dev/null @@ -1,57 +0,0 @@ - - - - - - -
    -
    -
    -
    -

    Superhero Web Framework

    -

    Build extensions to ERPNext or make your own app

    -
    -
    -
    - -
    -
    -
    -
    -
    - - - -
    -
    -
    -

    Frappe is a full stack web application framework written in Python, -Javascript, HTML/CSS with MySQL as the backend. It was built for ERPNext -but is pretty generic and can be used to build database driven apps.

    - -

    The key differece in Frappe compared to other frameworks is that Frappe -is that meta-data is also treated as data and is used to build front-ends -very easily. Frappe comes with a full blown admin UI called the Desk -that handles forms, navigation, lists, menus, permissions, file attachment -and much more out of the box.

    - -

    Frappe also has a plug-in architecture that can be used to build plugins -to ERPNext.

    - -

    Frappe Framework was designed to build ERPNext, open source -ERP for managing small and medium sized businesses.

    - -

    Get started with the Tutorial

    - -
    -
    -
    - - - - - - \ No newline at end of file diff --git a/frappe/docs/index.md b/frappe/docs/index.md new file mode 100644 index 0000000000..1a88720be9 --- /dev/null +++ b/frappe/docs/index.md @@ -0,0 +1,25 @@ +# Frappé Framework + +### Tutorials, API documentation and Model Reference + +Frappé is a full stack web application framework written in Python, +Javascript, HTML/CSS with MySQL as the backend. It was built for ERPNext +but is pretty generic and can be used to build database driven apps. + +The key differece in Frappé compared to other frameworks is that Frappé +is that meta-data is also treated as data and is used to build front-ends +very easily. Frappé comes with a full blown admin UI called the **Desk** +that handles forms, navigation, lists, menus, permissions, file attachment +and much more out of the box. + +Frappé also has a plug-in architecture that can be used to build plugins +to ERPNext. + +Frappé Framework was designed to build [ERPNext](https://erpnext.com), open source +ERP for managing small and medium sized businesses. + +[Get started with the Tutorial](/docs/user/) + +### Feedback + +You're encouraged to help improve the quality of this documentation, by sending a pull request on the [GitHub Repository](https://github.com/frappe/erpnext). If you would like to have a discussion regarding the documentation, you can do so [at the forum](https://discuss.erpnext.com). \ No newline at end of file diff --git a/frappe/docs/index.txt b/frappe/docs/index.txt deleted file mode 100644 index 4bdfdc4ac8..0000000000 --- a/frappe/docs/index.txt +++ /dev/null @@ -1,6 +0,0 @@ -assets -user -contents -current -install -license diff --git a/frappe/docs/install.md b/frappe/docs/install.md deleted file mode 100644 index 7350f8f4ab..0000000000 --- a/frappe/docs/install.md +++ /dev/null @@ -1,30 +0,0 @@ - - -# Installation - -Frappe Framework is based on the Frappe Framework, a full stack web framework based on Python, MariaDB, Redis, Node. - -To intall Frappe Framework, you will have to install the Frappe Bench, the command-line, package manager and site manager for Frappe Framework. For more details, read the Bench README. - -After you have installed Frappe Bench, go to you bench folder, which is `frappe.bench` by default and setup **frappe**. - - bench get-app frappe {{ source_link }} - -Then create a new site to install the app. - - bench new-site mysite - -This will create a new folder in your `/sites` directory and create a new database for this site. - -Next, install frappe in this site - - bench --site mysite install-app frappe - -To run this locally, run - - bench start - -Fire up your browser and go to http://localhost:8000 and you should see the login screen. Login as **Administrator** and **admin** (or the password you set at the time of `new-site`) and you are set. - - - \ No newline at end of file diff --git a/frappe/docs/license.html b/frappe/docs/license.html deleted file mode 100644 index 602685d65f..0000000000 --- a/frappe/docs/license.html +++ /dev/null @@ -1,16 +0,0 @@ - - -

    MIT

    - -

    The MIT License (MIT)

    - -

    Copyright (c) 2016 Frappe Technologies Pvt. Ltd.

    - -

    Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

    - -

    The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

    - -

    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

    - - - \ No newline at end of file diff --git a/frappe/docs/user/en/bench/guides/diagnosing-the-scheduler.md b/frappe/docs/user/en/bench/guides/diagnosing-the-scheduler.md index a0661dbb17..492205cf5b 100755 --- a/frappe/docs/user/en/bench/guides/diagnosing-the-scheduler.md +++ b/frappe/docs/user/en/bench/guides/diagnosing-the-scheduler.md @@ -1,3 +1,5 @@ +# Diagnosing The Scheduler + If you're experiencing delays in scheduled jobs or they don't seem to run, you can run the several commands to diagnose the issue. diff --git a/frappe/docs/user/en/bench/guides/index.md b/frappe/docs/user/en/bench/guides/index.md index 0dff60b400..8ab035a2f5 100644 --- a/frappe/docs/user/en/bench/guides/index.md +++ b/frappe/docs/user/en/bench/guides/index.md @@ -1 +1,5 @@ +# Guides + + + {index} \ No newline at end of file diff --git a/frappe/docs/user/en/bench/guides/manual-setup.md b/frappe/docs/user/en/bench/guides/manual-setup.md index a0c55f374f..bc9272b3b3 100755 --- a/frappe/docs/user/en/bench/guides/manual-setup.md +++ b/frappe/docs/user/en/bench/guides/manual-setup.md @@ -1,3 +1,5 @@ +# Manual Setup + Manual Setup -------------- @@ -49,7 +51,7 @@ Basic Usage * Add site - Frappe apps are run by frappe sites and you will have to create at least one + Frappé apps are run by frappe sites and you will have to create at least one site. The new-site command allows you to do that. bench new-site site1.local @@ -60,7 +62,7 @@ Basic Usage bench start - To login to Frappe / ERPNext, open your browser and go to `localhost:8000` + To login to Frappé / ERPNext, open your browser and go to `localhost:8000` The default user name is "Administrator" and password is what you set when you created the new site. diff --git a/frappe/docs/user/en/bench/guides/settings-limits.md b/frappe/docs/user/en/bench/guides/settings-limits.md index 8d2a9e4b1d..0960b98843 100644 --- a/frappe/docs/user/en/bench/guides/settings-limits.md +++ b/frappe/docs/user/en/bench/guides/settings-limits.md @@ -1,6 +1,6 @@ # Setting Limits for your Site -Frappe v7 has added support for setting limits and restrictions for your site. +Frappé v7 has added support for setting limits and restrictions for your site. These restrictions are set in the `site_config.json` file inside the site's folder. { @@ -36,4 +36,4 @@ Example: You can check your usage by opening the "Usage Info" page from the toolbar / AwesomeBar. A limit will only show up on the page if it has been set. -Doctype Saved +Doctype Saved diff --git a/frappe/docs/user/en/bench/guides/setup-multitenancy.md b/frappe/docs/user/en/bench/guides/setup-multitenancy.md index 8f204cd9eb..1404fd707c 100755 --- a/frappe/docs/user/en/bench/guides/setup-multitenancy.md +++ b/frappe/docs/user/en/bench/guides/setup-multitenancy.md @@ -1,3 +1,5 @@ +# Setup Multitenancy + Assuming that you've already got your first site running and you've performed the [production deployment steps](setup-production.html), this section explains how to host your second site (and more). Your first site is automatically set as default site. You can diff --git a/frappe/docs/user/en/bench/guides/setup-production.md b/frappe/docs/user/en/bench/guides/setup-production.md index 414aab10e2..171adc3b9f 100644 --- a/frappe/docs/user/en/bench/guides/setup-production.md +++ b/frappe/docs/user/en/bench/guides/setup-production.md @@ -1,3 +1,5 @@ +# Setup Production + You can setup the bench for production use by configuring two programs, Supervisor and nginx. If you want to revert your Production Setup to Development Setup refer to [these commands](https://github.com/frappe/bench/wiki/Stopping-Production-and-starting-Development) ####Easy Production Setup @@ -8,7 +10,7 @@ These steps are automated if you run `sudo bench setup production` Supervisor ---------- -Supervisor makes sure that the process that power the Frappe system keep running +Supervisor makes sure that the process that power the Frappé system keep running and it restarts them if they happen to crash. You can generate the required configuration for supervisor using the command `bench setup supervisor`. The configuration will be available in `config/supervisor.conf` directory. You can diff --git a/frappe/docs/user/en/bench/index.md b/frappe/docs/user/en/bench/index.md index 0dff60b400..39e8cc0ffe 100644 --- a/frappe/docs/user/en/bench/index.md +++ b/frappe/docs/user/en/bench/index.md @@ -1 +1,3 @@ +# Bench + {index} \ No newline at end of file diff --git a/frappe/docs/user/en/bench/resources/background-services.md b/frappe/docs/user/en/bench/resources/background-services.md index d019d25d8c..253f858c19 100755 --- a/frappe/docs/user/en/bench/resources/background-services.md +++ b/frappe/docs/user/en/bench/resources/background-services.md @@ -1,3 +1,5 @@ +# Background Services + External services ----------------- @@ -6,7 +8,7 @@ External services * nginx (for production deployment) * supervisor (for production deployment) -Frappe Processes +Frappé Processes ---------------- @@ -19,12 +21,12 @@ Frappe Processes * Redis Worker Processes - * The Celery worker processes execute background jobs in the Frappe system. + * The Celery worker processes execute background jobs in the Frappé system. These processes are automatically started when `bench start` is run and for production are configured in supervisor configuration. * Scheduler Process * The Scheduler process schedules enqeueing of scheduled jobs in the - Frappe system. This process is automatically started when `bench start` is + Frappé system. This process is automatically started when `bench start` is run and for production are configured in supervisor configuration. \ No newline at end of file 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 b412d5f78a..054cd072f6 100755 --- a/frappe/docs/user/en/bench/resources/bench-commands-cheatsheet.md +++ b/frappe/docs/user/en/bench/resources/bench-commands-cheatsheet.md @@ -1,3 +1,5 @@ +# Bench Commands Cheatsheet + ### General Usage * `bench --version` - Show bench version * `bench src` - Show bench repo directory diff --git a/frappe/docs/user/en/bench/resources/bench-procfile.md b/frappe/docs/user/en/bench/resources/bench-procfile.md index 61ecfc6eec..9a9176556c 100755 --- a/frappe/docs/user/en/bench/resources/bench-procfile.md +++ b/frappe/docs/user/en/bench/resources/bench-procfile.md @@ -1,3 +1,5 @@ +# Bench Procfile + `bench start` uses [honcho](http://honcho.readthedocs.org) to manage multiple processes in **developer mode**. ### Processes diff --git a/frappe/docs/user/en/bench/resources/index.md b/frappe/docs/user/en/bench/resources/index.md index 0dff60b400..48852466ed 100644 --- a/frappe/docs/user/en/bench/resources/index.md +++ b/frappe/docs/user/en/bench/resources/index.md @@ -1 +1,5 @@ +# Resources + + + {index} \ No newline at end of file diff --git a/frappe/docs/user/en/guides/app-development/adding-custom-button-to-form.md b/frappe/docs/user/en/guides/app-development/adding-custom-button-to-form.md index 29a1a608b3..fba6397b94 100644 --- a/frappe/docs/user/en/guides/app-development/adding-custom-button-to-form.md +++ b/frappe/docs/user/en/guides/app-development/adding-custom-button-to-form.md @@ -1,3 +1,5 @@ +# Adding Custom Button To Form + To create a custom button on your form, you need to edit the javascript file associated to your doctype. For example, If you want to add a custom button to User form then you must edit `user.js`. In this file, you need to write a new method `add_custom_button` which should add a button to your form. @@ -22,7 +24,7 @@ We should edit `frappe\core\doctype\user\user.js` You should be seeing a button on user form as shown below, -Custom Button +Custom Button diff --git a/frappe/docs/user/en/guides/app-development/adding-module-icons-on-desktop.md b/frappe/docs/user/en/guides/app-development/adding-module-icons-on-desktop.md index 44b014267d..4fbf6fda1e 100755 --- a/frappe/docs/user/en/guides/app-development/adding-module-icons-on-desktop.md +++ b/frappe/docs/user/en/guides/app-development/adding-module-icons-on-desktop.md @@ -1,3 +1,5 @@ +# Adding Module Icons On Desktop + 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 diff --git a/frappe/docs/user/en/guides/app-development/custom-module-icon.md b/frappe/docs/user/en/guides/app-development/custom-module-icon.md index 609f7f3803..4a001b9c2c 100755 --- a/frappe/docs/user/en/guides/app-development/custom-module-icon.md +++ b/frappe/docs/user/en/guides/app-development/custom-module-icon.md @@ -1,3 +1,5 @@ +# Custom Module Icon + 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. @@ -8,10 +10,10 @@ Example: def get_data(): return { - "Frappe Apps": { + "Frappé Apps": { "color": "orange", "icon": "assets/frappe/images/frappe.svg", - "label": _("Frappe.io Portal"), + "label": _("Frappé.io Portal"), "type": "module" } } diff --git a/frappe/docs/user/en/guides/app-development/dialogs-types.md b/frappe/docs/user/en/guides/app-development/dialogs-types.md index a1aa4f9a07..99902aed71 100755 --- a/frappe/docs/user/en/guides/app-development/dialogs-types.md +++ b/frappe/docs/user/en/guides/app-development/dialogs-types.md @@ -1,8 +1,10 @@ -Frappe provide a group of standard dialogs that are very usefull while coding. +# Dialogs Types + +Frappé provide a group of standard dialogs that are very usefull while coding. ## Alert Dialog - + Is helpfull for show a non-obstructive message. @@ -16,7 +18,7 @@ This dialog have 2 parameters `txt`that is the message and `seconds` that is the ## Prompt Dialog - + Is helpful for ask a value for the user @@ -42,7 +44,7 @@ This dialog have 4 parameters, they are: --- ## Confirm Dialog - + Usefull to get a confirmation from the user before do an action @@ -68,7 +70,7 @@ This dialog have 3 arguments, they are: ## Message Print - + Is helpfull for show a informational dialog for the user; @@ -91,7 +93,7 @@ This dialog have 2 arguments, they are: ### Custom Dialog - + Frappé provide too a `Class` that you can extend and build your own custom dialogs diff --git a/frappe/docs/user/en/guides/app-development/executing-code-on-doctype-events.md b/frappe/docs/user/en/guides/app-development/executing-code-on-doctype-events.md index a5b57e0920..ff03f85236 100755 --- a/frappe/docs/user/en/guides/app-development/executing-code-on-doctype-events.md +++ b/frappe/docs/user/en/guides/app-development/executing-code-on-doctype-events.md @@ -1,3 +1,5 @@ +# Executing Code On Doctype Events + 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 diff --git a/frappe/docs/user/en/guides/app-development/exporting-customizations.md b/frappe/docs/user/en/guides/app-development/exporting-customizations.md index 12592ecb02..710db1d3e5 100644 --- a/frappe/docs/user/en/guides/app-development/exporting-customizations.md +++ b/frappe/docs/user/en/guides/app-development/exporting-customizations.md @@ -4,12 +4,12 @@ A common use case is to extend a DocType via Custom Fields and Property Setters 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/fetch-custom-field-value-from-master-to-all-related-transactions.md b/frappe/docs/user/en/guides/app-development/fetch-custom-field-value-from-master-to-all-related-transactions.md index d6e495ca1e..7e47d63f5f 100755 --- a/frappe/docs/user/en/guides/app-development/fetch-custom-field-value-from-master-to-all-related-transactions.md +++ b/frappe/docs/user/en/guides/app-development/fetch-custom-field-value-from-master-to-all-related-transactions.md @@ -5,11 +5,11 @@ Let's say, there is a custom field "VAT Number" in Supplier, which should be fet #### 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/en/guides/app-development/generating-docs.md b/frappe/docs/user/en/guides/app-development/generating-docs.md index 8494533d22..3f08c68ec9 100755 --- a/frappe/docs/user/en/guides/app-development/generating-docs.md +++ b/frappe/docs/user/en/guides/app-development/generating-docs.md @@ -1,18 +1,17 @@ # 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. +Frappé 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). + +Version 8.7 onwards, these will be generated in a target app. ## 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)""" @@ -29,16 +28,6 @@ The first step is to setup the docs folder. For that you must create a new file pass -#### 1.2. Generate `/docs` - -To generate the docs for the `current` version, go to the command line and write - - bench --site [site] build-docs [appname] - -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. @@ -51,64 +40,31 @@ Some quick tips: ### 3. Linking -While linking make sure you add `{{ docs_base_url }}` to all your links. +While linking make sure you add `/docs` to all your links. - {% raw %}Link Description{% endraw %} + {% 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 - -The output docs are generated in your `docs/appname` folder using the `write-docs` command. - ---- - -## Viewing Locally - -To test your docs locally, add a `--local` option to the `write-docs` command. - - bench --site [site] write-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 + {% raw %}{% endraw %} --- -## 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 push your documentation there. +## Building Docs -1. To easily publish your docs on gh-pages, commit and push your `apps/docs` folder on you master branch first. -2. The `/docs` generation will also generate a `/docs` folder in your bench, parallel to your `/sites` folder. e.g. `/frappe-bench/docs` -3. Generate you documentation using the `write-docs` command. -4. Go to your docs folder `cd docs/myapp` -5. Checkout the gh-pages branch `git checkout --orphan gh-pages` -6. Push your documentation to Github. +You must create a new app that will have the output of the docs, which is called the "target" app. For example, the docs for ERPNext are hosted at erpnext.org, which is based on the app "foundation". You can create a new app just to push docs of any other app. -Note > The branch name `gh-pages` is only if you are using GitHub. If you are hosting this on any other static file server, you can create any other orphan branch instead. +To output docs to another app, -Putting it all together: + bench --site [site] build-docs [app] --target [target_app] - # build the apps/docs folder and write the compiled docs at docs/appname - bench --site [site] build-docs [appname] +This will create a new folder `/docs` inside the `www` folder of the target app and generate automatic docs (from code), model references and copy user docs and assets. - # commit to the gh-pages branch (for GitHub Pages) - cd docs/appname - git checkout --orphan gh-pages - git remote add origin [remote git repository] - git add * - git commit -m "Documentation Initialization" - git push origin gh-pages +To view the docs, just go the the `/docs` url on your target app. Example: -To check your documentation online go to: https://[orgname].github.io/[reponame] + https://erpnext.org/docs diff --git a/frappe/docs/user/en/guides/app-development/how-enable-developer-mode-in-frappe.md b/frappe/docs/user/en/guides/app-development/how-enable-developer-mode-in-frappe.md index 5c47385fbd..b3c769828a 100755 --- a/frappe/docs/user/en/guides/app-development/how-enable-developer-mode-in-frappe.md +++ b/frappe/docs/user/en/guides/app-development/how-enable-developer-mode-in-frappe.md @@ -1,3 +1,5 @@ +# How Enable Developer Mode In Frappé + 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: diff --git a/frappe/docs/user/en/guides/app-development/how-to-create-custom-fields-during-app-installation.md b/frappe/docs/user/en/guides/app-development/how-to-create-custom-fields-during-app-installation.md index 20b7fdce30..0b71521116 100755 --- a/frappe/docs/user/en/guides/app-development/how-to-create-custom-fields-during-app-installation.md +++ b/frappe/docs/user/en/guides/app-development/how-to-create-custom-fields-during-app-installation.md @@ -1,6 +1,8 @@ +# How To Create Custom Fields During App Installation + 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. +To do this, add the new custom fields that your app requires, using the Frappé web application. In your `hooks.py` file, add `"Custom Fields"` diff --git a/frappe/docs/user/en/guides/app-development/how-to-improve-a-standard-control.md b/frappe/docs/user/en/guides/app-development/how-to-improve-a-standard-control.md index e3c8ef82c0..df137db2a9 100755 --- a/frappe/docs/user/en/guides/app-development/how-to-improve-a-standard-control.md +++ b/frappe/docs/user/en/guides/app-development/how-to-improve-a-standard-control.md @@ -1,3 +1,5 @@ +# How To Improve A Standard Control + 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: diff --git a/frappe/docs/user/en/guides/app-development/insert-a-document-via-api.md b/frappe/docs/user/en/guides/app-development/insert-a-document-via-api.md index aab15b4ac9..c3a8358ddb 100755 --- a/frappe/docs/user/en/guides/app-development/insert-a-document-via-api.md +++ b/frappe/docs/user/en/guides/app-development/insert-a-document-via-api.md @@ -1,3 +1,5 @@ +# Insert A Document Via Api + You can insert documents via a script using the `frappe.get_doc` method ### Examples: diff --git a/frappe/docs/user/en/guides/app-development/overriding-link-query-by-custom-script.md b/frappe/docs/user/en/guides/app-development/overriding-link-query-by-custom-script.md index cc8c8058fd..4e78d7fd58 100755 --- a/frappe/docs/user/en/guides/app-development/overriding-link-query-by-custom-script.md +++ b/frappe/docs/user/en/guides/app-development/overriding-link-query-by-custom-script.md @@ -1,3 +1,5 @@ +# Overriding Link Query By Custom Script + You can override the standard link query by using `set_query` ### 1. Adding Fitlers diff --git a/frappe/docs/user/en/guides/app-development/running-background-jobs.md b/frappe/docs/user/en/guides/app-development/running-background-jobs.md index ae2972b855..f46349d36f 100644 --- a/frappe/docs/user/en/guides/app-development/running-background-jobs.md +++ b/frappe/docs/user/en/guides/app-development/running-background-jobs.md @@ -1,6 +1,8 @@ +# Running Background Jobs + Sometimes you may not want a user request to be executed immediately but added to a queue that will be executed by a background worker. The advantage of doing this is that your web workers remain free to execute other requests and longer jobs do not eat up all of your resources. -From version 7, Frappe uses Python RQ to run background jobs. +From version 7, Frappé uses Python RQ to run background jobs. To enqueue a job, diff --git a/frappe/docs/user/en/guides/app-development/single-type-doctype.md b/frappe/docs/user/en/guides/app-development/single-type-doctype.md index ee5c24b492..3565246506 100755 --- a/frappe/docs/user/en/guides/app-development/single-type-doctype.md +++ b/frappe/docs/user/en/guides/app-development/single-type-doctype.md @@ -1,3 +1,5 @@ +# Single Type Doctype + 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. @@ -6,4 +8,4 @@ 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 +In Frappé, Single types are **System Settings** and **Customize Form** \ No newline at end of file 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 cc60f6d0b8..776742d4ea 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,3 +1,5 @@ +# Trigger Event On Deletion Of Grid Row + 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: 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 f6242babb7..2de921551f 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,8 @@ -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. +# Using Html Templates In Javascript -> Note 1: In Frappe we use the Jinja-like `{% raw %}{%{% endraw %}` tags to embed code rather than the standard `<%` +Often while building javascript interfaces, there is a need to render DOM as an HTML template. Frappé Framework uses John Resig's Microtemplate script to render HTML templates in the Desk application. + +> Note 1: In Frappé 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/automated-testing/qunit-testing.md b/frappe/docs/user/en/guides/automated-testing/qunit-testing.md index ac7a1f3ccf..55a1242e51 100644 --- a/frappe/docs/user/en/guides/automated-testing/qunit-testing.md +++ b/frappe/docs/user/en/guides/automated-testing/qunit-testing.md @@ -1,4 +1,4 @@ -# UI Testing with Frappe API +# UI Testing with Frappé API You can either write integration tests, or directly write tests in Javascript using [QUnit](http://api.qunitjs.com/) @@ -12,7 +12,7 @@ To run your files, you can use the **Test Runner**. The **Test Runner** gives a In the CI, all QUnit tests are run by the **Test Runner** using `frappe/tests/test_test_runner.py` - + ### Running Tests diff --git a/frappe/docs/user/en/guides/automated-testing/unit-testing.md b/frappe/docs/user/en/guides/automated-testing/unit-testing.md index 730ae792de..b5c2a8b8b2 100755 --- a/frappe/docs/user/en/guides/automated-testing/unit-testing.md +++ b/frappe/docs/user/en/guides/automated-testing/unit-testing.md @@ -2,12 +2,12 @@ ## 1.Introduction -Frappe provides some basic tooling to quickly write automated tests. There are some basic rules: +Frappé 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. Frappé 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_`. @@ -159,7 +159,7 @@ It’s designed for the CI Jenkins, but will work for anything else that underst #### Example (for `test_event.py`): - # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors + # Copyright (c) 2015, Frappé Technologies Pvt. Ltd. and Contributors # MIT License. See license.txt import frappe diff --git a/frappe/docs/user/en/guides/basics/apps.md b/frappe/docs/user/en/guides/basics/apps.md index 5805ad38af..2498992172 100755 --- a/frappe/docs/user/en/guides/basics/apps.md +++ b/frappe/docs/user/en/guides/basics/apps.md @@ -1,6 +1,6 @@ -# Frappe Apps +# Frappé Apps -Frappe Apps are Python packages which use the Frappe platform. They can live +Frappé Apps are Python packages which use the Frappé 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. @@ -8,7 +8,7 @@ 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 +Frappé 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. @@ -58,7 +58,7 @@ The boiler plate contains just enough files to show your app icon on the [Desk]. #### `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 +with other parts of Frappé or Frappé 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. @@ -75,7 +75,7 @@ now, it just contains the details you entered during app creation. #### `modules.txt` -Modules in Frappe help you organize Documents in Frappe and they are defined in +Modules in Frappé help you organize Documents in Frappé 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 diff --git a/frappe/docs/user/en/guides/basics/frappe_ajax_call.md b/frappe/docs/user/en/guides/basics/frappe_ajax_call.md index a92356843a..7f5294015e 100644 --- a/frappe/docs/user/en/guides/basics/frappe_ajax_call.md +++ b/frappe/docs/user/en/guides/basics/frappe_ajax_call.md @@ -1,4 +1,6 @@ -In Frappe Framework, you can manage ajax calls via frappe.call. The frappe.call works in asynchronous manner ie. send requests and handle response via callback mechanism. +# Frappé Ajax Call + +In Frappé Framework, you can manage ajax calls via frappe.call. The frappe.call works in asynchronous manner ie. send requests and handle response via callback mechanism. ## frappe.call Structure diff --git a/frappe/docs/user/en/guides/basics/hooks.md b/frappe/docs/user/en/guides/basics/hooks.md index 5a3eabf419..9fb71f7800 100755 --- a/frappe/docs/user/en/guides/basics/hooks.md +++ b/frappe/docs/user/en/guides/basics/hooks.md @@ -1,14 +1,14 @@ # 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. +Hooks are the duct tape of the Frappé system. Hooks allow you to "hook" in to +functionality and events of other parts of the Frappé system. Following are the +official hooks from Frappé. ### Application Name and Details 1. `app_name` - slugified name with underscores e.g. "shopping\_cart" -2. `app_title` - full title name e.g. "Frappe" +2. `app_title` - full title name e.g. "Frappé" 3. `app_publisher` 4. `app_description` 5. `app_version` @@ -31,7 +31,7 @@ Note, the `before_install` and `after_install` hooks are called with no argument ### Boot Session -After a successful login, the Frappe JS Client requests for a resource called +After a successful login, the Frappé 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 @@ -123,7 +123,7 @@ 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. +By default, for every email, a footer with content, "Sent via Frappé" is sent. You can customize this globally by adding a `mail_footer` hook. The hook should be a dotted path to a variable. @@ -144,7 +144,7 @@ Eg, ### 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 +methods that invalidate your caches when Frappé tries to clear cache for all website related pages. ### Document hooks diff --git a/frappe/docs/user/en/guides/basics/install.md b/frappe/docs/user/en/guides/basics/install.md index 3afa43a37c..cf65752cf1 100755 --- a/frappe/docs/user/en/guides/basics/install.md +++ b/frappe/docs/user/en/guides/basics/install.md @@ -1,10 +1,10 @@ -# Installing Frappe +# Installing Frappé -## Frappe bench +## Frappé 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 +develop Frappé 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/) +For more info, see [Frappé Bench](https://github.com/frappe/bench/) diff --git a/frappe/docs/user/en/guides/basics/site_config.md b/frappe/docs/user/en/guides/basics/site_config.md index cf56db9e9a..4f4407271e 100755 --- a/frappe/docs/user/en/guides/basics/site_config.md +++ b/frappe/docs/user/en/guides/basics/site_config.md @@ -28,7 +28,7 @@ Example: ### Remote Database Host Settings - `db_host`: Database host if not `localhost`. -To connect to a remote database server using ssl, you must first configure the database host to accept SSL connections. An example of how to do this is available at https://www.digitalocean.com/community/tutorials/how-to-configure-ssl-tls-for-mysql-on-ubuntu-16-04. After you do the configuration, set the following three options. All options must be set for Frappe to attempt to connect using SSL. +To connect to a remote database server using ssl, you must first configure the database host to accept SSL connections. An example of how to do this is available at https://www.digitalocean.com/community/tutorials/how-to-configure-ssl-tls-for-mysql-on-ubuntu-16-04. After you do the configuration, set the following three options. All options must be set for Frappé to attempt to connect using SSL. - `db_ssl_ca`: Full path to the ca.pem file used for connecting to a database host using ssl. Example value is `"/etc/mysql/ssl/ca.pem"`. - `db_ssl_cert`: Full path to the cert.pem file used for connecting to a database host using ssl. Example value is `"/etc/mysql/ssl/client-cert.pem"`. - `db_ssl_key`: Full path to the key.pem file used for connecting to a database host using ssl. Example value is `"/etc/mysql/ssl/client-key.pem"`. diff --git a/frappe/docs/user/en/guides/basics/sites.md b/frappe/docs/user/en/guides/basics/sites.md index 00b0007007..abf360cdf1 100755 --- a/frappe/docs/user/en/guides/basics/sites.md +++ b/frappe/docs/user/en/guides/basics/sites.md @@ -2,7 +2,7 @@ ## Sites Directory -Frappe is a multitenant platform and each tenant is called a site. Sites exist +Frappé 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. @@ -13,10 +13,10 @@ 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 +`apps.txt` contain a list of Python packages to treat as Frappé 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). +[Frappé Apps](/help/apps). #### common\_site\_config.json @@ -35,7 +35,7 @@ generated using the `bench build` command. ## Site -A site is a directory in `sites_dir` which represents a tenant in Frappe Platform. +A site is a directory in `sites_dir` which represents a tenant in Frappé Platform. ### Directory Structure @@ -70,7 +70,7 @@ Presently, it is limited only to backups. 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 +* `X-Frappé-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. diff --git a/frappe/docs/user/en/guides/basics/translations.md b/frappe/docs/user/en/guides/basics/translations.md index e5ff22e9f2..86d630898f 100755 --- a/frappe/docs/user/en/guides/basics/translations.md +++ b/frappe/docs/user/en/guides/basics/translations.md @@ -1,3 +1,5 @@ +# Translations + diff --git a/frappe/docs/user/en/guides/data/import-large-csv-file.md b/frappe/docs/user/en/guides/data/import-large-csv-file.md index 5194ebd98d..013d02ac92 100755 --- a/frappe/docs/user/en/guides/data/import-large-csv-file.md +++ b/frappe/docs/user/en/guides/data/import-large-csv-file.md @@ -1,3 +1,5 @@ +# Import Large Csv File + 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. diff --git a/frappe/docs/user/en/guides/deployment/email-notifications-for-failed-background-jobs.md b/frappe/docs/user/en/guides/deployment/email-notifications-for-failed-background-jobs.md index 03f57eb410..b56c57891b 100755 --- a/frappe/docs/user/en/guides/deployment/email-notifications-for-failed-background-jobs.md +++ b/frappe/docs/user/en/guides/deployment/email-notifications-for-failed-background-jobs.md @@ -1,6 +1,8 @@ +# Email Notifications For Failed Background Jobs + -

    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.

    +

    Frappé 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

    diff --git a/frappe/docs/user/en/guides/deployment/how-to-enable-social-logins.md b/frappe/docs/user/en/guides/deployment/how-to-enable-social-logins.md index 441fa64ab0..bd16e88037 100755 --- a/frappe/docs/user/en/guides/deployment/how-to-enable-social-logins.md +++ b/frappe/docs/user/en/guides/deployment/how-to-enable-social-logins.md @@ -1,11 +1,13 @@ -Use Facebook, Google or GitHub authentication to login to Frappe, and your users will be spared from remembering another password. +# How To Enable Social Logins -The system uses the **Email Address** 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. +Use Facebook, Google or GitHub authentication to login to Frappé, and your users will be spared from remembering another password. + +The system uses the **Email Address** supplied by these services to **match with an existing user** in Frappé. 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 +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. +To enable these signups, you need to have **Client ID** and **Client Secret** from these authentication services for your Frappé 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. diff --git a/frappe/docs/user/en/guides/deployment/how-to-migrate-doctype-changes-to-production.md b/frappe/docs/user/en/guides/deployment/how-to-migrate-doctype-changes-to-production.md index 35a15645ab..4bf2ab9351 100755 --- a/frappe/docs/user/en/guides/deployment/how-to-migrate-doctype-changes-to-production.md +++ b/frappe/docs/user/en/guides/deployment/how-to-migrate-doctype-changes-to-production.md @@ -1,3 +1,5 @@ +# How To Migrate Doctype Changes To Production + #### 1. DocType / Schema Changes If you are in `developer_mode`, the `.json` files for each **DocType** are automatically updated. diff --git a/frappe/docs/user/en/guides/deployment/migrations.md b/frappe/docs/user/en/guides/deployment/migrations.md index c40a2a7c4b..e40f48f0be 100755 --- a/frappe/docs/user/en/guides/deployment/migrations.md +++ b/frappe/docs/user/en/guides/deployment/migrations.md @@ -1,10 +1,10 @@ # 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 +its life. It may also be required patch existing data. Frappé bundles tools to handle these schenarios. -When you pull updates from any Frappe app (including Frappe), you should run +When you pull updates from any Frappé app (including Frappé), you should run `bench migrate` to apply schema changes and data migrations if any. ## Schema changes @@ -23,7 +23,7 @@ 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. +Note: Frappé doesn't support reverse schema migrations. ## Data Migrations @@ -37,7 +37,7 @@ 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 +The directory structure followed in Frappé is as below frappe diff --git a/frappe/docs/user/en/guides/desk/formatter_for_link_fields.md b/frappe/docs/user/en/guides/desk/formatter_for_link_fields.md index ceb2a4474c..586477f32e 100644 --- a/frappe/docs/user/en/guides/desk/formatter_for_link_fields.md +++ b/frappe/docs/user/en/guides/desk/formatter_for_link_fields.md @@ -1,3 +1,5 @@ +# Formatter For Link Fields + In case where a code and a name is maintained for an entity, (for example for Employee there may be an Employee Code and Employee Name) and we want to show both the ID and name in a link field, we can make a formatter. #### Example: diff --git a/frappe/docs/user/en/guides/desk/index.md b/frappe/docs/user/en/guides/desk/index.md index c32fa85913..47a3b43578 100755 --- a/frappe/docs/user/en/guides/desk/index.md +++ b/frappe/docs/user/en/guides/desk/index.md @@ -1,5 +1,5 @@ # Desk Customization -Articles related to customization of Frappe Desk +Articles related to customization of Frappé Desk {index} 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 index 91a5d9438a..c8143590f2 100644 --- a/frappe/docs/user/en/guides/desk/making_charts_in_c3js.md +++ b/frappe/docs/user/en/guides/desk/making_charts_in_c3js.md @@ -1,6 +1,6 @@ # 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. +Frappé 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 diff --git a/frappe/docs/user/en/guides/desk/making_graphs.md b/frappe/docs/user/en/guides/desk/making_graphs.md index 9234fa58b4..d20f8b88d4 100644 --- a/frappe/docs/user/en/guides/desk/making_graphs.md +++ b/frappe/docs/user/en/guides/desk/making_graphs.md @@ -1,61 +1,100 @@ # Making Graphs -The Frappe UI **Graph** object enables you to render simple line and bar graphs for a discreet set of data points. You can also set special checkpoint values and summary stats. +The Frappé UI **Graph** object enables you to render simple line, bar or percentage graphs for single or multiple discreet sets of data points. You can also set special checkpoint values and summary stats. ### Example: Line graph -Here's is an example of a simple sales graph: - - render_graph: function() { - $('.form-graph').empty(); - - var months = ['Aug', 'Sep', 'Oct', 'Nov', 'Dec', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul']; - var values = [2410, 3100, 1700, 1200, 2700, 1600, 2740, 1000, 850, 1500, 400, 2013]; - - var goal = 2500; - var current_val = 2013; - - new frappe.ui.Graph({ - parent: $('.form-graph'), - width: 700, - height: 140, - mode: 'line-graph', - - title: 'Sales', - subtitle: 'Monthly', - y_values: values, - x_points: months, - - specific_values: [ - { - name: "Goal", - line_type: "dashed", // "dashed" or "solid" - value: goal - }, - ], - summary_values: [ - { - name: "This month", - color: 'green', // Indicator colors: 'grey', 'blue', 'red', - // 'green', 'orange', 'purple', 'darkgrey', - // 'black', 'yellow', 'lightblue' - value: '₹ ' + current_val - }, - { - name: "Goal", - color: 'blue', - value: '₹ ' + goal - }, - { - name: "Completed", - color: 'green', - value: (current_val/goal*100).toFixed(1) + "%" - } - ] - }); - }, - - - -Setting the mode to 'bar-graph': - - +Here's an example of a simple sales graph: + + // Data + let months = ['August, 2016', 'September, 2016', 'October, 2016', 'November, 2016', + 'December, 2016', 'January, 2017', 'February, 2017', 'March, 2017', 'April, 2017', + 'May, 2017', 'June, 2017', 'July, 2017']; + + let values1 = [24100, 31000, 17000, 12000, 27000, 16000, 27400, 11000, 8500, 15000, 4000, 20130]; + let values2 = [17890, 10400, 12350, 20400, 17050, 23000, 7100, 13800, 16000, 20400, 11000, 13000]; + let goal = 25000; + let current_val = 20130; + + let g = new frappe.ui.Graph({ + parent: $('.form-graph').empty(), + height: 200, // optional + mode: 'line', // 'line', 'bar' or 'percentage' + + title: 'Sales', + subtitle: 'Monthly', + + y: [ + { + title: 'Data 1', + values: values1, + formatted: values1.map(d => '$ ' + d), + color: 'green' // Indicator colors: 'grey', 'blue', 'red', + // 'green', 'light-green', 'orange', 'purple', 'darkgrey', + // 'black', 'yellow', 'lightblue' + }, + { + title: 'Data 2', + values: values2, + formatted: values2.map(d => '$ ' + d), + color: 'light-green' + } + ], + + x: { + values: months.map(d => d.substring(0, 3)), + formatted: months + }, + + specific_values: [ + { + name: 'Goal', + line_type: 'dashed', // 'dashed' or 'solid' + value: goal + }, + ], + + summary: [ + { + name: 'This month', + color: 'orange', + value: '$ ' + current_val + }, + { + name: 'Goal', + color: 'blue', + value: '$ ' + goal + }, + { + name: 'Completed', + color: 'green', + value: (current_val/goal*100).toFixed(1) + "%" + } + ] + }); + + + +`bar` mode yeilds: + + + +You can set the `colors` property of `x` to an array of color values for `percentage` mode: + + + +You can also change the values of an existing graph with a new set of `y` values: + + setTimeout(() => { + g.change_values([ + { + values: data[2], + formatted: data[2].map(d => d + 'L') + }, + { + values: data[3], + formatted: data[3].map(d => d + 'L') + } + ]); + }, 1000); + + diff --git a/frappe/docs/user/en/guides/index.md b/frappe/docs/user/en/guides/index.md index 9eeed8baa8..765334be62 100755 --- a/frappe/docs/user/en/guides/index.md +++ b/frappe/docs/user/en/guides/index.md @@ -1,6 +1,6 @@ # 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. +The Frappé 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. 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 index 37fd85de85..6a4fc93650 100644 --- a/frappe/docs/user/en/guides/integration/how_to_setup_oauth.md +++ b/frappe/docs/user/en/guides/integration/how_to_setup_oauth.md @@ -1,6 +1,6 @@ # 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 2.0 provider based on oauthlib is built into frappe. Third party apps can now access resources of users based on Frappé Role and User permission system. To setup an app to access ## OAuth defines four roles @@ -26,11 +26,11 @@ Go to > Setup > Integrations > OAuth Provider Settings - + ### Add Primary Server -This is the main server hosting all the users. e.g. `https://frappe.io`. To setup this as the main server, go to *Setup* > *Integrations* > *Social Login Keys* and enter `https://frappe.io` in the field `Frappe Server URL`. This URL repeats in all other Frappe servers who connect to this server to authenticate. Effectively, this is the main Identity Provider (IDP). +This is the main server hosting all the users. e.g. `https://frappe.io`. To setup this as the main server, go to *Setup* > *Integrations* > *Social Login Keys* and enter `https://frappe.io` in the field `Frappé Server URL`. This URL repeats in all other Frappé servers who connect to this server to authenticate. Effectively, this is the main Identity Provider (IDP). Under this server add as many `OAuth Client`(s) as required. @@ -40,7 +40,7 @@ As a System Manager go to > Setup > Integrations > OAuth Client - + To add a client fill in the following details diff --git a/frappe/docs/user/en/guides/integration/openid_connect_and_frappe_social_login.md b/frappe/docs/user/en/guides/integration/openid_connect_and_frappe_social_login.md index b214841f90..f9a4269ec7 100644 --- a/frappe/docs/user/en/guides/integration/openid_connect_and_frappe_social_login.md +++ b/frappe/docs/user/en/guides/integration/openid_connect_and_frappe_social_login.md @@ -1,8 +1,8 @@ -# OpenID Connect and Frappe social login +# OpenID Connect and Frappé social login ## OpenID Connect -Frappe also uses Open ID connect essential standard for authenticating users. To get `id_token` with `access_token`, pass `openid` as the value for the scope parameter during authorization request. +Frappé also uses Open ID connect essential standard for authenticating users. To get `id_token` with `access_token`, pass `openid` as the value for the scope parameter during authorization request. If the scope is `openid` the JSON response with `access_token` will also include a JSON Web Token (`id_token`) signed with `HS256` and `Client Secret`. The decoded `id_token` includes the `at_hash`. @@ -19,54 +19,54 @@ Example Bearer Token with scope `openid` } ``` -## Frappe social login setup +## Frappé social login setup In this example there are 2 servers, ### Primary Server -This is the main server hosting all the users. e.g. `https://frappe.io`. To setup this as the main server, go to *Setup* > *Integrations* > *Social Login Keys* and enter `https://frappe.io` in the field `Frappe Server URL`. This URL repeats in all other Frappe servers who connect to this server to authenticate. Effectively, this is the main Identity Provider (IDP). +This is the main server hosting all the users. e.g. `https://frappe.io`. To setup this as the main server, go to *Setup* > *Integrations* > *Social Login Keys* and enter `https://frappe.io` in the field `Frappé Server URL`. This URL repeats in all other Frappé servers who connect to this server to authenticate. Effectively, this is the main Identity Provider (IDP). Under this server add as many `OAuth Client`(s) as required. Because we are setting up one app server, add only one `OAuth Client` -### Frappe App Server -This is the client connecting to the IDP. Go to *Setup* > *Integrations* > *Social Login Keys* on this server and add appropriate values to `Frappe Client ID` and `Frappe Client Secret` (refer to client added in primary server). As mentioned before keep the `Frappe Server URL` as `https://frappe.io` +### Frappé App Server +This is the client connecting to the IDP. Go to *Setup* > *Integrations* > *Social Login Keys* on this server and add appropriate values to `Frappé Client ID` and `Frappé Client Secret` (refer to client added in primary server). As mentioned before keep the `Frappé Server URL` as `https://frappe.io` -Now you will see Frappe icon on the login page. Click on this icon to login with account created in primary server (IDP) `https://frappe.io` +Now you will see Frappé icon on the login page. Click on this icon to login with account created in primary server (IDP) `https://frappe.io` **Note**: If `Skip Authorization` is checked while registering a client, page to allow or deny the granting access to resource is not shown. This can be used if the apps are internal to one organization and seamless user experience is needed. ## Steps -### Part 1 : on Frappe Identity Provider (IDP) +### Part 1 : on Frappé Identity Provider (IDP) Login to IDP - + Add OAuth Client on IDP - + Set Server URL on IDP - + -### Part 2 : on Frappe App Server +### Part 2 : on Frappé App Server -Set `Frappe Client ID` and `Frappe Client Secret` on App server (refer the client set on IDP) - +Set `Frappé Client ID` and `Frappé Client Secret` on App server (refer the client set on IDP) + -**Note**: Frappe Server URL is the main server where identities from your organization are stored. +**Note**: Frappé Server URL is the main server where identities from your organization are stored. Login Screen on App Server (login with frappe) - + ### Part 3 : Redirected on IDP login with user on IDP - + Confirm Access on IDP - + ### Part 4 : Back on App Server Logged in on app server with ID from IDP - + diff --git a/frappe/docs/user/en/guides/integration/rest_api.md b/frappe/docs/user/en/guides/integration/rest_api.md index 7f28f82b38..58e3815364 100755 --- a/frappe/docs/user/en/guides/integration/rest_api.md +++ b/frappe/docs/user/en/guides/integration/rest_api.md @@ -1,6 +1,6 @@ # REST API -Frappe ships with an HTTP API. There are two parts of this API. +Frappé ships with an HTTP API. There are two parts of this API. 1. Remote Procedure Calls (RPC) 2. REST @@ -28,7 +28,7 @@ _Response:_ ## 2. REST -All documents in Frappe are available via a RESTful API with prefix +All documents in Frappé are available via a RESTful API with prefix `/api/resource/`. ### Login diff --git a/frappe/docs/user/en/guides/integration/using_oauth.md b/frappe/docs/user/en/guides/integration/using_oauth.md index 48bb853d0a..d3db092905 100644 --- a/frappe/docs/user/en/guides/integration/using_oauth.md +++ b/frappe/docs/user/en/guides/integration/using_oauth.md @@ -20,7 +20,7 @@ redirect_uri = #### Confirmation Dialog - + Click 'Allow' to receive authorization code in redirect uri. @@ -106,4 +106,4 @@ status : 400 ### Accessing Resource -Add header `Authorizaton: Bearer ` to Frappe's REST API endpoints to access user's resource +Add header `Authorizaton: Bearer ` to Frappé's REST API endpoints to access user's resource diff --git a/frappe/docs/user/en/guides/portal-development/adding-pages.md b/frappe/docs/user/en/guides/portal-development/adding-pages.md index c8bdd53552..c9cd0191c3 100755 --- a/frappe/docs/user/en/guides/portal-development/adding-pages.md +++ b/frappe/docs/user/en/guides/portal-development/adding-pages.md @@ -13,7 +13,7 @@ Either file must be present for the system to make this a valid folder to build ### Markdown # This is a title - + This is some page content a [link](/link/to/page) diff --git a/frappe/docs/user/en/guides/portal-development/generators.md b/frappe/docs/user/en/guides/portal-development/generators.md index 78d83d20f6..8bf52d7299 100644 --- a/frappe/docs/user/en/guides/portal-development/generators.md +++ b/frappe/docs/user/en/guides/portal-development/generators.md @@ -22,7 +22,7 @@ We added `published`, `route` in the DocType **Note:** The field `route` is mandatory -Generator fields +Generator fields #### 2. Added Website Generator to Hooks diff --git a/frappe/docs/user/en/guides/portal-development/index.md b/frappe/docs/user/en/guides/portal-development/index.md index a9aaab4312..bba3ff1fb1 100755 --- a/frappe/docs/user/en/guides/portal-development/index.md +++ b/frappe/docs/user/en/guides/portal-development/index.md @@ -1,6 +1,6 @@ # 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 +Frappé 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 diff --git a/frappe/docs/user/en/guides/portal-development/portal-roles.md b/frappe/docs/user/en/guides/portal-development/portal-roles.md index d3f5be7287..2248bb0b55 100644 --- a/frappe/docs/user/en/guides/portal-development/portal-roles.md +++ b/frappe/docs/user/en/guides/portal-development/portal-roles.md @@ -8,7 +8,7 @@ Roles can be assigned to Website Users and they will see menu based on their rol 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 -Portal Settings +Portal Settings #### Rules for Default Role diff --git a/frappe/docs/user/en/guides/portal-development/web-forms.md b/frappe/docs/user/en/guides/portal-development/web-forms.md index 533790952e..7cca8de2cc 100644 --- a/frappe/docs/user/en/guides/portal-development/web-forms.md +++ b/frappe/docs/user/en/guides/portal-development/web-forms.md @@ -2,7 +2,7 @@ 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 utilities -Web Form +Web Form ### Standard Web Forms diff --git a/frappe/docs/user/en/guides/reports-and-printing/getting-information-from-another-document-in-print-format.md b/frappe/docs/user/en/guides/reports-and-printing/getting-information-from-another-document-in-print-format.md index 321028d7af..afe87c04f8 100755 --- a/frappe/docs/user/en/guides/reports-and-printing/getting-information-from-another-document-in-print-format.md +++ b/frappe/docs/user/en/guides/reports-and-printing/getting-information-from-another-document-in-print-format.md @@ -1,3 +1,5 @@ +# Getting Information From Another Document In Print Format + 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 %} diff --git a/frappe/docs/user/en/guides/reports-and-printing/how-to-make-query-report.md b/frappe/docs/user/en/guides/reports-and-printing/how-to-make-query-report.md index 9eb11b066a..c4c047a63f 100755 --- a/frappe/docs/user/en/guides/reports-and-printing/how-to-make-query-report.md +++ b/frappe/docs/user/en/guides/reports-and-printing/how-to-make-query-report.md @@ -1,3 +1,5 @@ +# How To Make Query Report + 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. @@ -6,7 +8,7 @@ To create a new Query Report: ### 1. Create a new Report -Query Report +Query Report 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 @@ -35,11 +37,11 @@ You can define complex queries such as: ### 3. Check the Report -Query Report +Query Report ### 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.github.io/frappe/user/en/guides/reports-and-printing/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. +If you are making a standard report, you can add filters in your query report just like [script reports](https://frappe.io/docs/user/en/guides/reports-and-printing/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 diff --git a/frappe/docs/user/en/guides/reports-and-printing/how-to-make-script-reports.md b/frappe/docs/user/en/guides/reports-and-printing/how-to-make-script-reports.md index ab03c11051..80522d435b 100755 --- a/frappe/docs/user/en/guides/reports-and-printing/how-to-make-script-reports.md +++ b/frappe/docs/user/en/guides/reports-and-printing/how-to-make-script-reports.md @@ -4,13 +4,13 @@ You can create tabulated reports using server side scripts by creating a new Rep > 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](https://frappe.github.io/frappe/user/en/guides/app-development/). +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](https://frappe.io/docs/user/en/guides/app-development/). -> Note: You must be in [Developer Mode](https://frappe.github.io/frappe/user/en/guides/app-development/how-enable-developer-mode-in-frappe) to do this +> Note: You must be in [Developer Mode](https://frappe.io/docs/user/en/guides/app-development/how-enable-developer-mode-in-frappe) to do this ### 1. Create a new Report -Script Report +Script Report 1. Set Report Type as "Script Report" 1. Set "Is Standard" as "Yes" 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 f663816757..07a7bc222b 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 @@ -58,7 +58,7 @@ Here is how the General Ledger Report is built: Here is what the report looks like: -General Ledger +General Ledger ##### Comments: diff --git a/frappe/docs/user/en/guides/reports-and-printing/where-do-i-find-standard-print-formats.md b/frappe/docs/user/en/guides/reports-and-printing/where-do-i-find-standard-print-formats.md index 00e351c615..c1816751dd 100755 --- a/frappe/docs/user/en/guides/reports-and-printing/where-do-i-find-standard-print-formats.md +++ b/frappe/docs/user/en/guides/reports-and-printing/where-do-i-find-standard-print-formats.md @@ -1,3 +1,5 @@ +# Where Do I Find Standard Print Formats + Standard Print formats are auto generated from the layout of the DocType. You can customize the standard format by

    diff --git a/frappe/docs/user/en/index.md b/frappe/docs/user/en/index.md index a25785e59e..a68da6210b 100755 --- a/frappe/docs/user/en/index.md +++ b/frappe/docs/user/en/index.md @@ -1,3 +1,3 @@ -# Develop Apps with Frappe +# Develop Apps with Frappé {index} diff --git a/frappe/docs/user/en/tutorial/app.md b/frappe/docs/user/en/tutorial/app.md index e338ce8a17..4233b7cc1b 100755 --- a/frappe/docs/user/en/tutorial/app.md +++ b/frappe/docs/user/en/tutorial/app.md @@ -1,9 +1,9 @@ # What is an Application -An Application in Frappe is just a standard Python application. You can structure a Frappe Application the same way you structure a standard Python Application. For deployment, Frappe uses the standard Python Setuptools, so you can easily port and install the application on any machine. +An Application in Frappé is just a standard Python application. You can structure a Frappé Application the same way you structure a standard Python Application. For deployment, Frappé uses the standard Python Setuptools, so you can easily port and install the application on any machine. -Frappe Framework provides a WSGI interface and for development you can use the built-in Werkzeug server. For implementing in production, we recommend using nginx and gunicorn. +Frappé Framework provides a WSGI interface and for development you can use the built-in Werkzeug server. For implementing in production, we recommend using nginx and gunicorn. -Frappe also has a multi-tenant architecture, grounds up. This means that you can run multiple "sites" in your setup, each could be serving a different set of applications and users. The database for each site is separate. +Frappé also has a multi-tenant architecture, grounds up. This means that you can run multiple "sites" in your setup, each could be serving a different set of applications and users. The database for each site is separate. {next} diff --git a/frappe/docs/user/en/tutorial/before.md b/frappe/docs/user/en/tutorial/before.md index 80f34e01dd..48c6f968a3 100755 --- a/frappe/docs/user/en/tutorial/before.md +++ b/frappe/docs/user/en/tutorial/before.md @@ -1,16 +1,17 @@ # Before You Start -

    A list of resources to help you get started with building apps using Frappe

    +

    A list of resources to help you get started with building apps using Frappé

    --- #### 1. Python -Frappe uses Python (v2.7) for server-side programming. It is highly recommended to learn Python before you start building apps with Frappe. +Frappé uses Python (v2.7) for server-side programming. It is highly recommended to learn Python before you start building apps with Frappé. To write quality server-side code, you must also include automated tests. Resources: + 1. [Codecademy Tutorial for Python](https://www.codecademy.com/learn/python) 1. [Official Python Tutorial](https://docs.python.org/2.7/tutorial/index.html) 1. [Basics of Test-driven development](http://code.tutsplus.com/tutorials/beginning-test-driven-development-in-python--net-30137) @@ -19,20 +20,22 @@ Resources: #### 2. MariaDB / MySQL -To create database-driven apps with Frappe, you must understand the basics of database management, like how to install, login, create new databases, and basic SQL queries. +To create database-driven apps with Frappé, you must understand the basics of database management, like how to install, login, create new databases, and basic SQL queries. Resources: + 1. [Codecademy Tutorial for SQL](https://www.codecademy.com/learn/learn-sql) - 1. [A basic MySQL tutorial by DigitalOcean](https://www.digitalocean.com/community/tutorials/a-basic-mysql-tutorial) + 1. [A basic MySQL tutorial by DigitalOcean](https://www.digitalocean.com/community/tutorials/a-basic-mysql-tutorial) 1. [Getting started with MariaDB](https://mariadb.com/kb/en/mariadb/documentation/getting-started/) --- #### 3. HTML / CSS -If you want to build user interfaces using Frappe, you will need to learn basic HTML / CSS and the Boostrap CSS Framework. +If you want to build user interfaces using Frappé, you will need to learn basic HTML / CSS and the Boostrap CSS Framework. Resources: + 1. [Codecademy Tutorial for HTML/CSS](https://www.codecademy.com/learn/learn-html-css) 1. [Getting started with Bootstrap](https://getbootstrap.com/getting-started/) @@ -44,6 +47,7 @@ To customize forms and create rich user interfaces, you should learn JavaScript Resources: + 1. [Codecademy Tutorial for JavaScript](https://www.codecademy.com/learn/learn-javascript) 1. [Codecademy Tutorial for jQuery](https://www.codecademy.com/learn/jquery) --- @@ -53,6 +57,7 @@ Resources: If you are customizing Print templates or Web pages, you need to learn the Jinja Templating language. It is an easy way to create dynamic web pages (HTML). Resources: + 1. [Primer on Jinja Templating](https://realpython.com/blog/python/primer-on-jinja-templating/) 1. [Official Documentation](http://jinja.pocoo.org/) @@ -63,10 +68,11 @@ Resources: Learn how to contribute back to an open source project using Git and GitHub, two great tools to help you manage your code and share it with others. Resources: + 1. [Basic Git Tutorial](https://try.github.io) 2. [How to contribute to Open Source](https://opensource.guide/how-to-contribute/) --- -When you are ready, you can try [building a sample application]({{ docs_base_url }}/user/en/tutorial/app) using Frappe. +When you are ready, you can try [building a sample application](/docs/user/en/tutorial/app) using Frappé. diff --git a/frappe/docs/user/en/tutorial/bench.md b/frappe/docs/user/en/tutorial/bench.md index e720d9dc98..8772638265 100755 --- a/frappe/docs/user/en/tutorial/bench.md +++ b/frappe/docs/user/en/tutorial/bench.md @@ -1,10 +1,10 @@ -# Installing the Frappe Bench +# Installing the Frappé Bench -Easiest way to setup frappe on a Unix Like system is to use frappe-bench. Read the detailed instructions on how to install using Frappe Bench. +Easiest way to setup frappe on a Unix Like system is to use frappe-bench. Read the detailed instructions on how to install using Frappé Bench. > [https://github.com/frappe/bench](https://github.com/frappe/bench) -With Frappe Bench you will be able to setup and host multiple applications and sites and it will also setup a Python Virtualenv so that you can have an isolated environment to run your applications (and will not have version conflict with other development environments). +With Frappé Bench you will be able to setup and host multiple applications and sites and it will also setup a Python Virtualenv so that you can have an isolated environment to run your applications (and will not have version conflict with other development environments). The `bench` command line tool will also be installed that will help you in development and management of the installation. diff --git a/frappe/docs/user/en/tutorial/conclusion.md b/frappe/docs/user/en/tutorial/conclusion.md index f45b64ba23..bc254ba5d9 100755 --- a/frappe/docs/user/en/tutorial/conclusion.md +++ b/frappe/docs/user/en/tutorial/conclusion.md @@ -1,7 +1,7 @@ # Conclusion -We hope this will give you an overview of how applications are developed in Frappe. The objective was to briefly touch on the various aspects of application development and give a broad overview. To get help on specific issues, look at the API. +We hope this will give you an overview of how applications are developed in Frappé. The objective was to briefly touch on the various aspects of application development and give a broad overview. To get help on specific issues, look at the API. For help, join the community at the [chat channel on Gitter](https://gitter.im/frappe/erpnext) or the [developer forum](https://discuss.erpnext.com) diff --git a/frappe/docs/user/en/tutorial/controllers.md b/frappe/docs/user/en/tutorial/controllers.md index b994f71971..09edce15d0 100755 --- a/frappe/docs/user/en/tutorial/controllers.md +++ b/frappe/docs/user/en/tutorial/controllers.md @@ -48,7 +48,7 @@ In this script: Check if your validations work by creating new records -Transaction +Transaction #### Debugging diff --git a/frappe/docs/user/en/tutorial/doctypes.md b/frappe/docs/user/en/tutorial/doctypes.md index 2e70ab4146..889f923b3f 100755 --- a/frappe/docs/user/en/tutorial/doctypes.md +++ b/frappe/docs/user/en/tutorial/doctypes.md @@ -6,7 +6,7 @@ To create a new **DocType**, go to: > Developer > Documents > Doctype > New -New Doctype +New Doctype In the DocType, first the Module, which in our case is **Library Management** @@ -25,7 +25,7 @@ Fields are much more than database columns, they can be: Let us add the fields of the Article. -Adding Fields +Adding Fields When you add fields, you need to enter the **Type**. **Label** is optional for Section Break and Column Break. **Name** (`fieldname`) is the name of the database table column and also the property of the controller. This has to be *code friendly*, i.e. it has to have small cases are _ instead of " ". If you leave the Fieldname blank, it will be automatically set when you save it. @@ -45,9 +45,9 @@ We can add the following fields: #### Add Permissions -After adding the fields, hit done and add a new row in the Permission Rules section. For now, let us give Read, Write, Create, Delete and Report access to **Librarian**. Frappe has a finely grained Role based permission model. You can also change permissions later using the **Role Permissions Manager** from **Setup**. +After adding the fields, hit done and add a new row in the Permission Rules section. For now, let us give Read, Write, Create, Delete and Report access to **Librarian**. Frappé has a finely grained Role based permission model. You can also change permissions later using the **Role Permissions Manager** from **Setup**. -Adding Permissions +Adding Permissions #### Saving diff --git a/frappe/docs/user/en/tutorial/form-client-scripting.md b/frappe/docs/user/en/tutorial/form-client-scripting.md index a70b8475ff..d4673c55d4 100755 --- a/frappe/docs/user/en/tutorial/form-client-scripting.md +++ b/frappe/docs/user/en/tutorial/form-client-scripting.md @@ -1,3 +1,5 @@ +# Form Client Scripting + ## Scripting Forms Now we have created a basic system that works out of the box without us having to write any code. Let us now write some scripts to make the application richer and add validations so that the user does not enter wrong data. diff --git a/frappe/docs/user/en/tutorial/index.md b/frappe/docs/user/en/tutorial/index.md index 43f04583e9..69f276db10 100755 --- a/frappe/docs/user/en/tutorial/index.md +++ b/frappe/docs/user/en/tutorial/index.md @@ -1,6 +1,6 @@ -# Frappe Tutorial +# Frappé 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: +In this guide, we will show you how to create an application from scratch using **Frappé**. Using the example of a Library Management System, we will cover: 1. Installation 1. Making a New App @@ -12,9 +12,9 @@ In this guide, we will show you how to create an application from scratch using ## 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 the minimum, if you have never used Python before, you should take a quick tutorial before your use this Guide. +This guide is intended for software developers who are familiar with how the web applications are built and served. Frappé 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 the 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. +Frappé 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 @@ -26,7 +26,7 @@ For this guide book, we will build a simple **Library Management** application. 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. +The user interface (UI) for the librarian will be the **Frappé 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. diff --git a/frappe/docs/user/en/tutorial/models.md b/frappe/docs/user/en/tutorial/models.md index 04114e1369..0dbb1b1b32 100755 --- a/frappe/docs/user/en/tutorial/models.md +++ b/frappe/docs/user/en/tutorial/models.md @@ -1,6 +1,6 @@ # Making Models -The next step is to create the models as we discussed in the introduction. In Frappe, models are called **DocTypes**. You can create new DocTypes from the Desk UI. **DocTypes** are made of fields called **DocField** and role based permissions are integrated into the models, these are called **DocPerms**. +The next step is to create the models as we discussed in the introduction. In Frappé, models are called **DocTypes**. You can create new DocTypes from the Desk UI. **DocTypes** are made of fields called **DocField** and role based permissions are integrated into the models, these are called **DocPerms**. When a DocType is saved, a new table is created in the database. This table is named as `tab[doctype]`. diff --git a/frappe/docs/user/en/tutorial/naming-and-linking.md b/frappe/docs/user/en/tutorial/naming-and-linking.md index dd0fca7245..546af3d50d 100755 --- a/frappe/docs/user/en/tutorial/naming-and-linking.md +++ b/frappe/docs/user/en/tutorial/naming-and-linking.md @@ -4,7 +4,7 @@ Then let us create the other DocType and save it too: 1. Library Member (First Name, Last Name, Email Address, Phone, Address) -Doctype Saved +Doctype Saved #### Naming of DocTypes @@ -20,21 +20,21 @@ This can be set by entering the **Autoname** field. For controller, leave blank. > **Search Fields**: A DocType may be named on a series but it still needs to be searched by name. In our case, the Article will be searched by the title or the author name. So this can be entered in search field. -Autonaming and Search Field +Autonaming and Search Field #### Link and Select Fields -Foreign keys are specified in Frappe as **Link** type fields. The target DocType must be mentioned in the Options text area. +Foreign keys are specified in Frappé as **Link** type fields. The target DocType must be mentioned in the Options text area. In our example, in the Library Transaction DocType, we have to link both the Library Member and the Article. **Note:** Remeber that Link fields are not automatically set as Foreign Keys in the MariaDB database, because that will implicitly index the column. This may not be optimum hence the Foreign Key validation is done by the Framework. -Link Field +Link Field For select fields, as we mentioned earlier, add the various options in the **Options** input box, each option on a new row. -Select Field +Select Field Similary complete making the other models. @@ -44,7 +44,7 @@ A standard pattern is when you select an ID, say **Library Member** in **Library To do this, we can use Read Only fields and in options, we can set the the name of the link and the fieldname of the property we want to fetch. For this example in **Member First Name** we can set `library_member.first_name` -Fetch values +Fetch values ### Complete the Models @@ -52,19 +52,19 @@ In the same way, you can complete all the models so that the final fields look l #### Article -Article +Article #### Library Member -Library Member +Library Member #### Library Membership -Library Membership +Library Membership #### Library Transaction -Library Transaction +Library Transaction > Make sure to give permissions to **Librarian** on each DocType diff --git a/frappe/docs/user/en/tutorial/new-app.md b/frappe/docs/user/en/tutorial/new-app.md index cbb60cc3d3..94a3ad1f1e 100755 --- a/frappe/docs/user/en/tutorial/new-app.md +++ b/frappe/docs/user/en/tutorial/new-app.md @@ -7,7 +7,7 @@ To make a new application, go to your bench folder and run, `bench new-app {app_ $ 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 Publisher: Frappé App Email: info@frappe.io App Icon (default 'octicon octicon-file-directory'): octicon octicon-book App Color (default 'grey'): #589494 @@ -44,11 +44,11 @@ The application will be created in a folder called `library_management` and will 1. `config` folder contains application configuration info 1. `desktop.py` is where desktop icons can be added to the Desk 1. `hooks.py` is where integrations with the environment and other applications is mentioned. -1. `library_management` (inner) is a **module** that is bootstrapped. In Frappe, a **module** is where model and controller files reside. +1. `library_management` (inner) is a **module** that is bootstrapped. In Frappé, a **module** is where model and controller files reside. 1. `modules.txt` contains list of **modules** in the app. When you create a new module, it is required that you update it in this file. 1. `patches.txt` is where migration patches are written. They are python module references using the dot notation. 1. `templates` is the folder where web view templates are maintained. Templates for **Login** and other standard pages are bootstrapped in frappe. -1. `generators` are where templates for models are maintained, where each model instance has a separte web route, for example a **Blog Post** where each post has its unique web url. In Frappe, the templating engine used is Jinja2 +1. `generators` are where templates for models are maintained, where each model instance has a separte web route, for example a **Blog Post** where each post has its unique web url. In Frappé, the templating engine used is Jinja2 1. `pages` is where single route templates are maintained. For example for a "/blog" type of page. {next} diff --git a/frappe/docs/user/en/tutorial/reports.md b/frappe/docs/user/en/tutorial/reports.md index 03b61822f2..6e2a2b6c42 100755 --- a/frappe/docs/user/en/tutorial/reports.md +++ b/frappe/docs/user/en/tutorial/reports.md @@ -2,6 +2,6 @@ You can also click on the Reports text on the sidebar (left) to see tabulated records -Report +Report {next} diff --git a/frappe/docs/user/en/tutorial/roles.md b/frappe/docs/user/en/tutorial/roles.md index d358556083..42b8246bec 100755 --- a/frappe/docs/user/en/tutorial/roles.md +++ b/frappe/docs/user/en/tutorial/roles.md @@ -9,6 +9,6 @@ To create a new Role, go to: > Setup > Users > Role > New -Adding Roles +Adding Roles {next} diff --git a/frappe/docs/user/en/tutorial/setting-up-the-site.md b/frappe/docs/user/en/tutorial/setting-up-the-site.md index 30f4b86d1f..2a823dc5f3 100755 --- a/frappe/docs/user/en/tutorial/setting-up-the-site.md +++ b/frappe/docs/user/en/tutorial/setting-up-the-site.md @@ -18,7 +18,7 @@ Let us create a new site and call it `library`. You can then install a new site, by the command `bench new-site library`. -This will create a new database and site folder and install `frappe` (which is also an application!) in the new site. The `frappe` application has two built-in modules **Core** and **Website**. The Core module contains the basic models for the application. Frappe is a batteries included framework and comes with a lot of built-in models. These models are called **DocTypes**. More on that later. +This will create a new database and site folder and install `frappe` (which is also an application!) in the new site. The `frappe` application has two built-in modules **Core** and **Website**. The Core module contains the basic models for the application. Frappé is a batteries included framework and comes with a lot of built-in models. These models are called **DocTypes**. More on that later. $ bench new-site library MySQL root password: diff --git a/frappe/docs/user/en/tutorial/single-doctypes.md b/frappe/docs/user/en/tutorial/single-doctypes.md index 9b0e4171b1..d01f13d700 100755 --- a/frappe/docs/user/en/tutorial/single-doctypes.md +++ b/frappe/docs/user/en/tutorial/single-doctypes.md @@ -1,9 +1,9 @@ # Single DocTypes -A application will usually have a Settings page. In our application, we can define a page where we can set the loan period. We also need to save this property. In Frappe, this can be done using a **Single** type DocType. A Single DocType is like the Singleton pattern in Java. It is an object with only one instance. Let us call this as **Library Managment Settings**. +A application will usually have a Settings page. In our application, we can define a page where we can set the loan period. We also need to save this property. In Frappé, this can be done using a **Single** type DocType. A Single DocType is like the Singleton pattern in Java. It is an object with only one instance. Let us call this as **Library Managment Settings**. To create an new Single DocType, mark the **Is Single** property as checked. -Single Doctypes +Single Doctypes {next} diff --git a/frappe/docs/user/en/tutorial/start.md b/frappe/docs/user/en/tutorial/start.md index 0182654494..10c921a2f2 100755 --- a/frappe/docs/user/en/tutorial/start.md +++ b/frappe/docs/user/en/tutorial/start.md @@ -14,7 +14,7 @@ To start the development server, run `bench start` You can now open your browser and go to `http://localhost:8000`. You should see this login page if all goes well: -Login Screen +Login Screen Now login with : @@ -24,8 +24,8 @@ Password : **Use the password that was created during installation** When you login, you should see the "Desk" home page -Desk +Desk -As you can see, the Frappe basic system comes with several pre-loaded applications like To Do, File Manager etc. These apps can integrated in your app workflow as we progress. +As you can see, the Frappé basic system comes with several pre-loaded applications like To Do, File Manager etc. These apps can integrated in your app workflow as we progress. {next} diff --git a/frappe/docs/user/en/tutorial/task-runner.md b/frappe/docs/user/en/tutorial/task-runner.md index fcf520fedb..0afdfeaa1b 100755 --- a/frappe/docs/user/en/tutorial/task-runner.md +++ b/frappe/docs/user/en/tutorial/task-runner.md @@ -1,6 +1,6 @@ # Scheduled Tasks -Finally, an application also has to send email notifications and do other kind of scheduled tasks. In Frappe, if you have setup the bench, the task / scheduler is setup via Celery using Redis Queue. +Finally, an application also has to send email notifications and do other kind of scheduled tasks. In Frappé, if you have setup the bench, the task / scheduler is setup via Celery using Redis Queue. To add a new task handler, go to `hooks.py` and add a new handler. Default handlers are `all`, `daily`, `weekly`, `monthly`. The `all` handler is called every 3 minutes by default. @@ -15,7 +15,7 @@ To add a new task handler, go to `hooks.py` and add a new handler. Default handl Here we can point to a Python function and that function will be executed every day. Let us look what this function looks like: - # Copyright (c) 2013, Frappe + # Copyright (c) 2013, Frappé # For license information, please see license.txt from __future__ import unicode_literals diff --git a/frappe/docs/user/en/tutorial/users-and-records.md b/frappe/docs/user/en/tutorial/users-and-records.md index 394899a795..4f0b0ad85d 100755 --- a/frappe/docs/user/en/tutorial/users-and-records.md +++ b/frappe/docs/user/en/tutorial/users-and-records.md @@ -1,6 +1,6 @@ # Making Users and Records -Now that we have created the models, we can directly start making records using Frappe Desk UI. You do not have to create views! Views in Frappe are automatically made based on the DocType properties. +Now that we have created the models, we can directly start making records using Frappé Desk UI. You do not have to create views! Views in Frappé are automatically made based on the DocType properties. ### 4.1 Creating User @@ -12,7 +12,7 @@ Create a new User and set the name and first name and new password. Also give the Librarian and Library Member Roles to this user -Add User Roles +Add User Roles Now logout and login using the new user id and password. @@ -20,36 +20,36 @@ Now logout and login using the new user id and password. You will now see an icon for the Library Management module. Click on that icon and you will see the Module page: -Library Management Module +Library Management Module Here you can see the DocTypes that we have created for the application. Let us start creating a few records. First let us create a new Article: -New Article +New 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 +New Article You can also add an image. -Attach Image +Attach Image Now let us create a new member: -New Library Member +New Library Member After this, let us create a new membership record for the member. Here if you remember we had set the values of Member First Name and Member Last Name to be directly fetched from the Member records and as soon as you will select the member id, the names will be updated. -New Library Membership +New Library Membership As you can see that the date is formatted as year-month-day which is a system format. To set / change date, time and number formats, go to > Setup > Settings > System Settings -System Settings +System Settings {next} diff --git a/frappe/docs/user/en/tutorial/web-views.md b/frappe/docs/user/en/tutorial/web-views.md index 2953104958..9eb36d48c7 100755 --- a/frappe/docs/user/en/tutorial/web-views.md +++ b/frappe/docs/user/en/tutorial/web-views.md @@ -1,8 +1,8 @@ # Web Views -Frappe has two main user environments, the Desk and Web. Desk is a controlled UI environment with a rich AJAX application and the web is more traditional HTML templates served for public consumption. Web views can also be generated to create more controlled views for users who may login but still do not have access to the Desk. +Frappé has two main user environments, the Desk and Web. Desk is a controlled UI environment with a rich AJAX application and the web is more traditional HTML templates served for public consumption. Web views can also be generated to create more controlled views for users who may login but still do not have access to the Desk. -In Frappe, Web Views are managed by templates and they are usually in the `templates` folder. There are 2 main types of templates. +In Frappé, Web Views are managed by templates and they are usually in the `templates` folder. There are 2 main types of templates. 1. Pages: These are Jinja templates where a single view exists for a single web route e.g. `/blog`. 2. Generators: These are templates where each instance of a DocType has a separate web route `/blog/a-blog`, `blog/b-blog` etc. @@ -16,11 +16,11 @@ Let us look at the standard Web Views: If you are logged in as the test user, go to `/article` and you should see the list of articles: -web list +web list Click on one article and you will see the default web view -web view +web view Now if you want to make a better list view for the article, drop a file called `row_template.html` in the `library_management/templates/includes/list/` folder. Here is an example file: @@ -46,11 +46,11 @@ Here, you will get all the properties of the article in the `doc` object. The updated list view looks like this! -new web list +new web list #### Home Page -Frappe also has a built-in signup workflow which also includes 3rd party signups via Google, Facebook and GitHub. When a user signs up on the web, she does not have access to the desk interface by default. +Frappé also has a built-in signup workflow which also includes 3rd party signups via Google, Facebook and GitHub. When a user signs up on the web, she does not have access to the desk interface by default. > To allow user access into the Desk, open set the user from Setup > User and set the User Type as "System User" diff --git a/frappe/docs/user/en/videos/index.md b/frappe/docs/user/en/videos/index.md index 8b095501e4..f95904a615 100755 --- a/frappe/docs/user/en/videos/index.md +++ b/frappe/docs/user/en/videos/index.md @@ -1,8 +1,8 @@ -# Video Tutorials for Frappe Framework +# Video Tutorials for Frappé Framework -This 10-part video tutorial will teach you how to build complex apps in Frappe +This 10-part video tutorial will teach you how to build complex apps in Frappé -Pre-requisites: You must have some understanding of Python, Javascript and MySQL before you start this tutorial. +Pre-requisites: You must have some understanding of Python, Javascript and MySQL before you start this tutorial. --- diff --git a/frappe/docs/user/es/bench/guides/diagnosing-the-scheduler.md b/frappe/docs/user/es/bench/guides/diagnosing-the-scheduler.md index d1317c9bcc..18faa2fbe2 100644 --- a/frappe/docs/user/es/bench/guides/diagnosing-the-scheduler.md +++ b/frappe/docs/user/es/bench/guides/diagnosing-the-scheduler.md @@ -1,3 +1,5 @@ +# Diagnosing The Scheduler + En caso que estes experimentando inconvenientes con las tareas programadas, puedes ejecutar varios comandos para diagnosticar el problema. diff --git a/frappe/docs/user/es/bench/guides/index.md b/frappe/docs/user/es/bench/guides/index.md index 0dff60b400..8ab035a2f5 100644 --- a/frappe/docs/user/es/bench/guides/index.md +++ b/frappe/docs/user/es/bench/guides/index.md @@ -1 +1,5 @@ +# Guides + + + {index} \ No newline at end of file diff --git a/frappe/docs/user/es/bench/guides/manual-setup.md b/frappe/docs/user/es/bench/guides/manual-setup.md index e07c56433c..de314b1675 100644 --- a/frappe/docs/user/es/bench/guides/manual-setup.md +++ b/frappe/docs/user/es/bench/guides/manual-setup.md @@ -1,3 +1,5 @@ +# Manual Setup + Instalación Manual -------------- @@ -30,7 +32,7 @@ Uso básico * Crea un nuevo bench - El comando init va a crear un directorio conteniendo el framework Frappe instalado. + El comando init va a crear un directorio conteniendo el framework Frappé instalado. Va a ser configurado para copias de seguridad periódicas y actualizaciones automáticas una vez por día. bench init frappe-bench && cd frappe-bench @@ -47,7 +49,7 @@ Uso básico * Agregar Site - Las aplicaciones Frappe son montadas en los Sites y por tanto tendras que crear por lo menos un site. + Las aplicaciones Frappé son montadas en los Sites y por tanto tendras que crear por lo menos un site. El comando new-site te permite crearlos. bench new-site site1.local @@ -58,7 +60,7 @@ Uso básico bench start - Para acceder a Frappe / ERPNext, abra su navegador favorito y escriba la ruta `localhost:8000` + Para acceder a Frappé / ERPNext, abra su navegador favorito y escriba la ruta `localhost:8000` El usuario por defecto es "Administrator" y la contraseña es la que específicaste al momento de crear el nuevo site. diff --git a/frappe/docs/user/es/bench/guides/settings-limits.md b/frappe/docs/user/es/bench/guides/settings-limits.md index a72ddc2d79..d918cd7c60 100644 --- a/frappe/docs/user/es/bench/guides/settings-limits.md +++ b/frappe/docs/user/es/bench/guides/settings-limits.md @@ -1,6 +1,6 @@ # Estableciendo límites para su sitio -La versión 7 de Frappe ha agregado soporte para la configuración de límites y restricciones para su site. +La versión 7 de Frappé ha agregado soporte para la configuración de límites y restricciones para su site. Estas restricciones están en el archivo `site_config.json` dentro de la carpeta del site. { @@ -36,4 +36,4 @@ Ejemplo: Puedes verificar el uso abriendo la página de "Usage Info" ubicada en el toolbar / AwesomeBar. Un límite solo va a mostrarse en la página si ha sido configurado. -Doctype Saved +Doctype Saved diff --git a/frappe/docs/user/es/bench/guides/setup-multitenancy.md b/frappe/docs/user/es/bench/guides/setup-multitenancy.md index f663704521..ca7469f1fd 100644 --- a/frappe/docs/user/es/bench/guides/setup-multitenancy.md +++ b/frappe/docs/user/es/bench/guides/setup-multitenancy.md @@ -1,3 +1,5 @@ +# Setup Multitenancy + Asumiento que tiene su primer site corriendo y ha realizado los [pasos para producción](setup-production.html), esta sección explica como montar su segundo site (y más). Su primer site se configuró como el site por defecto de forma automática. Puedes cambiarlo ejecutando el comando, diff --git a/frappe/docs/user/es/bench/guides/setup-production.md b/frappe/docs/user/es/bench/guides/setup-production.md index 2ab7e290dc..6ef279c84f 100644 --- a/frappe/docs/user/es/bench/guides/setup-production.md +++ b/frappe/docs/user/es/bench/guides/setup-production.md @@ -1,3 +1,5 @@ +# Setup Production + Puedes configurar el bench para producción configurando dos parametros, Supervisor y nginx. Si quieres volver a ponerlo en desarrollo debes ver [estos comandos](https://github.com/frappe/bench/wiki/Stopping-Production-and-starting-Development) ####Configuración para producción facíl @@ -7,7 +9,7 @@ Estos pasos son automátizados si ejecutas `sudo bench setup production` ####Configuración manual para producción Supervisor ---------- -Supervisor se asegura de mantener el proceso que inició Frappe corriendo y lo reinicia en caso de cualquier inconveniente. +Supervisor se asegura de mantener el proceso que inició Frappé corriendo y lo reinicia en caso de cualquier inconveniente. Puedes generar la configuración necesaria para supervisor ejecutando el comando `bench setup supervisor`. La configuración va a estar disponible en la carpeta `config/supervisor.conf`. Luego puedes copiar/enlazar este archivo al directorio de configuración de supervisor y reiniciar el servicio para que tome efecto de los cambios realizados. diff --git a/frappe/docs/user/es/bench/index.md b/frappe/docs/user/es/bench/index.md index ccd28db145..24baef4875 100644 --- a/frappe/docs/user/es/bench/index.md +++ b/frappe/docs/user/es/bench/index.md @@ -1 +1,5 @@ +# Bench + + + {index} diff --git a/frappe/docs/user/es/bench/resources/background-services.md b/frappe/docs/user/es/bench/resources/background-services.md index 093d666e3a..52294cc1f5 100644 --- a/frappe/docs/user/es/bench/resources/background-services.md +++ b/frappe/docs/user/es/bench/resources/background-services.md @@ -1,3 +1,5 @@ +# Background Services + Servicios Externos ----------------- @@ -6,7 +8,7 @@ Servicios Externos * nginx (para producción) * supervisor (para producción) -Procesos de Frappe +Procesos de Frappé ---------------- @@ -18,12 +20,12 @@ Procesos de Frappe * Procesos de Redis Worker - * Los procesos de Celery se encargan de ejecutar tareas en background en Frappe. + * Los procesos de Celery se encargan de ejecutar tareas en background en Frappé. Estos procesos son iniciados automáticamente cuando se ejecuta el comando `bench start` y para producción se configuran en las configuraciones de supervisor. * Procesos Scheduler - * Los procesos del Scheduler programan la lista de tareas programadas en Frappe. + * Los procesos del Scheduler programan la lista de tareas programadas en Frappé. Este proceso es iniciado automáticamente cuando se ejecuta el comando `bench start` y para producción se configuran en las configuraciones de supervisor. diff --git a/frappe/docs/user/es/bench/resources/bench-commands-cheatsheet.md b/frappe/docs/user/es/bench/resources/bench-commands-cheatsheet.md index e816124fe4..c5277c45bc 100644 --- a/frappe/docs/user/es/bench/resources/bench-commands-cheatsheet.md +++ b/frappe/docs/user/es/bench/resources/bench-commands-cheatsheet.md @@ -1,3 +1,5 @@ +# Bench Commands Cheatsheet + ### Uso General * `bench --version` - Muestra la versión del bench * `bench src` - Muestra el directorio repo del bench diff --git a/frappe/docs/user/es/bench/resources/bench-procfile.md b/frappe/docs/user/es/bench/resources/bench-procfile.md index c80bf03481..dc357bdd4e 100644 --- a/frappe/docs/user/es/bench/resources/bench-procfile.md +++ b/frappe/docs/user/es/bench/resources/bench-procfile.md @@ -1,3 +1,5 @@ +# Bench Procfile + `bench start` usa [honcho](http://honcho.readthedocs.org) para manejar múltiples procesos en **developer mode**. ### Procesos diff --git a/frappe/docs/user/es/bench/resources/index.md b/frappe/docs/user/es/bench/resources/index.md index 0dff60b400..48852466ed 100644 --- a/frappe/docs/user/es/bench/resources/index.md +++ b/frappe/docs/user/es/bench/resources/index.md @@ -1 +1,5 @@ +# Resources + + + {index} \ No newline at end of file diff --git a/frappe/docs/user/es/index.md b/frappe/docs/user/es/index.md index d483120e16..56c4acd3db 100644 --- a/frappe/docs/user/es/index.md +++ b/frappe/docs/user/es/index.md @@ -1,3 +1,3 @@ -# Desarrollo de aplicaciones con Frappe +# Desarrollo de aplicaciones con Frappé {index} diff --git a/frappe/docs/user/es/tutorial/app.md b/frappe/docs/user/es/tutorial/app.md index c7df575fbe..02122f510e 100644 --- a/frappe/docs/user/es/tutorial/app.md +++ b/frappe/docs/user/es/tutorial/app.md @@ -1,10 +1,10 @@ # Qué es una aplicación -Una aplicación en Frappe es una aplicación estandar en Python. Puedes estructurar una aplicación hecha en Frappe de la misma forma que estructuras una aplicación en Python. -Para implementación, Frappe usa los Python Setuptools, lo que nos permite facilmente instalar la aplicación en cualquier computadora. +Una aplicación en Frappé es una aplicación estandar en Python. Puedes estructurar una aplicación hecha en Frappé de la misma forma que estructuras una aplicación en Python. +Para implementación, Frappé usa los Python Setuptools, lo que nos permite facilmente instalar la aplicación en cualquier computadora. -El Framework Frappe provee una interfaz WSGI y para el desarrollo puedes usar el servidor interno de frappe llamado Werkzeug. Para implementación en producción, recomendamos usar nginx y gunicorn. +El Framework Frappé provee una interfaz WSGI y para el desarrollo puedes usar el servidor interno de frappe llamado Werkzeug. Para implementación en producción, recomendamos usar nginx y gunicorn. -Frappe tambien soporta la architectura multi-tenant. Esto significa que puedes correr varios "sitios" en su instalación, cada uno de ellos estará poniendo a disposición un conjunto de aplicaciones y usuarios. La base de datos para cada sitio es separada. +Frappé tambien soporta la architectura multi-tenant. Esto significa que puedes correr varios "sitios" en su instalación, cada uno de ellos estará poniendo a disposición un conjunto de aplicaciones y usuarios. La base de datos para cada sitio es separada. {next} diff --git a/frappe/docs/user/es/tutorial/before.md b/frappe/docs/user/es/tutorial/before.md index e894a3301c..b9eeb84541 100644 --- a/frappe/docs/user/es/tutorial/before.md +++ b/frappe/docs/user/es/tutorial/before.md @@ -1,12 +1,12 @@ # Antes de empezar -

    Una lista de recursos que te ayudaran a inicar con el desarrollo de aplicaciones usando Frappe.

    +

    Una lista de recursos que te ayudaran a inicar con el desarrollo de aplicaciones usando Frappé.

    --- #### 1. Python -Frappe usa Python (v2.7) como lenguaje de parte del servidor. Es altamente recomendable aprender Python antes de iniciar a crear aplicaciones con Frappe. +Frappé usa Python (v2.7) como lenguaje de parte del servidor. Es altamente recomendable aprender Python antes de iniciar a crear aplicaciones con Frappé. Para escribir código de calidad del lado del servidor, también debes incluir pruebas automatizadas. @@ -30,7 +30,7 @@ Recursos: #### 3. HTML / CSS -Si quieres construir interfaces de usuario usando Frappe, necesitas aprender los conceptops básicos de HTML / CSS y el framework de CSS Bootstrap. +Si quieres construir interfaces de usuario usando Frappé, necesitas aprender los conceptops básicos de HTML / CSS y el framework de CSS Bootstrap. Recursos: 1. [Tutorial sobre HTML/CSS de Codecademy](https://www.codecademy.com/learn/learn-html-css) @@ -68,4 +68,4 @@ Recursos: --- -Cuando estes listo, puedes intentar [crear una aplicación simple]({{ docs_base_url }}/user/es/tutorial/app) usando Frappe. +Cuando estes listo, puedes intentar [crear una aplicación simple](/docs/user/es/tutorial/app) usando Frappé. diff --git a/frappe/docs/user/es/tutorial/bench.md b/frappe/docs/user/es/tutorial/bench.md index bc9e3ce215..5ce2f40c0f 100644 --- a/frappe/docs/user/es/tutorial/bench.md +++ b/frappe/docs/user/es/tutorial/bench.md @@ -1,10 +1,10 @@ -# Instalando el Frappe Bench +# Instalando el Frappé Bench -La forma más facíl de configurar frappe en un computador usando sitemas basados en Unix es usando frappe-bench. Lee las instrucciones detalladas acerca de como instalarlo usando Frappe Bench. +La forma más facíl de configurar frappe en un computador usando sitemas basados en Unix es usando frappe-bench. Lee las instrucciones detalladas acerca de como instalarlo usando Frappé Bench. > [https://github.com/frappe/bench](https://github.com/frappe/bench) -Con Frappe Bench vas a poder configurar y hostear multiples aplicaciones y sitios, también va a configurar un entorno virtual de Python por lo que vas a tener un entorno apartado para correr sus aplicaciones (y no va a tener conflictos de versiones con otros entornos de desarrollo). +Con Frappé Bench vas a poder configurar y hostear multiples aplicaciones y sitios, también va a configurar un entorno virtual de Python por lo que vas a tener un entorno apartado para correr sus aplicaciones (y no va a tener conflictos de versiones con otros entornos de desarrollo). El comando `bench` va a ser instalado en su sistema para ayudarlo en la fase de desarrollo y el manejo de la aplicación. diff --git a/frappe/docs/user/es/tutorial/conclusion.md b/frappe/docs/user/es/tutorial/conclusion.md index 05118d1b48..6f9a07fd1c 100644 --- a/frappe/docs/user/es/tutorial/conclusion.md +++ b/frappe/docs/user/es/tutorial/conclusion.md @@ -1,5 +1,5 @@ # Conclusión -Esperamos que esto te haya dado una idea de como son desarrolladas las aplicaicones en Frappe. El objetivo era de que de manera breve se tocaran varios de los aspectos del desarrollo de aplicaciones. Para obtener ayuda en inconvenientes o temas específicos, favor revisar el API. +Esperamos que esto te haya dado una idea de como son desarrolladas las aplicaicones en Frappé. El objetivo era de que de manera breve se tocaran varios de los aspectos del desarrollo de aplicaciones. Para obtener ayuda en inconvenientes o temas específicos, favor revisar el API. Para ayuda, únete a la comunidad en el [canal de chat en Gitter](https://gitter.im/frappe/erpnext) o el [foro de desarrollo](https://discuss.erpnext.com) diff --git a/frappe/docs/user/es/tutorial/controllers.md b/frappe/docs/user/es/tutorial/controllers.md index e9ea7527e8..7053719970 100644 --- a/frappe/docs/user/es/tutorial/controllers.md +++ b/frappe/docs/user/es/tutorial/controllers.md @@ -48,7 +48,7 @@ En este script: Verifica si sus validaciones funcionan creando nuevos registros. -Transaction +Transaction #### Depurando diff --git a/frappe/docs/user/es/tutorial/doctypes.md b/frappe/docs/user/es/tutorial/doctypes.md index 6d574ac721..6a13bde650 100644 --- a/frappe/docs/user/es/tutorial/doctypes.md +++ b/frappe/docs/user/es/tutorial/doctypes.md @@ -6,7 +6,7 @@ Para crear un nuevo **DocType**, ir a: > Developer > Documents > Doctype > New -New Doctype +New Doctype En el DocType, primero el módulo, lo que en nuestro caso es **Library Management** @@ -26,7 +26,7 @@ Fields are much more than database columns, they can be: Vamos a agregar los campos de el Article. -Adding Fields +Adding Fields Cuando agredas los campos, necesitas llenar el campo **Type**. **Label** es opcional para los Section Break y Column Break. **Name** (`fieldname`) es el nombre de la columna en la tabla de la base de datos y tambien el nombre de la propiedad para el controlador. Esto tiene que ser *code friendly*, i.e. Necesitas poner _ en lugar de " ". Si dejas en blanco este campo, se va a llenar automáticamente al momento de guardar. @@ -46,9 +46,9 @@ Podemos agregar los siguientes campos: #### Agregar permisos -Despues de agregar los campos, dar click en hecho y agrega una nueva fila en la sección de Permission Roles. Por ahora, vamos a darle accesos Lectura, Escritura, Creación y Reportes al Role **Librarian**. Frappe cuenta con un sistema basados en el modelo de Roles finamente granulado. Puedes cambiar los permisos más adealante usando el **Role Permissions Manager** desde **Setup**. +Despues de agregar los campos, dar click en hecho y agrega una nueva fila en la sección de Permission Roles. Por ahora, vamos a darle accesos Lectura, Escritura, Creación y Reportes al Role **Librarian**. Frappé cuenta con un sistema basados en el modelo de Roles finamente granulado. Puedes cambiar los permisos más adealante usando el **Role Permissions Manager** desde **Setup**. -Adding Permissions +Adding Permissions #### Guardando diff --git a/frappe/docs/user/es/tutorial/form-client-scripting.md b/frappe/docs/user/es/tutorial/form-client-scripting.md index b0c4b97f21..26f5410b83 100644 --- a/frappe/docs/user/es/tutorial/form-client-scripting.md +++ b/frappe/docs/user/es/tutorial/form-client-scripting.md @@ -1,3 +1,5 @@ +# Form Client Scripting + ## Añadir Scripts a nuestros formularios Ya que tenemos creado el sistema básico que funciona sin problemas sin escribir una linea de código. Vamos a escribir algunos scripts diff --git a/frappe/docs/user/es/tutorial/index.md b/frappe/docs/user/es/tutorial/index.md index 62d30a873c..652e7f9773 100644 --- a/frappe/docs/user/es/tutorial/index.md +++ b/frappe/docs/user/es/tutorial/index.md @@ -1,6 +1,6 @@ -# Tutorial sobre Frappe +# Tutorial sobre Frappé -En esta guía, vamos a mostrarte como crear una aplicación desde cero usando **Frappe**. Usando el ejemplo de un Sistema de Gestión de Librería. Vamos a cubrir: +En esta guía, vamos a mostrarte como crear una aplicación desde cero usando **Frappé**. Usando el ejemplo de un Sistema de Gestión de Librería. Vamos a cubrir: 1. Instalación 1. Creando una nueva App @@ -12,10 +12,10 @@ En esta guía, vamos a mostrarte como crear una aplicación desde cero usando ** ## Para Quién es este tutorial? -Esta guía esta orientada para desarrolladores de software que estan familiarizados con el proceso de como son creadas y servidas las aplicaciones web. El Framework Frappe está escrito en Python y usa MariaDB como base de datos y para la creación de las vistas web usa HTML/CSS/Javascript. Por lo que sería excelente si estas familiarizado con estas tecnologías. +Esta guía esta orientada para desarrolladores de software que estan familiarizados con el proceso de como son creadas y servidas las aplicaciones web. El Framework Frappé está escrito en Python y usa MariaDB como base de datos y para la creación de las vistas web usa HTML/CSS/Javascript. Por lo que sería excelente si estas familiarizado con estas tecnologías. Por lo menos, si nunca haz usado Python antes, deberías tomar un tutorial rápido antes de iniciar con este tutorial. -Frappe usa el sistema de gestión de versiones en GitHub. También, es importante estar familiarizado con los conceptos básicos de git y tener una cuenta en GitHub para manejar sus aplicaciones. +Frappé usa el sistema de gestión de versiones en GitHub. También, es importante estar familiarizado con los conceptos básicos de git y tener una cuenta en GitHub para manejar sus aplicaciones. ## Ejemplo @@ -27,7 +27,7 @@ Para esta guía, vamos a crear una aplicación simple llamada **Library Manageme 1. Library Membership (Un período en el que un miembro esta permitido hacer una trasacción) 1. Library Management Setting (Configuraciones generales, como el tiempo que dura el prestamo de un artículo) -La interfaz de usuario (UI) para la aplicación va a ser el **Frappe Desk**, un entorno para UI basado en el navegador y viene integrado en Frappe donde los formularios son generados automáticamente desde los modelos y los roles y permisos son aplicados. +La interfaz de usuario (UI) para la aplicación va a ser el **Frappé Desk**, un entorno para UI basado en el navegador y viene integrado en Frappé donde los formularios son generados automáticamente desde los modelos y los roles y permisos son aplicados. También, vamos a crear vistas webs para la librería donde los usuarios pueden buscar los artículos desde una página web. diff --git a/frappe/docs/user/es/tutorial/models.md b/frappe/docs/user/es/tutorial/models.md index 4951c55ec0..406558cae1 100644 --- a/frappe/docs/user/es/tutorial/models.md +++ b/frappe/docs/user/es/tutorial/models.md @@ -1,6 +1,6 @@ # Creando Modelos -El siguiente paso es crear los modelos que discutimos en la introducción. En Frappe, los modelos son llamados **DocTypes**. Puedes crear nuevos DocTypes desde el UI Escritorio de Frappe. **DocTypes** son creados de campos llamados **DocField** y los permisos basados en roles son integrados dentro de los modelos, estos son llamados **DocPerms**. +El siguiente paso es crear los modelos que discutimos en la introducción. En Frappé, los modelos son llamados **DocTypes**. Puedes crear nuevos DocTypes desde el UI Escritorio de Frappé. **DocTypes** son creados de campos llamados **DocField** y los permisos basados en roles son integrados dentro de los modelos, estos son llamados **DocPerms**. Cuando un DocType es guardado, se crea una nueva tabla en la base de datos. Esta tabla se nombra `tab[doctype]`. diff --git a/frappe/docs/user/es/tutorial/naming-and-linking.md b/frappe/docs/user/es/tutorial/naming-and-linking.md index da929b0272..52de06add7 100644 --- a/frappe/docs/user/es/tutorial/naming-and-linking.md +++ b/frappe/docs/user/es/tutorial/naming-and-linking.md @@ -4,7 +4,7 @@ Vamos a crear otro DocType y guardarlo: 1. Library Member (First Name, Last Name, Email Address, Phone, Address) -Doctype Saved +Doctype Saved #### Nombrando DocTypes @@ -20,21 +20,21 @@ Esto puede ser seteado a traves del campo **Autoname**. Para el controlador, dej > **Search Fields**: Un DocType puede ser nombrado por serie pero seguir teniendo la necesidad de ser buscado por nombre. En nuestro caso, el Article va ser buscado por el título o el nombre del autor. Por lo que vamos a poner esos campos en el campo de search. -Autonaming and Search Field +Autonaming and Search Field #### Campo de Enlace y Campo Select -Las claves foraneas son específicadas en Frappe como campos **Link** (Enlace). El DocType debe ser mencionado en el area de texto de Options. +Las claves foraneas son específicadas en Frappé como campos **Link** (Enlace). El DocType debe ser mencionado en el area de texto de Options. En nuestro ejemplo, en el DocType de Library Transaction,tenemos que enlazar los dos DocTypes de Library Member and the Article. **Nota:** Recuerda que los campos de Enlace no son automáticamente establecidos como claves foraneas en la base de datos MariaDB, porque esto crearía un indice en la columna. Las validaciones de claves foraneas son realizadas por el Framework. -Link Field +Link Field Por campos de tipo Select, como mencionamos antes, agrega varias opciones en la caja de texto **Options**, cada una en una nueva linea. -Select Field +Select Field De manera similar continua haciendo los otros modelos. @@ -44,7 +44,7 @@ Un patrón estandar es que cuando seleccionas un ID, dice **Library Member** en Para hacer esto, podemos usar campos de solo lectura y en opciones, podemos especificar el nombre del link (enlace) y el campo o propiedad que deseas obtener. Para este ejempo en **Member First Name** podemos especificar `library_member.first_name`. -Fetch values +Fetch values ### Completar los modelos @@ -52,19 +52,19 @@ En la misma forma, puedes completar todos los modelos, todos los campos deben ve #### Article -Article +Article #### Library Member -Library Member +Library Member #### Library Membership -Library Membership +Library Membership #### Library Transaction -Library Transaction +Library Transaction > Asegurate de dar permiso a **Librarian** en cada DocType diff --git a/frappe/docs/user/es/tutorial/new-app.md b/frappe/docs/user/es/tutorial/new-app.md index 2978145db7..5591c9d997 100644 --- a/frappe/docs/user/es/tutorial/new-app.md +++ b/frappe/docs/user/es/tutorial/new-app.md @@ -7,7 +7,7 @@ Para crear una nueva aplicación, debes posicionarte en el directorio del bench $ 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 Publisher: Frappé App Email: info@frappe.io App Icon (default 'octicon octicon-file-directory'): octicon octicon-book App Color (default 'grey'): #589494 @@ -44,11 +44,11 @@ La aplicación va a ser creada en el directorio llamado `library_management` y v 1. `config` contiene la información de configuración de la aplicación. 1. `desktop.py` es donde los íconos del escritorio pueden ser agregados al mismo. 1. `hooks.py` es donde se configuran las integraciones con el entorno y otras aplicaciones. -1. `library_management` (dentro) es un **módulo** que está contenido. En Frappe, un **módulo** es donde los modelos y controladores se almacenan. +1. `library_management` (dentro) es un **módulo** que está contenido. En Frappé, un **módulo** es donde los modelos y controladores se almacenan. 1. `modules.txt` contiene la lista de **módulos** en la aplicación. Cuando creas un nuevo módulo, es obligatorio que lo agregues a este archivo. 1. `patches.txt` es donde los patches para migraciones son establecidos. Son módulos de Python referenciados usando la nomenclatura de punto. -1. `templates` es el directorio donde son mantenidos las plantillas de vistas web. Plantillas para **Login** y otras páginas estandar estan contenidas en Frappe. -1. `generators` son donde las plantillas para los modelos son almacenadas, donde cada instancia de modelo tiene una ruta web separada, por ejemplo un **Blog Post** donde cada post tiene una url única. En Frappe, el manejador de plantillas utilizado es Jinja2. +1. `templates` es el directorio donde son mantenidos las plantillas de vistas web. Plantillas para **Login** y otras páginas estandar estan contenidas en Frappé. +1. `generators` son donde las plantillas para los modelos son almacenadas, donde cada instancia de modelo tiene una ruta web separada, por ejemplo un **Blog Post** donde cada post tiene una url única. En Frappé, el manejador de plantillas utilizado es Jinja2. 1. `pages` es donde las rutas simples son almacenadas. Por ejemplo para un tipo de página "/blog". {next} diff --git a/frappe/docs/user/es/tutorial/reports.md b/frappe/docs/user/es/tutorial/reports.md index 3fb5a9d76e..99d6655792 100644 --- a/frappe/docs/user/es/tutorial/reports.md +++ b/frappe/docs/user/es/tutorial/reports.md @@ -2,6 +2,6 @@ Puedes dar click en el texto que dice Reportes en el panel lateral izquierdo para ver los registros de manera tabulada. -Report +Report {next} diff --git a/frappe/docs/user/es/tutorial/roles.md b/frappe/docs/user/es/tutorial/roles.md index d65aed184c..017cd064c9 100644 --- a/frappe/docs/user/es/tutorial/roles.md +++ b/frappe/docs/user/es/tutorial/roles.md @@ -9,6 +9,6 @@ Para crear un nuevo Role, ir a: > Setup > Users > Role > New -Adding Roles +Adding Roles {next} diff --git a/frappe/docs/user/es/tutorial/setting-up-the-site.md b/frappe/docs/user/es/tutorial/setting-up-the-site.md index 83c925d909..28a0dd7301 100644 --- a/frappe/docs/user/es/tutorial/setting-up-the-site.md +++ b/frappe/docs/user/es/tutorial/setting-up-the-site.md @@ -19,7 +19,7 @@ Vamos a crear un nuevo Site llamado `library`. Ahora puedes instalar un nuevo site, ejecutando el comando `bench new-site library`. La ejecución del comando anterior va a generar una nueva base de datos, un directorio en la carpeta sites y va a instalar `frappe` (el cual también es una aplicación!) en el nuevo site. - La aplicación `frappe` tiene dos módulos integrados que son **Core** y **Website**. El módulo Core contiene los modelos básicos para la aplicación. Frappe es un Framework con muchas funcionalidades incluidas y viene con muchos modelos integrados. Estos modelos son llamados **DocTypes**. Vamos a ver más de esto en lo adelante. + La aplicación `frappe` tiene dos módulos integrados que son **Core** y **Website**. El módulo Core contiene los modelos básicos para la aplicación. Frappé es un Framework con muchas funcionalidades incluidas y viene con muchos modelos integrados. Estos modelos son llamados **DocTypes**. Vamos a ver más de esto en lo adelante. $ bench new-site library MySQL root password: diff --git a/frappe/docs/user/es/tutorial/single-doctypes.md b/frappe/docs/user/es/tutorial/single-doctypes.md index 0b8d7745c2..cb6d55644b 100644 --- a/frappe/docs/user/es/tutorial/single-doctypes.md +++ b/frappe/docs/user/es/tutorial/single-doctypes.md @@ -1,9 +1,9 @@ # DocTypes Simples -Una aplicación normalmente va a tener una página de configuración. En nuestra aplicación, podemos definir una página donde específiquemos el período de prestamos. Necesitaremos almacenar esta propiedad. En Frappe, esto puede lograrse usando los DocType de tipo **Simple**. Un DocType Simple es como el patrón Singleton en Java. Es un objeto con tan solo una instancia. Vamos a llamarlo **Library Managment Settings**. +Una aplicación normalmente va a tener una página de configuración. En nuestra aplicación, podemos definir una página donde específiquemos el período de prestamos. Necesitaremos almacenar esta propiedad. En Frappé, esto puede lograrse usando los DocType de tipo **Simple**. Un DocType Simple es como el patrón Singleton en Java. Es un objeto con tan solo una instancia. Vamos a llamarlo **Library Managment Settings**. Para crear un Single DocType, marca el checkbox **Is Single**. -Single Doctypes +Single Doctypes {next} diff --git a/frappe/docs/user/es/tutorial/start.md b/frappe/docs/user/es/tutorial/start.md index f8abb4d675..51f63b6acb 100644 --- a/frappe/docs/user/es/tutorial/start.md +++ b/frappe/docs/user/es/tutorial/start.md @@ -14,7 +14,7 @@ Para iniciar el servidor de desarrollo, ejecuta `bench start`. Ahora abre tu navegador y ve a la dirección `http://localhost:8000`. Deberías ver la páagina de inicio de sesión si todo salió bien.: -Login Screen +Login Screen Ahora accede con : @@ -24,8 +24,8 @@ Password : **Usa la contraseña que creaste durante la instalación** Cuando accedas, deberías poder ver la página de inicio (Desk). -Desk +Desk -Como puedes ver, el sistema básico de Frappe viene con algunas aplicaciones preinstaladas como To Do, File Manager etc. Estas aplicaciones pueden integrarse en el flujo de trabajo de su aplicació a medida que avancemos. +Como puedes ver, el sistema básico de Frappé viene con algunas aplicaciones preinstaladas como To Do, File Manager etc. Estas aplicaciones pueden integrarse en el flujo de trabajo de su aplicació a medida que avancemos. {next} diff --git a/frappe/docs/user/es/tutorial/task-runner.md b/frappe/docs/user/es/tutorial/task-runner.md index 215bd6b46d..148ae61b00 100644 --- a/frappe/docs/user/es/tutorial/task-runner.md +++ b/frappe/docs/user/es/tutorial/task-runner.md @@ -1,6 +1,6 @@ # Tareas Programadas -Finalmente, una aplicación también tiene que mandar notificaciones de email y hacer otros tipos de tareas programadas. En Frappe, si tienes el bench configurado, el programador de tareas es configurado vía Celery usando Redis Queue. +Finalmente, una aplicación también tiene que mandar notificaciones de email y hacer otros tipos de tareas programadas. En Frappé, si tienes el bench configurado, el programador de tareas es configurado vía Celery usando Redis Queue. Para agregar un nuevo manejador(Handler) de tareas, ir a `hooks.py` y agrega un nuevo manejador. Los manejadores (Handlers) por defecto son `all`, `daily`, `weekly`, `monthly`. El manejador `all` es llamado cada 3 minutos por defecto. @@ -15,7 +15,7 @@ Para agregar un nuevo manejador(Handler) de tareas, ir a `hooks.py` y agrega un Aquí hacemos referencia a una función Python que va a ser ejecutada diariamente. Vamos a ver como se ve esta función: - # Copyright (c) 2013, Frappe + # Copyright (c) 2013, Frappé # For license information, please see license.txt from __future__ import unicode_literals diff --git a/frappe/docs/user/es/tutorial/users-and-records.md b/frappe/docs/user/es/tutorial/users-and-records.md index e599c4301b..2885c6bd5c 100644 --- a/frappe/docs/user/es/tutorial/users-and-records.md +++ b/frappe/docs/user/es/tutorial/users-and-records.md @@ -1,6 +1,6 @@ # Creando Usuarios y Registros -Teniendo los modelos creados, podemos empezar a crear registros usando la interfaz gráfica de usuario de Frappe. No necesitas crear vistas! Las vistas en Frappe son automáticamente creadas basadas en las propiedades del DocType. +Teniendo los modelos creados, podemos empezar a crear registros usando la interfaz gráfica de usuario de Frappé. No necesitas crear vistas! Las vistas en Frappé son automáticamente creadas basadas en las propiedades del DocType. ### 4.1 Creando Usuarios @@ -12,7 +12,7 @@ Crea un nuevo Usuario y llena los campos de nombre, primer nombre y nueva contra Luego dale los Roles de Librarian y de Library Member a este usuario. -Add User Roles +Add User Roles Ahora cierra sesión y accede usando las credenciales del nuevo usuario. @@ -20,36 +20,36 @@ Ahora cierra sesión y accede usando las credenciales del nuevo usuario. Debes ver un ícono del módulo de Library Management. Dar click en el ícono para entrar a la página del módulo: -Library Management Module +Library Management Module Aquí puedes ver los DocTypes que fueron creados para la aplicación. Vamos a comenzar a crear nuevos registros. Primero vamos a crear un nuevo Article: -New Article +New Article Aquí vas a ver que los DocTypes que haz creado han sido renderizados como un formulario. Las validaciones y las otras restricciones también están aplicadas según se diseñaron. Vamos a llenar los datos de un Article. -New Article +New Article Puedes agregar una imagen si deseas. -Attach Image +Attach Image Ahora vamos a crear un nuevo miembro: -New Library Member +New Library Member Despues de esto, crearemos una nueva membresía (membership) para el miembro. Si recuerdas, aquí hemos específicado los valores del nombre y apellido del miembro directamente desde el registro del miembro tan pronto selecciones el miembro id, los nombres serán actualizados. -New Library Membership +New Library Membership Como puedes ver la fecha tiene un formato de año-mes-día lo cual es una fecha del sistema. Para seleccionar o cambiar la fecha, tiempo y formatos de números, ir a: > Setup > Settings > System Settings -System Settings +System Settings {next} diff --git a/frappe/docs/user/es/tutorial/web-views.md b/frappe/docs/user/es/tutorial/web-views.md index 2fe98f1523..79a311e7b5 100644 --- a/frappe/docs/user/es/tutorial/web-views.md +++ b/frappe/docs/user/es/tutorial/web-views.md @@ -1,8 +1,8 @@ # Vistas Web (Web Views) -Frappe tiene dos entornos principales, El escritorio y la Web. El escritorio es una interfaz de usuario controlada con una excelente aplicación AJAX y la web es mas plantillas de HTML tradicionales dispuestas para consumo público. Vistas Web pueden también ser generadas para crear vistas controladas para los usuarios que puedes acceder al sistema pero aún así no tener acceso al escritorio. +Frappé tiene dos entornos principales, El escritorio y la Web. El escritorio es una interfaz de usuario controlada con una excelente aplicación AJAX y la web es mas plantillas de HTML tradicionales dispuestas para consumo público. Vistas Web pueden también ser generadas para crear vistas controladas para los usuarios que puedes acceder al sistema pero aún así no tener acceso al escritorio. -En Frappe, Las vistas web son manejadas por plantillas que estan usualmente en el directorio `templates`. Hay dos tipos principales de plantillas. +En Frappé, Las vistas web son manejadas por plantillas que estan usualmente en el directorio `templates`. Hay dos tipos principales de plantillas. 1. Pages: Estos son plantillas Jinja donde una vista existe solo para una ruta. ejemplo. `/blog`. 2. Generators: Estas son plantiallas donde cada instancia de un DocType tiene una ruta diferente `/blog/a-blog`, `blog/b-blog` etc. @@ -16,11 +16,11 @@ Vamos a ver las Vistas web estandar: Si estas logueado como el usuario de prueba, ve a `/article` y deberías ver la lista de artículos. -web list +web list Da click en uno de los artículos y vas a ver una vista web por defecto -web view +web view Si deseas hacer una mejor vista para la lista de artículos, crea un archivo llamado `row_template.html` en el directorio `library_management/templates/includes/list/`. Aquí hay un archivo de ejemplo: @@ -45,11 +45,11 @@ Aquí, vas a tener todas las propiedades de un artículo en el objeto `doc`. La lista actualizada debe lucir de esta manera! -new web list +new web list #### Página de Inicio -Frappe también tiene vistas para el registro de usuarios que incluye opciones de registro usando Google, Facebook y GitHub. Cuando un usuario se registra vía la web, no tiene acceso a la interfaz del Escritorio por defecto. +Frappé también tiene vistas para el registro de usuarios que incluye opciones de registro usando Google, Facebook y GitHub. Cuando un usuario se registra vía la web, no tiene acceso a la interfaz del Escritorio por defecto. > Para permitirles a los usuarios acceso al Escritorio, debes especificar que el usuario es de tipo "System User" en Setup > User diff --git a/frappe/docs/user/es/videos/index.md b/frappe/docs/user/es/videos/index.md index a5fb5f4638..ef190c1a14 100644 --- a/frappe/docs/user/es/videos/index.md +++ b/frappe/docs/user/es/videos/index.md @@ -1,8 +1,8 @@ -# Videos Tutoriales acerca del Framework Frappe +# Videos Tutoriales acerca del Framework Frappé -Este video tutorial de 10 videos va a enseñarte como crear aplicaciones complejas en Frappe. +Este video tutorial de 10 videos va a enseñarte como crear aplicaciones complejas en Frappé. -Prerrequisitos: Debes tener conocimientos básicos de Python, Javascript y MySQl antes de empezar este tutorial. +Prerrequisitos: Debes tener conocimientos básicos de Python, Javascript y MySQl antes de empezar este tutorial. --- diff --git a/frappe/docs/user/fr/index.md b/frappe/docs/user/fr/index.md index a25785e59e..a68da6210b 100644 --- a/frappe/docs/user/fr/index.md +++ b/frappe/docs/user/fr/index.md @@ -1,3 +1,3 @@ -# Develop Apps with Frappe +# Develop Apps with Frappé {index} diff --git a/frappe/docs/user/fr/tutorial/app.md b/frappe/docs/user/fr/tutorial/app.md index 54c4fffd1e..9061e5348c 100644 --- a/frappe/docs/user/fr/tutorial/app.md +++ b/frappe/docs/user/fr/tutorial/app.md @@ -1,13 +1,13 @@ # Qu'est ce qu'une application ? -Dans Frappe, une application est juste une application Python standard. Vous pouvez structurer une application Frappe de -la même facon que vous structurez une application Python standard. Pour le déploiement, Frappe utilise Setuptools donc +Dans Frappé, une application est juste une application Python standard. Vous pouvez structurer une application Frappé de +la même facon que vous structurez une application Python standard. Pour le déploiement, Frappé utilise Setuptools donc vous pouvez facilement déployer votre application sur n'importe quelle machine. -Frappe fournit une interface WSGI et pendant vos développements vous pouvez utiliser le serveur Werkzeug embarqué. Pour le +Frappé fournit une interface WSGI et pendant vos développements vous pouvez utiliser le serveur Werkzeug embarqué. Pour le déploiement en production, nous recommandons d'utiliser nginx et gunicorn. -Frappe, c'est aussi une une architecture multi-tenant ce qui signifie que vous pouvez lancer plusieurs sites sur une même +Frappé, c'est aussi une une architecture multi-tenant ce qui signifie que vous pouvez lancer plusieurs sites sur une même configuration, chaque site utilisant ses propres applications et utilisateurs. La base de données de chaque site est indépendante. {next} diff --git a/frappe/docs/user/fr/tutorial/before.md b/frappe/docs/user/fr/tutorial/before.md index 9c73c934dc..b10e89c33d 100644 --- a/frappe/docs/user/fr/tutorial/before.md +++ b/frappe/docs/user/fr/tutorial/before.md @@ -1,6 +1,6 @@ # Avant de commencer -

    Liste des outils et technologies utiles dans le développement d'applications avec Frappe.

    +

    Liste des outils et technologies utiles dans le développement d'applications avec Frappé.

    Il y a un grand nombres de tutoriels en ligne et nous recommandons [CodeAcademy](http://www.codecademy.com/) ou vous trouverez beaucoup de ressources de qualité. @@ -8,7 +8,7 @@ Il y a un grand nombres de tutoriels en ligne et nous recommandons [CodeAcademy] #### 1. Python -La partie serveur de Frappe est codée en Python et c'est une bonne idée d'[apprendre rapidement Python](http://www.codecademy.com/tracks/python) avant de commencer à comprendre Frappe. [Le tutoriel sur docs.python.org](https://docs.python.org/2.7/tutorial/index.html) est aussi une excellente ressource pour apprendre Python. Notez que Frappe utilise Python 2.7. +La partie serveur de Frappé est codée en Python et c'est une bonne idée d'[apprendre rapidement Python](http://www.codecademy.com/tracks/python) avant de commencer à comprendre Frappé. [Le tutoriel sur docs.python.org](https://docs.python.org/2.7/tutorial/index.html) est aussi une excellente ressource pour apprendre Python. Notez que Frappé utilise Python 2.7. Pour produire une application de qualité, vous devez inclure des tests automatiques. Vous pouvez comprendre les bases du développement par les tests [ici](http://code.tutsplus.com/tutorials/beginning-test-driven-development-in-python--net-30137). @@ -45,5 +45,5 @@ Si vous voulez modifier les affichages, vous devez apprendre le [language de tem --- -Quand vous êtes prêts, [essayez de développer une application simple avec Frappe]({{ docs_base_url }}/user/fr/tutorial/app) +Quand vous êtes prêts, [essayez de développer une application simple avec Frappé](/docs/user/fr/tutorial/app) diff --git a/frappe/docs/user/fr/tutorial/bench.md b/frappe/docs/user/fr/tutorial/bench.md index c2902b3a87..979202dfd5 100644 --- a/frappe/docs/user/fr/tutorial/bench.md +++ b/frappe/docs/user/fr/tutorial/bench.md @@ -1,8 +1,8 @@ -# Installer le bench de Frappe +# Installer le bench de Frappé -La façon la plus rapide d'installer Frappe sur un système Unix est d'utiliser frappe-bench. Lisez les instructions détaillées sur l'installation et l'utilisation de Frappe Bench : [https://github.com/frappe/bench](https://github.com/frappe/bench) +La façon la plus rapide d'installer Frappé sur un système Unix est d'utiliser frappe-bench. Lisez les instructions détaillées sur l'installation et l'utilisation de Frappé Bench : [https://github.com/frappe/bench](https://github.com/frappe/bench) -Avec Frappe Bench vous pourrez configurer et héberger plusieurs sites et applications. Frappe Bench configure aussi un environnement virtuel Python (Virtualenv), vos applications seront donc isolées les unes des autres et vous n'aurez pas de conflits avec d'autres environnements de développement. +Avec Frappé Bench vous pourrez configurer et héberger plusieurs sites et applications. Frappé Bench configure aussi un environnement virtuel Python (Virtualenv), vos applications seront donc isolées les unes des autres et vous n'aurez pas de conflits avec d'autres environnements de développement. La commande `bench` sera ajoutée pour vous aider dans le développement et la gestion de vos applications. diff --git a/frappe/docs/user/fr/tutorial/conclusion.md b/frappe/docs/user/fr/tutorial/conclusion.md index 8428b362b2..16929104c6 100644 --- a/frappe/docs/user/fr/tutorial/conclusion.md +++ b/frappe/docs/user/fr/tutorial/conclusion.md @@ -1,6 +1,6 @@ # Conclusion -Nous espérons vous avoir donné un apercu de comment les applications sont développées avec Frappe. L'ojectif était de vous +Nous espérons vous avoir donné un apercu de comment les applications sont développées avec Frappé. L'ojectif était de vous montrer brievement, les différents aspects du développement d'une application en vous donnant un apércu général. Pour aller plus loin dans les explications, consultez l'API. diff --git a/frappe/docs/user/fr/tutorial/controllers.md b/frappe/docs/user/fr/tutorial/controllers.md index dd9f2ab69d..2175ffbac8 100644 --- a/frappe/docs/user/fr/tutorial/controllers.md +++ b/frappe/docs/user/fr/tutorial/controllers.md @@ -50,7 +50,7 @@ Dans ce script: Vérifiez vos validations en créant de nouveaux enregistrements. -Transaction +Transaction #### Debogage diff --git a/frappe/docs/user/fr/tutorial/doctypes.md b/frappe/docs/user/fr/tutorial/doctypes.md index 698e1e6c69..69dae3a41a 100644 --- a/frappe/docs/user/fr/tutorial/doctypes.md +++ b/frappe/docs/user/fr/tutorial/doctypes.md @@ -6,7 +6,7 @@ Pour créer un nouveau **DocType**, rendez-vous sur: > Developer > Documents > Doctype > New -New Doctype +New Doctype Dans un premier temps, saisissez le module, dans notre cas, **Library Managment** @@ -25,7 +25,7 @@ Les champs sont bien plus que des colonnes d'une base de données, ils peuvent Ajoutons des champs pour l'article. -Adding Fields +Adding Fields Quand vous ajoutez des champs, vous devez entrer le **Type**. Le **Label** est optionnel pour les retours de sections et de colonnes. Le **Name** (`fieldname`) ets le nom de la colonne dans la base de données et aussi la propriété du controleur. Les définitions @@ -50,11 +50,11 @@ sur chacune des lignes comme ci-dessous #### Ajouter des permissins Après avoir ajouté les champs, validez et ajoutez un nouveau rôle dans la section des règles de permissions. Pour le moment -ajoutons les droits le lecture, écriture, création et suppression au modèle **Librarian**. Frappe à une gestion fine des +ajoutons les droits le lecture, écriture, création et suppression au modèle **Librarian**. Frappé à une gestion fine des permissions sur les modèles. Vous pouvez aussi changer les permissions plus tard en utilisant le gestionnaire de permissions dans la configuration. -Adding Permissions +Adding Permissions #### Sauvegarde diff --git a/frappe/docs/user/fr/tutorial/form-client-scripting.md b/frappe/docs/user/fr/tutorial/form-client-scripting.md index f2f2e21894..a6ab64a60c 100644 --- a/frappe/docs/user/fr/tutorial/form-client-scripting.md +++ b/frappe/docs/user/fr/tutorial/form-client-scripting.md @@ -1,3 +1,5 @@ +# Form Client Scripting + ## Codes des formulaires Jusqu'a maintenant, nous avons développé un système basique qui fonctionne parfaitement sans avoir eu besoin d'écrire une diff --git a/frappe/docs/user/fr/tutorial/index.md b/frappe/docs/user/fr/tutorial/index.md index b518478c72..3aa6ccb6ee 100644 --- a/frappe/docs/user/fr/tutorial/index.md +++ b/frappe/docs/user/fr/tutorial/index.md @@ -1,6 +1,6 @@ -# Tutoriel Frappe +# Tutoriel Frappé -Dans ce guide nous allons vous montrer comment créer une application de A à Z en utilisant **Frappe**. Avec un +Dans ce guide nous allons vous montrer comment créer une application de A à Z en utilisant **Frappé**. Avec un exemple de gestion de bibliothèque, nous allons aborder les sujets suivants: 1. Installation @@ -13,12 +13,12 @@ exemple de gestion de bibliothèque, nous allons aborder les sujets suivants: ## A qui s'adresse ce tutoriel ? -Ce guide est à destination des développeurs familiers avec la création d'applications web. Le framework Frappe est développé +Ce guide est à destination des développeurs familiers avec la création d'applications web. Le framework Frappé est développé avec Python, utilise le système de base de données MariaDB et HTML/CSS/Javascript pour le rendu des pages. Il est donc nécessaire d'être familier avec ces technologies. Si vous n'avez jamais utilisé Python auparavant, vous devriez suivre un tutoriel rapide avant de suivre ce guide. -Frappe utilise le système de gestion de version git sur Github. Il est donc important que vous connaissiez les bases de +Frappé utilise le système de gestion de version git sur Github. Il est donc important que vous connaissiez les bases de l'utilisation de git et que vous ayez un compte sur Github pour gérer vos applications. ## Exemple @@ -32,7 +32,7 @@ les modèles suivants: 1. Library Membership (période pendant laquelle un membre peut emprunter) 1. Library Management Setting (configuration générale) -L'interface utilisateur (UI) pour le bibliothécaire sera **Frappe Desk**, un système de rendu d'interface où les formulaires sont +L'interface utilisateur (UI) pour le bibliothécaire sera **Frappé Desk**, un système de rendu d'interface où les formulaires sont automatiquement générés depuis les modèles en appliquant rôles et permissions. Nous allons aussi créer des vues pour la bibliothèque afin que les utilisateurs puissent parcourir la liste des livres depuis un site internet. diff --git a/frappe/docs/user/fr/tutorial/models.md b/frappe/docs/user/fr/tutorial/models.md index f67d356bf6..3d69438512 100644 --- a/frappe/docs/user/fr/tutorial/models.md +++ b/frappe/docs/user/fr/tutorial/models.md @@ -1,6 +1,6 @@ # Définir des modèles -La prochaine étape est de définir les modèles que nous avons présenté en introduction. Dans **Frappe**, les modèles sont appelés +La prochaine étape est de définir les modèles que nous avons présenté en introduction. Dans **Frappé**, les modèles sont appelés des **DocTypes**. Vous pouvez définir de nouveaux **DocTypes** depuis l'interface. Les **DocTypes** sont faits de **DocField** et de permissions appelées **DocPerms**. diff --git a/frappe/docs/user/fr/tutorial/naming-and-linking.md b/frappe/docs/user/fr/tutorial/naming-and-linking.md index 28ef965efa..0cb2be8bf0 100644 --- a/frappe/docs/user/fr/tutorial/naming-and-linking.md +++ b/frappe/docs/user/fr/tutorial/naming-and-linking.md @@ -4,7 +4,7 @@ Définissons un nouveau **DocType**: 1. Library Member (First Name, Last Name, Email Address, Phone, Address) -Doctype Saved +Doctype Saved #### Le nommage des DocTypes @@ -21,11 +21,11 @@ Cela peut être configuré par le champs **Autoname**. Pour le controleur, laiss > **Search Fields**: Un **DocType** peut être nommé sur la base d'une serie mais nous devons toujours pouvoir le chercher par un nom. Dans notre cas, l'arcicle peut etre cherché par un titre ou par l'auteur. Remplissons donc le champs **Search Fields**. -Autonaming and Search Field +Autonaming and Search Field #### Relation et champs select -Les clés étrangères sont, dans Frappe, traduits par un champs de type **Link**. Le **DocType** ciblé doit être mentionné +Les clés étrangères sont, dans Frappé, traduits par un champs de type **Link**. Le **DocType** ciblé doit être mentionné dans le champs **Options**. Dans notre exemple, pour le **doctype** `Library Transaction`, nous avons un lien vers `Library Member` et vers `Article`. @@ -34,12 +34,12 @@ Dans notre exemple, pour le **doctype** `Library Transaction`, nous avons un lie d'indexer la colonne. Cela pourrait ne pas être optimum, c'est pour cela que la validation de la clé étrangère est faite par le framework. -Link Field +Link Field Pour les champs **select**, comme mentionné plus tôt, ajoutez chacune des options dans le champs **Options**, chaque option sur une nouvelle ligne. -Select Field +Select Field faites de même pour les autres modèles. @@ -52,7 +52,7 @@ Pour cela, nous pouvons utiliser des champs en lecture seules et, dans les optio et le nom du champs de la propriété que nous voulons parcourir. Dans cet exemple, dans **Member First Name** nous pouvons définir `library_member.first_name` -Fetch values +Fetch values ### Completer les Modeles @@ -60,19 +60,19 @@ De la même facon, vous pouvez compléter les autres modèles pour qu'au final l #### Article -Article +Article #### Library Member -Library Member +Library Member #### Library Membership -Library Membership +Library Membership #### Library Transaction -Library Transaction +Library Transaction > Vérifiez que le modèles **Librarian** aient les permissions sur chaque **DocType**. diff --git a/frappe/docs/user/fr/tutorial/new-app.md b/frappe/docs/user/fr/tutorial/new-app.md index 418a1643be..c3db75226c 100644 --- a/frappe/docs/user/fr/tutorial/new-app.md +++ b/frappe/docs/user/fr/tutorial/new-app.md @@ -9,7 +9,7 @@ remplissez les informations à propos de votre application. Cela va créer un te $ 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 Publisher: Frappé App Email: info@frappe.io App Icon (default 'octicon octicon-file-directory'): octicon octicon-book App Color (default 'grey'): #589494 @@ -46,11 +46,11 @@ L'application sera créée dans un répertoire appelé `library_management` et a 1. `config` contient les configurations de l'application 1. `desktop.py` est l'endroit ou les icones peuvent être ajoutées au bureau 1. `hooks.py` contient les définitions de la facon dont l'intégration avec l'environnement et les autres applications est faite. -1. `library_management` (interne) est un **module** bootstrappé. Dans Frappe, un **module** est l'endroit où sont les controlleurs et les modeles. +1. `library_management` (interne) est un **module** bootstrappé. Dans Frappé, un **module** est l'endroit où sont les controlleurs et les modeles. 1. `modules.txt` contient la liste des **modules** dans l'application. Quand vous créez un nouveau module, c'est obligatoire de l'ajouter dans ce fichier. 1. `patches.txt` contient les patchs de migration. Ce sont des références Python utilisant la notation par point. -1. `templates` est le repertoire qui contient les templates des vues. Les templates pour **Login** et autres pages par défaut sont déjà contenus dans Frappe. -1. `generators` est l'endroit où sont stockés les templates des modèles. Chaque instance du modèle a une route web comme par exemple les **articles de blog** ou chaque article a une adresse unique. Dans Frappe, Jinj2 est utilisé pour les templates. +1. `templates` est le repertoire qui contient les templates des vues. Les templates pour **Login** et autres pages par défaut sont déjà contenus dans Frappé. +1. `generators` est l'endroit où sont stockés les templates des modèles. Chaque instance du modèle a une route web comme par exemple les **articles de blog** ou chaque article a une adresse unique. Dans Frappé, Jinj2 est utilisé pour les templates. 1. `pages` contient les routes uniques pour les templates. Par exemple "/blog" ou "/article" {next} diff --git a/frappe/docs/user/fr/tutorial/reports.md b/frappe/docs/user/fr/tutorial/reports.md index 92b0ef4d45..b9bf4d024d 100644 --- a/frappe/docs/user/fr/tutorial/reports.md +++ b/frappe/docs/user/fr/tutorial/reports.md @@ -2,6 +2,6 @@ Vous pouvez aussi cliquer sur le texte "Rapports" dans la barre latérale de gauche pour voir vos données dans un tableau. -Report +Report {next} diff --git a/frappe/docs/user/fr/tutorial/roles.md b/frappe/docs/user/fr/tutorial/roles.md index ce5add6000..e0bc38da5b 100644 --- a/frappe/docs/user/fr/tutorial/roles.md +++ b/frappe/docs/user/fr/tutorial/roles.md @@ -10,6 +10,6 @@ Pour créer un nouveau rôle, se rendre sur: > Setup > Users > Role > New -Adding Roles +Adding Roles {next} diff --git a/frappe/docs/user/fr/tutorial/setting-up-the-site.md b/frappe/docs/user/fr/tutorial/setting-up-the-site.md index 11b7aad611..2637bde1d4 100644 --- a/frappe/docs/user/fr/tutorial/setting-up-the-site.md +++ b/frappe/docs/user/fr/tutorial/setting-up-the-site.md @@ -21,7 +21,7 @@ Vous pouvez installer un nouveau site avec la commande `bench new-site library` Cette commande va créer une nouvelle base de données, un repertoire et installer `frappe` (qui est aussi une application!) dans le nouveau site. L'application `frappe` a deux modules par défaut, **Core** et **Website**. Le module **Core** -contient les modèles basiques pour l'application. En effet, Frappe contient des modèles par défaut qui sont appelés **DocTypes** +contient les modèles basiques pour l'application. En effet, Frappé contient des modèles par défaut qui sont appelés **DocTypes** mais nous en reparlerons plus tard. $ bench new-site library diff --git a/frappe/docs/user/fr/tutorial/single-doctypes.md b/frappe/docs/user/fr/tutorial/single-doctypes.md index e3b10a0523..8f06d09b02 100644 --- a/frappe/docs/user/fr/tutorial/single-doctypes.md +++ b/frappe/docs/user/fr/tutorial/single-doctypes.md @@ -1,12 +1,12 @@ # Les DocTypes de type Single Une application aura en générale une seule page de configuration. Dans notre application nous pouvons donc définir une page -ou nous définierons la période prêt. Nous avons aussi besoin de sauvegarder cette propriété. Dans Frappe, ceci est possible +ou nous définierons la période prêt. Nous avons aussi besoin de sauvegarder cette propriété. Dans Frappé, ceci est possible en utilisant un un Doctype de type **Single**. Un DocType **Single** est comme le pattern Singleton, une instance unique d'une classe. Appelons le **Library Managment Settings**. Pour créer un DocType de type **Single** cochez la case **Is Single**. -Single Doctypes +Single Doctypes {suite} diff --git a/frappe/docs/user/fr/tutorial/start.md b/frappe/docs/user/fr/tutorial/start.md index 608127c0d5..b5ba0d1e73 100644 --- a/frappe/docs/user/fr/tutorial/start.md +++ b/frappe/docs/user/fr/tutorial/start.md @@ -14,7 +14,7 @@ Pour démarrer le serveur de développement, lancez la commande `bench start` Vous pouvez maintenant ouvrir votre navigateur et vous rendre sur `http://localhost:8000`. Si tout se passe bien vous devriez voir: -Login Screen +Login Screen Maintenant, connectez vous avec les identifiants suivants: @@ -24,9 +24,9 @@ Mot de passe: **Le mot de passe que vous avez définis pendant l'installation** Une fois connecté, vous devriez voir le `Desk`, c'est à dire la page d'accueil -Desk +Desk -Comme vous pouvez le voir, Frappe fournit quelques applications comme un To Do, un gestionnaire de fichiers etc. Ces applications +Comme vous pouvez le voir, Frappé fournit quelques applications comme un To Do, un gestionnaire de fichiers etc. Ces applications peuvent être intégrées par la suite. {next} diff --git a/frappe/docs/user/fr/tutorial/task-runner.md b/frappe/docs/user/fr/tutorial/task-runner.md index b455fb6af9..74d39f7de8 100644 --- a/frappe/docs/user/fr/tutorial/task-runner.md +++ b/frappe/docs/user/fr/tutorial/task-runner.md @@ -1,6 +1,6 @@ # Les tâches planifiées -Finalement, une application a aussi à envoyer des emails de notifications ou d'autres taches planifiées. Dans Frappe, si +Finalement, une application a aussi à envoyer des emails de notifications ou d'autres taches planifiées. Dans Frappé, si vous avez configuré le **bench**, la tâche / planificateur est configuré via Celery en utilisant les queues Redis. Pour ajouter un nouveau gestionnaire de tâches, ouvrez le fichier `hooks.py` et ajoutez un nouveau gestionnaire. Les gestionnaires @@ -18,7 +18,7 @@ Pour ajouter un nouveau gestionnaire de tâches, ouvrez le fichier `hooks.py` et Ici, nous pointons sur une fonction en Python et cette fonction sera appelée tous les jours. Voyons à quoi cette fonction ressemble: - # Copyright (c) 2013, Frappe + # Copyright (c) 2013, Frappé # For license information, please see license.txt from __future__ import unicode_literals diff --git a/frappe/docs/user/fr/tutorial/users-and-records.md b/frappe/docs/user/fr/tutorial/users-and-records.md index e0a714c9c4..a46ebf0fbb 100644 --- a/frappe/docs/user/fr/tutorial/users-and-records.md +++ b/frappe/docs/user/fr/tutorial/users-and-records.md @@ -1,7 +1,7 @@ # Créer des utilisateurs et des enregistrements Maintenant que nous avons définis des modèles, nous pouvons créér des enregistrements directement depuis l'interface. Vous -n'avez pas à créer des vues ! Les vues dans Frappe sont automatiquements créées à partir des propriétés de vos **DocTypes**. +n'avez pas à créer des vues ! Les vues dans Frappé sont automatiquements créées à partir des propriétés de vos **DocTypes**. ### 4.1 Créer des utilisateurs @@ -12,7 +12,7 @@ Afin de créer des enregistrements, nous avons tout d'abord besoin de créer un Saisissez un nom, un prénom ainsi qu'un mot de passe à votre utilisateur pour le créer et donnez lui les rôles `Librarian` et `Library Member`. -Add User Roles +Add User Roles Maintenant déconnectez-vous puis connectez-vous avec l'utilisateur que vous venez de créer. @@ -21,39 +21,39 @@ Maintenant déconnectez-vous puis connectez-vous avec l'utilisateur que vous ven Vous allez désormais voir une icone pour notre module de gestion de librairie. Cliquez sur cette icone et vous apercevrez la page du module: -Library Management Module +Library Management Module Vous pouvez donc voir les **DocTypes** que nous avons créés pour l'application. Créons quelques enregistrements. Définissons un nouvel Article: -New Article +New Article Le **DocType** que vous avons définis est transformé en formulaire. Les règles de validation seront appliquées selon nos définitions. Remplissons le formulaire pour créer notre premier article. -New Article +New Article Vous pouvez aussi ajouter une image. -Attach Image +Attach Image Maintenant créons un nouveau membre. -New Library Member +New Library Member Après cela, définissons un nouvel abonnement pour ce membre. Ici, si vous vous souvenez, nous avons définis que les noms et prénoms doivent automatiquement être renseignés dès que nous avons selectionné l'ID du membre. -New Library Membership +New Library Membership Comme vous pouvez le voir, la date est formattée en années-mois-jour qui est le format du système. Pour configurer / changer le format de la date et de l'heure, rendez-vous sur: > Setup > Settings > System Settings -System Settings +System Settings {suite} diff --git a/frappe/docs/user/fr/tutorial/web-views.md b/frappe/docs/user/fr/tutorial/web-views.md index 9b0f80a0e9..de7c0a2318 100644 --- a/frappe/docs/user/fr/tutorial/web-views.md +++ b/frappe/docs/user/fr/tutorial/web-views.md @@ -1,11 +1,11 @@ # Les vues web -Frappe a deux principaux environnements, le **bureau** et **le web**. Le **bureau** est un environnement riche AJAX alors +Frappé a deux principaux environnements, le **bureau** et **le web**. Le **bureau** est un environnement riche AJAX alors que **le web** est une collection plus traditionnelle de fichiers HTML pour la consultation publique. Les vues web peuvent aussi être générées pour créer des vues plus controllées pour les utilisateurs qui peuvent se connecter mais qui n'ont pas accès au desk. -Dans Frappe, les vues sont gérées par des templates et sont tout naturellement placés dans le repertoire `templates`. Il +Dans Frappé, les vues sont gérées par des templates et sont tout naturellement placés dans le repertoire `templates`. Il y a 2 principaux types de templates. 1. Pages: Ce sont des templates Jinja ou une vue unique existe pour une route (exemple:`/blog`). @@ -20,11 +20,11 @@ Jettons un oeil aux vues standards: Si vous êtes connecté avec votre utilisateur de test, rendez-vous sur`/article` et vous devriez voir la liste des articles: -web list +web list Cliquez sur un article et vous devriez voir une vue par défaut. -web view +web view Maintenant, si vous voulez une meilleur liste pour vos articles, créez un fichier appelé `row_template.html` dans le repertoire `library_management/templates/includes/list/`. Voici un exemple du contenu de ce fichier: @@ -50,11 +50,11 @@ Ici, vous aurez toutes les propriétés d'un article dans l'object `doc`. La mise à jour de la liste ressemble à ca ! -new web list +new web list #### La page d'accueil -Frappe permet l'inscription et inclut les inscriptions via Google, Facebook et Github. Quand un utilisateur s'inscrit via +Frappé permet l'inscription et inclut les inscriptions via Google, Facebook et Github. Quand un utilisateur s'inscrit via le web, il n'a pas accès à l'interface du desk par defaut. > Pour autoriser les utilisateurs à accéder au `Desk`, ouvrez la configuration de l'utilisateur (Setup > User) et définissez diff --git a/frappe/docs/user/index.md b/frappe/docs/user/index.md index 4091cec362..91ceae5836 100644 --- a/frappe/docs/user/index.md +++ b/frappe/docs/user/index.md @@ -1,8 +1,8 @@ -# Frappe Developer Tutorial +# Frappé Developer Tutorial Select your language -1. [English]({{docs_base_url}}/user/en) -1. [Français]({{docs_base_url}}/user/fr) -1. [Português]({{docs_base_url}}/user/pt) -1. [Español]({{docs_base_url}}/user/es) \ No newline at end of file +1. [English](/docs/user/en) +1. [Français](/docs/user/fr) +1. [Português](/docs/user/pt) +1. [Español](/docs/user/es) \ No newline at end of file diff --git a/frappe/docs/user/pt/index.md b/frappe/docs/user/pt/index.md index a25785e59e..a68da6210b 100755 --- a/frappe/docs/user/pt/index.md +++ b/frappe/docs/user/pt/index.md @@ -1,3 +1,3 @@ -# Develop Apps with Frappe +# Develop Apps with Frappé {index} diff --git a/frappe/docs/user/pt/tutorial/app.md b/frappe/docs/user/pt/tutorial/app.md index dd1a2597e3..3d0d327db1 100755 --- a/frappe/docs/user/pt/tutorial/app.md +++ b/frappe/docs/user/pt/tutorial/app.md @@ -1,9 +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. +Uma Aplicação em Frappé é apenas uma aplicação padrão em Python. Você pode estruturar uma aplicação Frappé da mesma forma que estrutura uma aplicação padrão do Python. Para fazer o deploy, Frappé 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. +Frappé 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. +Frappé 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 index 31f407a7d2..6d8cba9a12 100755 --- a/frappe/docs/user/pt/tutorial/before.md +++ b/frappe/docs/user/pt/tutorial/before.md @@ -1,6 +1,6 @@ # Antes de começar -

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

    +

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

    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 @@ -8,7 +8,7 @@ Há uma série de bons tutoriais on-line e encontramos [Codecademy] (http://www. #### 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 +O lado do servidor do Frappé é 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 Frappé. 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 Frappé 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). @@ -44,4 +44,4 @@ Se estiver personalizando modelos de impressão, você precisa aprender a [líng --- -Quando estiver pronto, [tente construir uma aplicação de exemplo em Frappe]({{docs_base_url}}/user/pt/tutorial/app) +Quando estiver pronto, [tente construir uma aplicação de exemplo em Frappé](/docs/user/pt/tutorial/app) diff --git a/frappe/docs/user/pt/tutorial/bench.md b/frappe/docs/user/pt/tutorial/bench.md index 5b764d7a8e..0402dbe4c0 100755 --- a/frappe/docs/user/pt/tutorial/bench.md +++ b/frappe/docs/user/pt/tutorial/bench.md @@ -1,10 +1,10 @@ -# Instalando o Frappe Bench +# Instalando o Frappé 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. +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 Frappé 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). +Com Frappé 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 index 8163004eeb..a3b62d9a0d 100755 --- a/frappe/docs/user/pt/tutorial/conclusion.md +++ b/frappe/docs/user/pt/tutorial/conclusion.md @@ -1,6 +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. +Esperamos que este tutorial lhe de uma visão geral de como as aplicações são desenvolvidas em Frappé. 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 index cf657e9d0d..85fc8fd1f6 100755 --- a/frappe/docs/user/pt/tutorial/controllers.md +++ b/frappe/docs/user/pt/tutorial/controllers.md @@ -48,7 +48,7 @@ Nesse script: Verifique se suas validações funcionaram, criando de novos registros. -Transaction +Transaction #### Debugging diff --git a/frappe/docs/user/pt/tutorial/doctypes.md b/frappe/docs/user/pt/tutorial/doctypes.md index fa6c7a26ee..5be3e534d4 100755 --- a/frappe/docs/user/pt/tutorial/doctypes.md +++ b/frappe/docs/user/pt/tutorial/doctypes.md @@ -6,7 +6,7 @@ Para criar um novo **DocType**, vá para: > Developer > Documents > Doctype > New -New Doctype +New Doctype No DocType, criamos o módulo, que no nosso caso é **Library Managment** @@ -25,7 +25,7 @@ Os campos são muito mais do que colunas de banco de dados, eles podem ser: Vamos adicionar os campos do artigo. -Adding Fields +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. @@ -45,9 +45,9 @@ Nós podemos adicionar os seguintes campos: #### 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**. +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**. Frappé 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 +Adding Permissions #### Salvando diff --git a/frappe/docs/user/pt/tutorial/form-client-scripting.md b/frappe/docs/user/pt/tutorial/form-client-scripting.md index c37196757f..faf453c57a 100755 --- a/frappe/docs/user/pt/tutorial/form-client-scripting.md +++ b/frappe/docs/user/pt/tutorial/form-client-scripting.md @@ -1,3 +1,5 @@ +# Form Client Scripting + ## 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. diff --git a/frappe/docs/user/pt/tutorial/index.md b/frappe/docs/user/pt/tutorial/index.md index 56f5817fc5..fad825afcf 100755 --- a/frappe/docs/user/pt/tutorial/index.md +++ b/frappe/docs/user/pt/tutorial/index.md @@ -1,6 +1,6 @@ -# Frappe Tutorial +# Frappé 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: +In this guide we will show you how to create an application from scratch using **Frappé**. Using the example of a Library Management System, we will cover: 1. Installation 1. Making a New App @@ -12,9 +12,9 @@ In this guide we will show you how to create an application from scratch using * ## 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. +This guide is intended for software developers who are familiar with how the web applications are built and served. Frappé 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. +Frappé 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 @@ -26,7 +26,7 @@ For this guide book, we will build a simple **Library Management** application. 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. +The user interface (UI) for the librarian will be the **Frappé 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. diff --git a/frappe/docs/user/pt/tutorial/models.md b/frappe/docs/user/pt/tutorial/models.md index f31c5c9864..4b0d7ac9f0 100755 --- a/frappe/docs/user/pt/tutorial/models.md +++ b/frappe/docs/user/pt/tutorial/models.md @@ -1,6 +1,6 @@ # 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**. +O próximo passo é criar os modelos como discutimos na introdução. Em Frappé, 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]`. diff --git a/frappe/docs/user/pt/tutorial/naming-and-linking.md b/frappe/docs/user/pt/tutorial/naming-and-linking.md index 1d72cd55c3..b8a6dbe952 100755 --- a/frappe/docs/user/pt/tutorial/naming-and-linking.md +++ b/frappe/docs/user/pt/tutorial/naming-and-linking.md @@ -4,7 +4,7 @@ Em seguida, vamos criar outro DocType e salva-lo também: 1. Library Member (First Name, Last Name, Email Address, Phone, Address) -Doctype Saved +Doctype Saved #### Nomeação de DocTypes @@ -20,21 +20,21 @@ Isso pode ser definido através do preenchimento do campo **Autoname**. Para o c > **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 +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. +As chaves estrangeiras são especificados no Frappé 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 +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 +Select Field Fazer o mesmo para outros modelos. @@ -44,7 +44,7 @@ Um modelo padrão é quando você seleciona um ID, **Library Member** na **Libra 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 +Fetch values ### Complete os modelos @@ -52,19 +52,19 @@ Da mesma forma, você pode completar todos os modelos de modo que os campos fina #### Article -Article +Article #### Library Member -Library Member +Library Member #### Library Membership -Library Membership +Library Membership #### Library Transaction -Library Transaction +Library Transaction > Lembre-se de dar permissões para **Librarian** em cada DocType diff --git a/frappe/docs/user/pt/tutorial/new-app.md b/frappe/docs/user/pt/tutorial/new-app.md index 1390f60766..56027e640a 100755 --- a/frappe/docs/user/pt/tutorial/new-app.md +++ b/frappe/docs/user/pt/tutorial/new-app.md @@ -7,7 +7,7 @@ Para criar uma nova aplicação, vá para a pasta do bench e execute, `bench new $ 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 Publisher: Frappé App Email: info@frappe.io App Icon (default 'octicon octicon-file-directory'): octicon octicon-book App Color (default 'grey'): #589494 @@ -44,11 +44,11 @@ O aplicativo será criado em uma pasta chamada `library_management` e terá a se 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. `library_management` (Interior) é um **módulo** que foi criado. Em Frappé, 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. `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 Frappé, 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 index 1889705874..c7a241bbc4 100755 --- a/frappe/docs/user/pt/tutorial/reports.md +++ b/frappe/docs/user/pt/tutorial/reports.md @@ -2,6 +2,6 @@ Você também pode clicar sobre o texto Relatórios na barra lateral (esquerda) para ver os registros tabulados -Report +Report {next} diff --git a/frappe/docs/user/pt/tutorial/roles.md b/frappe/docs/user/pt/tutorial/roles.md index 286d39afa7..07b687f936 100755 --- a/frappe/docs/user/pt/tutorial/roles.md +++ b/frappe/docs/user/pt/tutorial/roles.md @@ -9,6 +9,6 @@ Para criar um novo roles, vá para: > Setup > Users > Role > New -Adding Roles +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 index 90f499c9c7..e6fa9f40a4 100755 --- a/frappe/docs/user/pt/tutorial/setting-up-the-site.md +++ b/frappe/docs/user/pt/tutorial/setting-up-the-site.md @@ -19,7 +19,7 @@ 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**. +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. Frappé é 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: diff --git a/frappe/docs/user/pt/tutorial/single-doctypes.md b/frappe/docs/user/pt/tutorial/single-doctypes.md index c21115244e..41f218e2b5 100755 --- a/frappe/docs/user/pt/tutorial/single-doctypes.md +++ b/frappe/docs/user/pt/tutorial/single-doctypes.md @@ -1,9 +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**. +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 Frappé, 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 +Single Doctypes {next} diff --git a/frappe/docs/user/pt/tutorial/start.md b/frappe/docs/user/pt/tutorial/start.md index c4ed5e5f3c..80504b58f8 100755 --- a/frappe/docs/user/pt/tutorial/start.md +++ b/frappe/docs/user/pt/tutorial/start.md @@ -14,7 +14,7 @@ Para iniciar o servidor de desenvolvimento, digite `bench start` 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 +Login Screen Agora logue com : @@ -24,8 +24,8 @@ Senha : **Use a senha que foi criada durante a instalação** Quando voce logar, voce deverá ver o "Desk" da pagine home -Desk +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. +Como você pode ver, o básico do sistema Frappé 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 index c0f696410f..9d514eb151 100755 --- a/frappe/docs/user/pt/tutorial/task-runner.md +++ b/frappe/docs/user/pt/tutorial/task-runner.md @@ -1,6 +1,6 @@ # 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. +Finalmente, uma aplicação também tem que enviar notificações de e-mail e fazer outros tipos de tarefas agendadas. Em Frappé, 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. @@ -15,7 +15,7 @@ Para adicionar um novo task handler, vá para `hooks.py` e adicione um novo hand 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 + # Copyright (c) 2013, Frappé # For license information, please see license.txt from __future__ import unicode_literals diff --git a/frappe/docs/user/pt/tutorial/users-and-records.md b/frappe/docs/user/pt/tutorial/users-and-records.md index 878253bf0b..f581f13e59 100755 --- a/frappe/docs/user/pt/tutorial/users-and-records.md +++ b/frappe/docs/user/pt/tutorial/users-and-records.md @@ -1,6 +1,6 @@ # 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. +Agora que já criamos os modelos, podemos começar diretamente criando registros usando a interface Desk do Frappé. Você não precisa criar Views! Views no Frappé são geradas automaticamente com base nas propriedades do DocType. ### 4.1 Criando Usuarios @@ -12,7 +12,7 @@ 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 +Add User Roles Agora saia e se autentique usando o novo ID de usuário e senha. @@ -20,36 +20,36 @@ Agora saia e se autentique usando o novo ID de usuário e senha. 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 +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 +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 +New Article Você também pode adicionar uma imagem. -Attach Image +Attach Image Agora vamos criar um novo membro: -New Library Member +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 +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 +System Settings {next} diff --git a/frappe/docs/user/pt/tutorial/web-views.md b/frappe/docs/user/pt/tutorial/web-views.md index de7d5b93c9..ec1be924b6 100755 --- a/frappe/docs/user/pt/tutorial/web-views.md +++ b/frappe/docs/user/pt/tutorial/web-views.md @@ -1,8 +1,8 @@ # 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. +Frappé 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. +Em Frappé, 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. @@ -16,11 +16,11 @@ 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 +web list Clique em um artigo e você vai ver uma Web View padrão -web view +web view Agora, se você quiser fazer uma List View melhor para o artigo, crie um arquivo chamado `row_template.html` na pasta `library_management/templates/includes/list/`. Aqui está um exemplo de arquivo: @@ -46,11 +46,11 @@ Aqui, você vai ter todas as propriedades do artigo no objeto `doc`. A List View atualizada se parece com isso! -new web list +new web list #### 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. +Frappé 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" diff --git a/frappe/email/doctype/email_alert/email_alert.py b/frappe/email/doctype/email_alert/email_alert.py index 6aecac38c1..f4bce6f5cd 100755 --- a/frappe/email/doctype/email_alert/email_alert.py +++ b/frappe/email/doctype/email_alert/email_alert.py @@ -242,7 +242,7 @@ def evaluate_alert(doc, alert, event): alert.send(doc) except TemplateError: frappe.throw(_("Error while evaluating Email Alert {0}. Please fix your template.").format(alert)) - except Exception, e: + except Exception as e: frappe.log_error(message=frappe.get_traceback(), title=e) frappe.throw(_("Error in Email Alert")) diff --git a/frappe/email/doctype/newsletter/newsletter.py b/frappe/email/doctype/newsletter/newsletter.py index 29ecbee853..4f4713edbe 100755 --- a/frappe/email/doctype/newsletter/newsletter.py +++ b/frappe/email/doctype/newsletter/newsletter.py @@ -13,7 +13,6 @@ from frappe.utils.background_jobs import enqueue from frappe.utils.scheduler import log from frappe.email.queue import send from frappe.email.doctype.email_group.email_group import add_subscribers -from frappe.utils.file_manager import get_file from frappe.utils import parse_addr @@ -68,7 +67,7 @@ class Newsletter(Document): files = frappe.get_all("File", fields = ["name"], filters = {"attached_to_doctype": "Newsletter", "attached_to_name":self.name}, order_by="creation desc") - for file in files: + for a in files: try: # these attachments will be attached on-demand # and won't be stored in the message diff --git a/frappe/email/doctype/newsletter/test_newsletter.py b/frappe/email/doctype/newsletter/test_newsletter.py index a198037dc0..2b13799775 100644 --- a/frappe/email/doctype/newsletter/test_newsletter.py +++ b/frappe/email/doctype/newsletter/test_newsletter.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import frappe, unittest from frappe.email.doctype.newsletter.newsletter import confirmed_unsubscribe -from urllib import unquote +from six.moves.urllib.parse import unquote emails = ["test_subscriber1@example.com", "test_subscriber2@example.com", "test_subscriber3@example.com"] diff --git a/frappe/email/email_body.py b/frappe/email/email_body.py index 7f97a34b4d..a94163ed20 100755 --- a/frappe/email/email_body.py +++ b/frappe/email/email_body.py @@ -8,7 +8,7 @@ from frappe.email.smtp import get_outgoing_email_account from frappe.utils import (get_url, scrub_urls, strip, expand_relative_urls, cint, split_emails, to_markdown, markdown, encode, random_string, parse_addr) import email.utils -from six import iteritems +from six import iteritems, text_type from email.mime.multipart import MIMEMultipart @@ -233,7 +233,8 @@ class EMail: self.make() return self.msg_root.as_string() -def get_formatted_html(subject, message, footer=None, print_html=None, email_account=None, header=None): +def get_formatted_html(subject, message, footer=None, print_html=None, + email_account=None, header=None, unsubscribe_link=None): if not email_account: email_account = get_outgoing_email_account(False) @@ -247,9 +248,23 @@ def get_formatted_html(subject, message, footer=None, print_html=None, email_acc "subject": subject }) - sanitized_html = scrub_urls(rendered_email) - transformed_html = inline_style_in_html(sanitized_html) - return transformed_html + html = scrub_urls(rendered_email) + + if unsubscribe_link: + html = html.replace("", unsubscribe_link.html) + + html = inline_style_in_html(html) + return html + +@frappe.whitelist() +def get_email_html(template, args, subject, header=None): + import json + + args = json.loads(args) + if header and header.startswith('['): + header = json.loads(header) + email = frappe.utils.jinja.get_email_from_template(template, args) + return get_formatted_html(subject, email[0], header=header) def inline_style_in_html(html): ''' Convert email.css and html to inline-styled html @@ -292,7 +307,7 @@ def add_attachment(fname, fcontent, content_type=None, maintype, subtype = content_type.split('/', 1) if maintype == 'text': # Note: we should handle calculating the charset - if isinstance(fcontent, unicode): + if isinstance(fcontent, text_type): fcontent = fcontent.encode("utf-8") part = MIMEText(fcontent, _subtype=subtype, _charset="utf-8") elif maintype == 'image': @@ -331,25 +346,20 @@ def get_footer(email_account, footer=None): """append a footer (signature)""" footer = footer or "" - if email_account and email_account.footer: - footer += '
    {0}
    '.format(email_account.footer) + args = {} - footer += "" + if email_account and email_account.footer: + args.update({'email_account_footer': email_account.footer}) company_address = frappe.db.get_default("email_footer_address") if company_address: - company_address = company_address.splitlines(True) - footer += '' - footer += '' - for x in company_address: - footer += ''\ - .format(x) - footer += "
    {0}
    " + args.update({'company_address': company_address}) if not cint(frappe.db.get_default("disable_standard_email_footer")): - for default_mail_footer in frappe.get_hooks("default_mail_footer"): - footer += '
    {0}
    '.format(default_mail_footer) + args.update({'default_mail_footer': frappe.get_hooks('default_mail_footer')}) + + footer += frappe.utils.jinja.get_email_from_template('email_footer', args)[0] return footer diff --git a/frappe/email/queue.py b/frappe/email/queue.py index e48dbb9115..e3e2267f6e 100755 --- a/frappe/email/queue.py +++ b/frappe/email/queue.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals from six.moves import range import frappe -import HTMLParser +from six.moves import html_parser as HTMLParser import smtplib, quopri, json from frappe import msgprint, throw, _ from frappe.email.smtp import SMTPServer, get_outgoing_email_account @@ -15,6 +15,7 @@ from frappe.utils import get_url, nowdate, encode, now_datetime, add_days, split from frappe.utils.file_manager import get_file from rq.timeouts import JobTimeoutException from frappe.utils.scheduler import log +from six import text_type class EmailLimitCrossedError(frappe.ValidationError): pass @@ -75,8 +76,6 @@ def send(recipients=None, sender=None, subject=None, message=None, text_content= except HTMLParser.HTMLParseError: text_content = "See html attachment" - formatted = get_formatted_html(subject, message, email_account=email_account, header=header) - if reference_doctype and reference_name: unsubscribed = [d.email for d in frappe.db.get_all("Email Unsubscribe", "email", {"reference_doctype": reference_doctype, "reference_name": reference_name})] @@ -88,14 +87,22 @@ def send(recipients=None, sender=None, subject=None, message=None, text_content= recipients = [r for r in list(set(recipients)) if r and r not in unsubscribed] - email_content = formatted email_text_context = text_content - if add_unsubscribe_link and reference_doctype and (unsubscribe_message or reference_doctype=="Newsletter") and add_unsubscribe_link==1: + should_append_unsubscribe = (add_unsubscribe_link + and reference_doctype + and (unsubscribe_message or reference_doctype=="Newsletter") + and add_unsubscribe_link==1) + + unsubscribe_link = None + if should_append_unsubscribe or True: unsubscribe_link = get_unsubscribe_message(unsubscribe_message, expose_recipients) - email_content = email_content.replace("", unsubscribe_link.html) email_text_context += unsubscribe_link.text + email_content = get_formatted_html(subject, message, + email_account=email_account, header=header, + unsubscribe_link=unsubscribe_link) + # add to queue add(recipients, sender, subject, formatted=email_content, @@ -230,17 +237,21 @@ def get_emails_sent_this_month(): status='Sent' and MONTH(creation)=MONTH(CURDATE())""")[0][0] def get_unsubscribe_message(unsubscribe_message, expose_recipients): - if not unsubscribe_message: - unsubscribe_message = _("Unsubscribe from this list") + if unsubscribe_message: + unsubscribe_html = '''{0}'''.format(unsubscribe_message) + else: + unsubscribe_link = '''{0}'''.format(_('Unsubscribe')) + unsubscribe_html = _("{0} to stop receiving emails of this type").format(unsubscribe_link) - html = """
    + html = """""".format(unsubscribe_message=unsubscribe_message) +
    + {0} +
    +
    """.format(unsubscribe_html) + if expose_recipients == "footer": text = "\n" else: @@ -430,10 +441,10 @@ def send_one(email, smtpserver=None, auto_commit=True, now=False, from_test=Fals if any("Sent" == s.status for s in recipients_list): frappe.db.sql("""update `tabEmail Queue` set status='Partially Errored', error=%s where name=%s""", - (unicode(e), email.name), auto_commit=auto_commit) + (text_type(e), email.name), auto_commit=auto_commit) else: frappe.db.sql("""update `tabEmail Queue` set status='Error', error=%s -where name=%s""", (unicode(e), email.name), auto_commit=auto_commit) +where name=%s""", (text_type(e), email.name), auto_commit=auto_commit) if email.communication: frappe.get_doc('Communication', email.communication).set_delivery_status(commit=auto_commit) @@ -444,7 +455,7 @@ where name=%s""", (unicode(e), email.name), auto_commit=auto_commit) else: # log to Error Log - log('frappe.email.queue.flush', unicode(e)) + log('frappe.email.queue.flush', text_type(e)) def prepare_message(email, recipient, recipients_list): message = email.message diff --git a/frappe/email/receive.py b/frappe/email/receive.py index a36e120a0b..c4ce928f85 100644 --- a/frappe/email/receive.py +++ b/frappe/email/receive.py @@ -3,7 +3,7 @@ from __future__ import unicode_literals -from six import iteritems +from six import iteritems, text_type from six.moves import range import time, _socket, poplib, imaplib, email, email.utils, datetime, chardet, re, hashlib from email_reply_parser import EmailReplyParser @@ -482,7 +482,7 @@ class Email: charset = self.get_charset(part) try: - return unicode(part.get_payload(decode=True), str(charset), "ignore") + return text_type(part.get_payload(decode=True), str(charset), "ignore") except LookupError: return part.get_payload() diff --git a/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py index 8d37745073..3ceae47a0e 100644 --- a/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py +++ b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py @@ -10,7 +10,7 @@ from frappe.model.document import Document import dropbox, json from frappe.utils.backups import new_backup from frappe.utils.background_jobs import enqueue -from urlparse import urlparse, parse_qs +from six.moves.urllib.parse import urlparse, parse_qs from frappe.integrations.utils import make_post_request from frappe.utils import (cint, split_emails, get_request_site_address, cstr, get_files_path, get_backups_path, encode, get_url) diff --git a/frappe/integrations/doctype/gsuite_settings/gsuite_settings.py b/frappe/integrations/doctype/gsuite_settings/gsuite_settings.py index 9b6be3422d..68b8450da7 100644 --- a/frappe/integrations/doctype/gsuite_settings/gsuite_settings.py +++ b/frappe/integrations/doctype/gsuite_settings/gsuite_settings.py @@ -17,7 +17,7 @@ class GSuiteSettings(Document): def get_access_token(self): if not self.refresh_token: - raise UserError(_("Google GSuite is not configured.")) + raise frappe.ValidationError(_("Google GSuite is not configured.")) data = { 'client_id': self.client_id, 'client_secret': self.get_password(fieldname='client_secret',raise_exception=False), @@ -51,7 +51,7 @@ def gsuite_callback(code=None): frappe.db.set_value("Gsuite Settings", None, "refresh_token", r['refresh_token']) frappe.db.commit() return - except Exception, e: + except Exception as e: frappe.throw(e.message) def run_gsuite_script(option, filename = None, template_id = None, destination_id = None, json_data = None): @@ -68,7 +68,7 @@ def run_gsuite_script(option, filename = None, template_id = None, destination_i try: r = requests.post(gdoc.script_url, headers=headers, data=dumps(data, default=json_handler, separators=(',',':'))) - except Exception, e: + except Exception as e: frappe.throw(e.message) try: diff --git a/frappe/integrations/doctype/oauth_bearer_token/oauth_bearer_token.py b/frappe/integrations/doctype/oauth_bearer_token/oauth_bearer_token.py index 1c7e921140..09fd29075b 100644 --- a/frappe/integrations/doctype/oauth_bearer_token/oauth_bearer_token.py +++ b/frappe/integrations/doctype/oauth_bearer_token/oauth_bearer_token.py @@ -9,5 +9,5 @@ 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) + 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/integrations/doctype/social_login_keys/social_login_keys.py b/frappe/integrations/doctype/social_login_keys/social_login_keys.py index 693bce679b..33c8ab2560 100644 --- a/frappe/integrations/doctype/social_login_keys/social_login_keys.py +++ b/frappe/integrations/doctype/social_login_keys/social_login_keys.py @@ -10,11 +10,7 @@ import socket from frappe.model.document import Document from frappe import _ - -try: - from urllib.parse import urlparse -except ImportError: - from urlparse import urlparse +from six.moves.urllib.parse import urlparse class SocialLoginKeys(Document): def validate(self): diff --git a/frappe/integrations/oauth2.py b/frappe/integrations/oauth2.py index c59e989dfe..6889633c9b 100644 --- a/frappe/integrations/oauth2.py +++ b/frappe/integrations/oauth2.py @@ -2,9 +2,10 @@ 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 urllib import urlencode +from six.moves.urllib.parse import quote from werkzeug import url_fix -from urlparse import urlparse +from six.moves.urllib.parse import urlparse from frappe.integrations.doctype.oauth_provider_settings.oauth_provider_settings import get_oauth_settings from frappe import _ @@ -178,7 +179,6 @@ def openid_profile(*args, **kwargs): frappe.local.response = user_profile def validate_url(url_string): - from urlparse import urlparse try: result = urlparse(url_string) if result.scheme and result.scheme in ["http", "https", "ftp", "ftps"]: diff --git a/frappe/integrations/utils.py b/frappe/integrations/utils.py index 4d6d6c0ec4..8215eefd7b 100644 --- a/frappe/integrations/utils.py +++ b/frappe/integrations/utils.py @@ -4,7 +4,8 @@ from __future__ import unicode_literals import frappe -import json, urlparse +import json +from six.moves.urllib.parse import parse_qs from frappe.utils import get_request_session from frappe import _ @@ -40,7 +41,7 @@ def make_post_request(url, auth=None, headers=None, data=None): frappe.flags.integration_request.raise_for_status() if frappe.flags.integration_request.headers.get("content-type") == "text/plain; charset=utf-8": - return urlparse.parse_qs(frappe.flags.integration_request.text) + return parse_qs(frappe.flags.integration_request.text) return frappe.flags.integration_request.json() except Exception as exc: diff --git a/frappe/limits.py b/frappe/limits.py index 84b90f057c..0f597e445f 100755 --- a/frappe/limits.py +++ b/frappe/limits.py @@ -5,7 +5,8 @@ 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, disable_users -import os, subprocess, urlparse, urllib +import os, subprocess, urllib +from six.moves.urllib.parse import parse_qsl, urlsplit, urlunsplit class SiteExpiredError(frappe.ValidationError): http_status_code = 417 @@ -121,8 +122,8 @@ def get_usage_info(): return usage_info def get_upgrade_url(upgrade_url): - parts = urlparse.urlsplit(upgrade_url) - params = dict(urlparse.parse_qsl(parts.query)) + parts = urlsplit(upgrade_url) + params = dict(parse_qsl(parts.query)) params.update({ 'site': frappe.local.site, 'email': frappe.session.user, @@ -131,7 +132,7 @@ def get_upgrade_url(upgrade_url): }) query = urllib.urlencode(params, doseq=True) - url = urlparse.urlunsplit((parts.scheme, parts.netloc, parts.path, query, parts.fragment)) + url = urlunsplit((parts.scheme, parts.netloc, parts.path, query, parts.fragment)) return url def get_upgrade_link(upgrade_url, label=None): diff --git a/frappe/model/create_new.py b/frappe/model/create_new.py index cc5c394d9f..846d8101e6 100644 --- a/frappe/model/create_new.py +++ b/frappe/model/create_new.py @@ -11,6 +11,7 @@ from frappe.utils import nowdate, nowtime, now_datetime import frappe.defaults from frappe.model.db_schema import type_map import copy +from frappe.core.doctype.user_permission.user_permission import get_user_permissions def get_new_doc(doctype, parent_doc = None, parentfield = None, as_dict=False): if doctype not in frappe.local.new_doc_templates: @@ -47,7 +48,7 @@ def make_new_doc(doctype): return doc def set_user_and_static_default_values(doc): - user_permissions = frappe.defaults.get_user_permissions() + user_permissions = get_user_permissions() defaults = frappe.defaults.get_defaults() for df in doc.meta.get("fields"): @@ -103,7 +104,7 @@ def get_static_default_value(df, user_permissions): def set_dynamic_default_values(doc, parent_doc, parentfield): # these values should not be cached - user_permissions = frappe.defaults.get_user_permissions() + user_permissions = get_user_permissions() for df in frappe.get_meta(doc["doctype"]).get("fields"): if df.get("default"): diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py index f6cb19d86e..205fd3a1e7 100644 --- a/frappe/model/db_query.py +++ b/frappe/model/db_query.py @@ -388,7 +388,7 @@ class DatabaseQuery(object): # apply user permissions? if role_permissions.get("apply_user_permissions", {}).get("read"): # get user permissions - user_permissions = frappe.defaults.get_user_permissions(self.user) + user_permissions = frappe.permissions.get_user_permissions(self.user) self.add_user_permissions(user_permissions, user_permission_doctypes=role_permissions.get("user_permission_doctypes").get("read")) diff --git a/frappe/model/db_schema.py b/frappe/model/db_schema.py index 01725e12a6..37d644883e 100644 --- a/frappe/model/db_schema.py +++ b/frappe/model/db_schema.py @@ -464,8 +464,8 @@ class DbManager: """ Pass root_conn here for access to all databases. """ - if db: - self.db = db + if db: + self.db = db def get_current_host(self): return self.db.sql("select user()")[0][0].split('@')[1] diff --git a/frappe/model/delete_doc.py b/frappe/model/delete_doc.py index 3bf49a5f6a..e242a48c88 100644 --- a/frappe/model/delete_doc.py +++ b/frappe/model/delete_doc.py @@ -11,7 +11,7 @@ from frappe.utils.file_manager import remove_all from frappe.utils.password import delete_all_passwords_for from frappe import _ from frappe.model.naming import revert_series_if_last -from frappe.utils.global_search import delete_for_document +from frappe.utils.global_search import delete_for_document def delete_doc(doctype=None, name=None, force=0, ignore_doctypes=None, for_reload=False, ignore_permissions=False, flags=None, ignore_on_trash=False): @@ -158,8 +158,13 @@ def update_flags(doc, flags=None, ignore_permissions=False): def check_permission_and_not_submitted(doc): # permission - if not doc.flags.ignore_permissions and frappe.session.user!="Administrator" and (not doc.has_permission("delete") or (doc.doctype=="DocType" and not doc.custom)): - frappe.msgprint(_("User not allowed to delete {0}: {1}").format(doc.doctype, doc.name), raise_exception=True) + if (not doc.flags.ignore_permissions + and frappe.session.user!="Administrator" + and ( + not doc.has_permission("delete") + or (doc.doctype=="DocType" and not doc.custom))): + frappe.msgprint(_("User not allowed to delete {0}: {1}") + .format(doc.doctype, doc.name), raise_exception=frappe.PermissionError) # check if submitted if doc.docstatus == 1: diff --git a/frappe/model/utils/__init__.py b/frappe/model/utils/__init__.py index c795ae2159..ff7383fc19 100644 --- a/frappe/model/utils/__init__.py +++ b/frappe/model/utils/__init__.py @@ -7,6 +7,7 @@ import frappe from frappe.utils import cstr from frappe.build import html_to_js_template import re +from six import text_type """ @@ -51,7 +52,7 @@ def render_include(content): for path in paths: app, app_path = path.split('/', 1) with open(frappe.get_app_path(app, app_path), 'r') as f: - include = unicode(f.read(), 'utf-8') + include = text_type(f.read(), 'utf-8') if path.endswith('.html'): include = html_to_js_template(path, include) diff --git a/frappe/model/utils/link_count.py b/frappe/model/utils/link_count.py index 98004ef744..a53ee84d59 100644 --- a/frappe/model/utils/link_count.py +++ b/frappe/model/utils/link_count.py @@ -44,7 +44,7 @@ def update_link_count(): try: frappe.db.sql('update `tab{0}` set idx = idx + {1} where name=%s'.format(key[0], count), key[1], auto_commit=1) - except Exception, e: + except Exception as e: if e.args[0]!=1146: # table not found, single raise e # reset the count diff --git a/frappe/modules/patch_handler.py b/frappe/modules/patch_handler.py index 715b364257..b212f3e6b1 100644 --- a/frappe/modules/patch_handler.py +++ b/frappe/modules/patch_handler.py @@ -78,7 +78,7 @@ def execute_patch(patchmodule, method=None, methodargs=None): frappe.flags.final_patches.append(patchmodule) else: if patchmodule.startswith("execute:"): - exec patchmodule.split("execute:")[1] in globals() + exec(patchmodule.split("execute:")[1],globals()) else: frappe.get_attr(patchmodule.split()[0] + ".execute")() update_patch_log(patchmodule) diff --git a/frappe/modules/utils.py b/frappe/modules/utils.py index 2acb5b5db6..79933a60a5 100644 --- a/frappe/modules/utils.py +++ b/frappe/modules/utils.py @@ -180,7 +180,7 @@ def load_doctype_module(doctype, module=None, prefix="", suffix=""): try: if key not in doctype_python_modules: doctype_python_modules[key] = frappe.get_module(module_name) - except ImportError, e: + except ImportError as e: raise ImportError('Module import failed for {0} ({1})'.format(doctype, module_name + ' Error: ' + str(e))) return doctype_python_modules[key] @@ -206,6 +206,8 @@ def get_app_publisher(module): 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)) + if template_name.endswith('._py'): + template_name = template_name[:-4] + '.py' target_file_path = os.path.join(target_path, template_name) if not doc: doc = {} diff --git a/frappe/oauth.py b/frappe/oauth.py index 645a68e211..9763cdaa52 100644 --- a/frappe/oauth.py +++ b/frappe/oauth.py @@ -1,9 +1,8 @@ from __future__ import print_function -import frappe, urllib +import frappe import pytz from frappe import _ -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, OpenIDConnectAuthCode from oauthlib.oauth2 import RequestValidator @@ -12,6 +11,7 @@ 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 +from six.moves.urllib.parse import parse_qs, urlparse, unquote def get_url_delimiter(separator_character=" "): return separator_character @@ -134,7 +134,7 @@ class OAuthWebRequestValidator(RequestValidator): oac.scopes = get_url_delimiter().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.user = unquote(cookie_dict['user_id']) oac.authorization_code = code['code'] oac.save(ignore_permissions=True) frappe.db.commit() @@ -159,7 +159,7 @@ class OAuthWebRequestValidator(RequestValidator): except Exception as 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")) + return frappe.session.user == 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') diff --git a/frappe/patches.txt b/frappe/patches.txt index 8adf636701..3347ae8287 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -189,3 +189,4 @@ frappe.patches.v8_1.enable_allow_error_traceback_in_system_settings frappe.patches.v8_1.update_format_options_in_auto_email_report frappe.patches.v8_1.delete_custom_docperm_if_doctype_not_exists frappe.patches.v8_5.delete_email_group_member_with_invalid_emails +frappe.patches.v8_x.update_user_permission diff --git a/frappe/patches/v4_0/file_manager_hooks.py b/frappe/patches/v4_0/file_manager_hooks.py index 2ebcf8ac04..60686b1e0e 100644 --- a/frappe/patches/v4_0/file_manager_hooks.py +++ b/frappe/patches/v4_0/file_manager_hooks.py @@ -1,7 +1,7 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # MIT License. See license.txt -from __future__ import unicode_literals +from __future__ import unicode_literals, print_function import frappe import os @@ -25,7 +25,7 @@ def execute(): _file_name, content = get_file(name) b.content_hash = get_content_hash(content) except IOError: - print 'Warning: Error processing ', name + print('Warning: Error processing ', name) _file_name = old_file_name b.content_hash = None diff --git a/frappe/patches/v8_0/set_currency_field_precision.py b/frappe/patches/v8_0/set_currency_field_precision.py index 51bb1ee05e..8daca7010c 100644 --- a/frappe/patches/v8_0/set_currency_field_precision.py +++ b/frappe/patches/v8_0/set_currency_field_precision.py @@ -6,7 +6,7 @@ import frappe from frappe.utils import get_number_format_info def execute(): - frappe.reload_doc('core', 'doctype', 'system_settings') + frappe.reload_doc('core', 'doctype', 'system_settings', force=True) if not frappe.db.get_value("System Settings", None, "currency_precision"): default_currency = frappe.db.get_default("currency") number_format = frappe.db.get_value("Currency", default_currency, "number_format") \ @@ -15,9 +15,8 @@ def execute(): precision = get_number_format_info(number_format)[2] else: precision = 2 - + ss = frappe.get_doc("System Settings") ss.currency_precision = precision ss.flags.ignore_mandatory = True ss.save() - \ No newline at end of file diff --git a/frappe/patches/v8_x/__init__.py b/frappe/patches/v8_x/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/patches/v8_x/update_user_permission.py b/frappe/patches/v8_x/update_user_permission.py new file mode 100644 index 0000000000..0f8045f48d --- /dev/null +++ b/frappe/patches/v8_x/update_user_permission.py @@ -0,0 +1,27 @@ +import frappe + +def execute(): + frappe.reload_doc('core', 'doctype', 'user_permission') + frappe.delete_doc('core', 'page', 'user-permissions') + for perm in frappe.db.sql(""" + select + name, parent, defkey, defvalue + from + tabDefaultValue + where + parent not in ('__default', '__global') + and + substr(defkey,1,1)!='_' + and + parenttype='User Permission' + """, as_dict=True): + if frappe.db.exists(perm.defkey, perm.defvalue) and frappe.db.exists('User', perm.parent): + frappe.get_doc(dict( + doctype='User Permission', + user=perm.parent, + allow=perm.defkey, + for_value=perm.defvalue, + apply_for_all_roles=0, + )).insert(ignore_permissions = True) + + frappe.db.sql('delete from tabDefaultValue where parenttype="User Permission"') diff --git a/frappe/permissions.py b/frappe/permissions.py index 13da62d366..29f223d08e 100644 --- a/frappe/permissions.py +++ b/frappe/permissions.py @@ -7,7 +7,6 @@ import frappe, copy, json from frappe import _, msgprint from frappe.utils import cint import frappe.share - rights = ("read", "write", "create", "delete", "submit", "cancel", "amend", "print", "email", "report", "import", "export", "set_user_permissions", "share") @@ -25,6 +24,9 @@ def has_permission(doctype, ptype="read", doc=None, verbose=False, user=None): """ if not user: user = frappe.session.user + if verbose: + print('--- Checking for {0} {1} ---'.format(doctype, doc.name if doc else '-')) + if frappe.is_table(doctype): if verbose: print("Table type, always true") return True @@ -40,7 +42,7 @@ def has_permission(doctype, ptype="read", doc=None, verbose=False, user=None): return False if user=="Administrator": - if verbose: print("Administrator") + if verbose: print("Allowing Administrator") return True def false_if_not_shared(): @@ -210,7 +212,10 @@ def get_role_permissions(meta, user=None, verbose=False): if p.user_permission_doctypes: # set user_permission_doctypes in perms - user_permission_doctypes = json.loads(p.user_permission_doctypes) + try: + user_permission_doctypes = json.loads(p.user_permission_doctypes) + except ValueError: + user_permission_doctypes = [] else: user_permission_doctypes = get_linked_doctypes(meta.name) @@ -247,8 +252,12 @@ def get_role_permissions(meta, user=None, verbose=False): return frappe.local.role_permissions[cache_key] +def get_user_permissions(user): + from frappe.core.doctype.user_permission.user_permission import get_user_permissions + return get_user_permissions(user) + def user_has_permission(doc, verbose=True, user=None, user_permission_doctypes=None): - from frappe.defaults import get_user_permissions + from frappe.core.doctype.user_permission.user_permission import get_user_permissions user_permissions = get_user_permissions(user) user_permission_doctypes = get_user_permission_doctypes(user_permission_doctypes, user_permissions) @@ -258,6 +267,10 @@ def user_has_permission(doc, verbose=True, user=None, user_permission_doctypes=N messages = {} + if not user_permission_doctypes: + # no doctypes restricted + end_result = True + # check multiple sets of user_permission_doctypes using OR condition for doctypes in user_permission_doctypes: result = True @@ -309,9 +322,9 @@ def has_controller_permissions(doc, ptype, user=None): def get_doctypes_with_read(): return list(set([p.parent for p in get_valid_perms()])) -def get_valid_perms(doctype=None): +def get_valid_perms(doctype=None, user=None): '''Get valid permissions for the current user from DocPerm and Custom DocPerm''' - roles = get_roles() + roles = get_roles(user) perms = get_perms_for(roles) custom_perms = get_perms_for(roles, 'Custom DocPerm') @@ -360,7 +373,8 @@ def get_roles(user=None, with_standard=True): def get_perms_for(roles, perm_doctype='DocPerm'): '''Get perms for given roles''' - return frappe.db.sql("""select * from `tab{doctype}` where docstatus=0 + return frappe.db.sql(""" + select * from `tab{doctype}` where docstatus=0 and ifnull(permlevel,0)=0 and role in ({roles})""".format(doctype = perm_doctype, roles=", ".join(["%s"]*len(roles))), tuple(roles), as_dict=1) @@ -386,22 +400,28 @@ def set_user_permission_if_allowed(doctype, name, user, with_message=False): if get_role_permissions(frappe.get_meta(doctype), user).set_user_permissions!=1: add_user_permission(doctype, name, user, with_message) -def add_user_permission(doctype, name, user, with_message=False): - '''Add user default''' - if name not in frappe.defaults.get_user_permissions(user).get(doctype, []): +def add_user_permission(doctype, name, user, apply=False): + '''Add user permission''' + from frappe.core.doctype.user_permission.user_permission import get_user_permissions + if name not in get_user_permissions(user).get(doctype, []): if not frappe.db.exists(doctype, name): frappe.throw(_("{0} {1} not found").format(_(doctype), name), frappe.DoesNotExistError) - frappe.defaults.add_default(doctype, name, user, "User Permission") - elif with_message: - msgprint(_("Permission already set")) + frappe.get_doc(dict( + doctype='User Permission', + user=user, + allow=doctype, + for_value=name, + apply_for_all_roles=apply + )).insert() -def remove_user_permission(doctype, name, user, default_value_name=None): - frappe.defaults.clear_default(key=doctype, value=name, parent=user, parenttype="User Permission", - name=default_value_name) +def remove_user_permission(doctype, name, user): + user_permission_name = frappe.db.get_value('User Permission', + dict(user=user, allow=doctype, for_value=name)) + frappe.delete_doc('User Permission', user_permission_name) def clear_user_permissions_for_doctype(doctype): - frappe.defaults.clear_default(parenttype="User Permission", key=doctype) + frappe.cache().delete_value('user_permissions') def can_import(doctype, raise_exception=False): if not ("System Manager" in frappe.get_roles() or has_permission(doctype, "import")): @@ -426,9 +446,10 @@ def apply_user_permissions(doctype, ptype, user=None): def get_user_permission_doctypes(user_permission_doctypes, user_permissions): """returns a list of list like [["User", "Blog Post"], ["User"]]""" - if cint(frappe.db.get_single_value("System Settings", "ignore_user_permissions_if_missing")): + if cint(frappe.get_system_settings('ignore_user_permissions_if_missing')): # select those user permission doctypes for which user permissions exist! - user_permission_doctypes = [list(set(doctypes).intersection(set(user_permissions.keys()))) + user_permission_doctypes = [ + list(set(doctypes).intersection(set(user_permissions.keys()))) for doctypes in user_permission_doctypes] if len(user_permission_doctypes) > 1: @@ -452,6 +473,22 @@ def get_user_permission_doctypes(user_permission_doctypes, user_permissions): return user_permission_doctypes +def update_permission_property(doctype, role, permlevel, ptype, value=None, validate=True): + '''Update a property in Custom Perm''' + from frappe.core.doctype.doctype.doctype import validate_permissions_for_doctype + out = setup_custom_perms(doctype) + + name = frappe.get_value('Custom DocPerm', dict(parent=doctype, role=role, + permlevel=permlevel)) + + frappe.db.sql(""" + update `tabCustom DocPerm` + set `{0}`=%s where name=%s""".format(ptype), (value, name)) + if validate: + validate_permissions_for_doctype(doctype) + + return out + def setup_custom_perms(parent): '''if custom permssions are not setup for the current doctype, set them up''' if not frappe.db.exists('Custom DocPerm', dict(parent=parent)): diff --git a/frappe/public/build.json b/frappe/public/build.json index 3fafb6c5f2..054421286e 100755 --- a/frappe/public/build.json +++ b/frappe/public/build.json @@ -52,7 +52,8 @@ "public/css/desktop.css", "public/css/form.css", "public/css/mobile.css", - "public/css/kanban.css" + "public/css/kanban.css", + "public/css/graphs.css" ], "css/frappe-rtl.css": [ "public/css/bootstrap-rtl.css", @@ -85,6 +86,7 @@ "public/js/frappe/dom.js", "public/js/frappe/ui/messages.js", "public/js/frappe/ui/keyboard.js", + "public/js/frappe/ui/emoji.js", "public/js/frappe/request.js", "public/js/frappe/socketio_client.js", @@ -132,6 +134,7 @@ "public/js/frappe/misc/help.js", "public/js/frappe/misc/help_links.js", "public/js/frappe/misc/address_and_contact.js", + "public/js/frappe/misc/preview_email.js", "public/js/frappe/ui/upload.html", "public/js/frappe/upload.js", @@ -162,7 +165,8 @@ "public/js/frappe/query_string.js", "public/js/frappe/ui/charts.js", - "public/js/frappe/ui/graph.js", + "public/js/frappe/ui/graphs.js", + "public/js/frappe/ui/comment.js", "public/js/frappe/misc/rating_icons.html", "public/js/frappe/feedback.js" diff --git a/frappe/public/css/desk.css b/frappe/public/css/desk.css index 8ea2c5650f..22ecdb993b 100644 --- a/frappe/public/css/desk.css +++ b/frappe/public/css/desk.css @@ -258,7 +258,7 @@ a[disabled="disabled"]:hover { } .link-btn { position: absolute; - top: 2px; + top: 3px; right: 4px; border-radius: 2px; padding: 3px; @@ -709,6 +709,21 @@ a.progress-small .progress-bar { .modal .note-editor [data-original-title="Table"] { display: none; } +.note-hint-popover { + border-radius: 3px; + border-color: #d1d8dd; + padding: 0; +} +.note-hint-popover .popover-content { + padding: 0; +} +.note-hint-popover .note-hint-item { + color: #36414C !important; + padding: 5px 8.8px !important; +} +.note-hint-popover .note-hint-item.active { + background-color: #F0F4F7 !important; +} .search-dialog .modal-dialog { width: 768px; } diff --git a/frappe/public/css/docs.css b/frappe/public/css/docs.css index 20b11d5cb1..2fdd0ae21d 100644 --- a/frappe/public/css/docs.css +++ b/frappe/public/css/docs.css @@ -597,9 +597,3 @@ a.edit:visited, .page-content-wrapper > .row .col-sm-4 { display: none; } -.screenshot { - border: 2px solid #d1d8dd; - box-shadow: 1px 1px 7px rgba(0, 0, 0, 0.15); - margin: 15px 0px; - max-width: 100%; -} diff --git a/frappe/public/css/email.css b/frappe/public/css/email.css index 3f2df6d15f..4e9dfbaa6e 100644 --- a/frappe/public/css/email.css +++ b/frappe/public/css/email.css @@ -48,6 +48,16 @@ hr { .body-table.has-header .email-footer { border-top: none; } +.email-footer-container { + margin-top: 10px; +} +.email-footer-container > div:not(:last-child) { + margin-bottom: 5px; +} +.email-unsubscribe a { + color: #8D99A6; + text-decoration: underline; +} .btn { text-decoration: none; padding: 7px 10px; @@ -121,6 +131,9 @@ hr { .text-small { font-size: 10px; } +.text-bold { + font-weight: bold; +} .indicator { width: 8px; height: 8px; diff --git a/frappe/public/css/form.css b/frappe/public/css/form.css index 0d21271862..c56811e892 100644 --- a/frappe/public/css/form.css +++ b/frappe/public/css/form.css @@ -678,80 +678,6 @@ select.form-control { padding: 10px; margin: 10px; } -.graph-container .graphics { - margin-top: 10px; - padding: 10px 0px; -} -.graph-container .stats-group { - display: flex; - justify-content: space-around; - flex: 1; -} -.graph-container .stats-container { - display: flex; - justify-content: space-around; -} -.graph-container .stats-container .stats { - padding-bottom: 15px; -} -.graph-container .stats-container .stats-title { - color: #8D99A6; -} -.graph-container .stats-container .stats-value { - font-size: 20px; - font-weight: 300; -} -.graph-container .stats-container .stats-description { - font-size: 12px; - color: #8D99A6; -} -.graph-container .stats-container .graph-data .stats-value { - color: #98d85b; -} -.bar-graph .axis, -.line-graph .axis { - font-size: 10px; - fill: #6a737d; -} -.bar-graph .axis line, -.line-graph .axis line { - stroke: rgba(27, 31, 35, 0.1); -} -.data-points circle { - fill: #28a745; - stroke: #fff; - stroke-width: 2; -} -.data-points g.mini { - fill: #98d85b; -} -.data-points path { - fill: none; - stroke: #28a745; - stroke-opacity: 1; - stroke-width: 2px; -} -.line-graph .path { - fill: none; - stroke: #28a745; - stroke-opacity: 1; - stroke-width: 2px; -} -line.dashed { - stroke-dasharray: 5,3; -} -.tick.x-axis-label { - display: block; -} -.tick .specific-value { - text-anchor: start; -} -.tick .y-value-text { - text-anchor: end; -} -.tick .x-value-text { - text-anchor: middle; -} body[data-route^="Form/Communication"] textarea[data-fieldname="subject"] { height: 80px !important; } diff --git a/frappe/public/css/graphs.css b/frappe/public/css/graphs.css new file mode 100644 index 0000000000..a9fdf62dc9 --- /dev/null +++ b/frappe/public/css/graphs.css @@ -0,0 +1,274 @@ +/* graphs */ +.graph-container .graph-focus-margin { + margin: 0px 5%; +} +.graph-container .graph-graphics { + margin-top: 10px; + padding: 10px 0px; + position: relative; +} +.graph-container .graph-stats-group { + display: flex; + justify-content: space-around; + flex: 1; +} +.graph-container .graph-stats-container { + display: flex; + justify-content: space-around; + padding-top: 10px; +} +.graph-container .graph-stats-container .stats { + padding-bottom: 15px; +} +.graph-container .graph-stats-container .stats-title { + color: #8D99A6; +} +.graph-container .graph-stats-container .stats-value { + font-size: 20px; + font-weight: 300; +} +.graph-container .graph-stats-container .stats-description { + font-size: 12px; + color: #8D99A6; +} +.graph-container .graph-stats-container .graph-data .stats-value { + color: #98d85b; +} +.graph-container .bar-graph .axis, +.graph-container .line-graph .axis { + font-size: 10px; + fill: #6a737d; +} +.graph-container .bar-graph .axis line, +.graph-container .line-graph .axis line { + stroke: rgba(27, 31, 35, 0.1); +} +.graph-container .percentage-graph { + margin-top: 35px; +} +.graph-container .percentage-graph .progress { + margin-bottom: 0px; +} +.graph-container .graph-data-points circle { + stroke: #fff; + stroke-width: 2; +} +.graph-container .graph-data-points path { + fill: none; + stroke-opacity: 1; + stroke-width: 2px; +} +.graph-container line.graph-dashed { + stroke-dasharray: 5,3; +} +.graph-container .tick.x-axis-label { + display: block; +} +.graph-container .tick .specific-value { + text-anchor: start; +} +.graph-container .tick .y-value-text { + text-anchor: end; +} +.graph-container .tick .x-value-text { + text-anchor: middle; +} +.graph-container .graph-svg-tip { + position: absolute; + z-index: 99999; + padding: 10px; + font-size: 12px; + color: #959da5; + text-align: center; + background: rgba(0, 0, 0, 0.8); + border-radius: 3px; +} +.graph-container .graph-svg-tip.comparison { + padding: 0; + text-align: left; + pointer-events: none; +} +.graph-container .graph-svg-tip.comparison .title { + display: block; + padding: 10px; + margin: 0; + font-weight: 600; + line-height: 1; + pointer-events: none; +} +.graph-container .graph-svg-tip.comparison ul { + margin: 0; + white-space: nowrap; + list-style: none; +} +.graph-container .graph-svg-tip.comparison li { + display: inline-block; + padding: 5px 10px; +} +.graph-container .graph-svg-tip ul, +.graph-container .graph-svg-tip ol { + padding-left: 0; + display: flex; +} +.graph-container .graph-svg-tip ul.data-point-list li { + min-width: 90px; + flex: 1; +} +.graph-container .graph-svg-tip strong { + color: #dfe2e5; +} +.graph-container .graph-svg-tip::after { + position: absolute; + bottom: -10px; + left: 50%; + width: 5px; + height: 5px; + margin: 0 0 0 -5px; + content: " "; + border: 5px solid transparent; + border-top-color: rgba(0, 0, 0, 0.8); +} +.graph-container .stroke.grey { + stroke: #F0F4F7; +} +.graph-container .stroke.blue { + stroke: #5e64ff; +} +.graph-container .stroke.red { + stroke: #ff5858; +} +.graph-container .stroke.light-green { + stroke: #98d85b; +} +.graph-container .stroke.green { + stroke: #28a745; +} +.graph-container .stroke.orange { + stroke: #ffa00a; +} +.graph-container .stroke.purple { + stroke: #743ee2; +} +.graph-container .stroke.darkgrey { + stroke: #b8c2cc; +} +.graph-container .stroke.black { + stroke: #36414C; +} +.graph-container .stroke.yellow { + stroke: #FEEF72; +} +.graph-container .stroke.light-blue { + stroke: #7CD6FD; +} +.graph-container .stroke.lightblue { + stroke: #7CD6FD; +} +.graph-container .fill.grey { + fill: #F0F4F7; +} +.graph-container .fill.blue { + fill: #5e64ff; +} +.graph-container .fill.red { + fill: #ff5858; +} +.graph-container .fill.light-green { + fill: #98d85b; +} +.graph-container .fill.green { + fill: #28a745; +} +.graph-container .fill.orange { + fill: #ffa00a; +} +.graph-container .fill.purple { + fill: #743ee2; +} +.graph-container .fill.darkgrey { + fill: #b8c2cc; +} +.graph-container .fill.black { + fill: #36414C; +} +.graph-container .fill.yellow { + fill: #FEEF72; +} +.graph-container .fill.light-blue { + fill: #7CD6FD; +} +.graph-container .fill.lightblue { + fill: #7CD6FD; +} +.graph-container .background.grey { + background: #F0F4F7; +} +.graph-container .background.blue { + background: #5e64ff; +} +.graph-container .background.red { + background: #ff5858; +} +.graph-container .background.light-green { + background: #98d85b; +} +.graph-container .background.green { + background: #28a745; +} +.graph-container .background.orange { + background: #ffa00a; +} +.graph-container .background.purple { + background: #743ee2; +} +.graph-container .background.darkgrey { + background: #b8c2cc; +} +.graph-container .background.black { + background: #36414C; +} +.graph-container .background.yellow { + background: #FEEF72; +} +.graph-container .background.light-blue { + background: #7CD6FD; +} +.graph-container .background.lightblue { + background: #7CD6FD; +} +.graph-container .border-top.grey { + border-top: 3px solid #F0F4F7; +} +.graph-container .border-top.blue { + border-top: 3px solid #5e64ff; +} +.graph-container .border-top.red { + border-top: 3px solid #ff5858; +} +.graph-container .border-top.light-green { + border-top: 3px solid #98d85b; +} +.graph-container .border-top.green { + border-top: 3px solid #28a745; +} +.graph-container .border-top.orange { + border-top: 3px solid #ffa00a; +} +.graph-container .border-top.purple { + border-top: 3px solid #743ee2; +} +.graph-container .border-top.darkgrey { + border-top: 3px solid #b8c2cc; +} +.graph-container .border-top.black { + border-top: 3px solid #36414C; +} +.graph-container .border-top.yellow { + border-top: 3px solid #FEEF72; +} +.graph-container .border-top.light-blue { + border-top: 3px solid #7CD6FD; +} +.graph-container .border-top.lightblue { + border-top: 3px solid #7CD6FD; +} diff --git a/frappe/public/css/list.css b/frappe/public/css/list.css index 2f342af46f..05cb1d07b6 100644 --- a/frappe/public/css/list.css +++ b/frappe/public/css/list.css @@ -215,17 +215,19 @@ text-align: left; } .tag-row { - margin-top: 5px; padding-left: 55px; + margin-bottom: 0px; + margin-top: -5px; } .taggle_placeholder { top: 0; - left: 0; + left: 5px; font-size: 11px; + color: #8D99A6; } .taggle_list { - padding-left: 0; - margin-bottom: 0; + padding-left: 5px; + margin-bottom: 3px; } .taggle_list .taggle { font-size: 11px; diff --git a/frappe/public/css/sidebar.css b/frappe/public/css/sidebar.css index 5b7fa2e8fd..1bea165047 100644 --- a/frappe/public/css/sidebar.css +++ b/frappe/public/css/sidebar.css @@ -293,19 +293,6 @@ body[data-route^="Module"] .main-menu .form-sidebar { .sidebar-left .list-sidebar .stat-label { margin-bottom: -10px; } -.list-sidebar .list-tag-preview { - padding: 5px !important; - margin-left: 5px; - display: inline !important; -} -.list-sidebar .list-tag-preview:hover, -a.close:hover, -.list-sidebar .list-tag-preview:active, -a.close:active, -.list-sidebar .list-tag-preview:focus, -a.close:focus { - border-color: transparent !important; -} .layout-side-section .module-sidebar-nav { margin-top: 15px; } diff --git a/frappe/public/css/website.css b/frappe/public/css/website.css index 9a92bccf38..b9b2d733bb 100644 --- a/frappe/public/css/website.css +++ b/frappe/public/css/website.css @@ -430,6 +430,9 @@ h6 a { color: inherit !important; text-decoration: none; } +li { + line-height: 1.7em; +} .navbar-brand { max-width: none; } @@ -503,6 +506,9 @@ h6 a { min-height: 140px; border-top: 1px solid #EBEFF2; } +.page_content { + padding-bottom: 30px; +} .carousel-control .icon { position: absolute; top: 50%; @@ -599,7 +605,7 @@ fieldset { } .web-sidebar .sidebar-item { margin: 0px; - padding: 12px 0px; + padding-bottom: 12px; border: none; color: #8D99A6; font-size: 12px; @@ -607,21 +613,14 @@ fieldset { .web-sidebar .sidebar-item .badge { font-weight: normal; } -.web-sidebar .sidebar-item:first-child { - padding-top: 10px; -} -.web-sidebar .sidebar-item:last-child { - padding-bottom: 10px; -} .web-sidebar .sidebar-item a { - color: #8D99A6; + color: #36414C !important; } .web-sidebar .sidebar-item a.active { color: #36414C !important; font-weight: 500 !important; } .web-sidebar .sidebar-items { - margin-top: -10px; margin-bottom: 30px; } .web-sidebar .sidebar-items .title { @@ -675,69 +674,25 @@ fieldset { .web-list-item:last-child { border-bottom: 0px; } -.blog-info { - text-align: center; - margin-top: 30px; -} -.post-description { - padding-bottom: 8px; -} -.post-description p { - margin-bottom: 8px; -} -.blog-footer { - padding: 5px 15px; - border-top: 1px solid #EBEFF2; - margin: 0px -15px -20px -15px; -} -.blog-list-content .website-list .result { +.website-list .result { border: 0px; } -.blog-list-content .web-list-item:hover { +.web-list-item:hover { background: transparent; } -.blog-category { - letter-spacing: 0.5px; - text-align: center; - margin-bottom: 30px; -} -.author { - letter-spacing: 0.5px; - border-bottom: 1px solid #EBEFF2; - padding-bottom: 30px; -} -.blogger { - padding-top: 0px; - padding-bottom: 50px; -} -.blog-dot:before { +.spacer-dot:before { padding-right: 8px; padding-left: 8px; content: "\2022"; } -.blog-list-item { - margin-top: 30px; - margin-bottom: 30px; -} -.blog-list-item .blog-header { - font-size: 1.6em; -} -.blog-header { - font-weight: 700; - font-size: 2em; -} .add-comment-section { padding-bottom: 30px; } -.blog-comments { - position: relative; - border-top: 1px solid #d1d8dd; -} -.blog-comment-row { +.comment-row { margin: 0px -15px; padding: 15px; } -.blog-comment-row:last-child { +.comment-row:last-child { margin-bottom: 30px; border-bottom: 0px; } @@ -837,7 +792,7 @@ a.active { } .sidebar-block, .page-content { - padding-top: 50px; + padding-top: 30px; padding-bottom: 50px; } .your-account-info { @@ -871,19 +826,6 @@ a.active { li.footer-child-item { margin: 15px 0px; } -.blog-info { - text-align: center; - margin-top: 30px; -} -.blog-text { - padding-top: 50px; - padding-bottom: 50px; - font-size: 18px; - line-height: 1.5; -} -.blog-text p { - margin-bottom: 30px; -} .comment-view { padding-bottom: 30px; } @@ -927,7 +869,7 @@ li.footer-child-item { overflow: hidden; } .vert-line > div + div { - border-left: 1px solid #EBEFF2; + border-left: 1px solid #d1d8dd; } .vert-line > div { padding-bottom: 2000px; @@ -994,3 +936,13 @@ li.footer-child-item { padding: 10px; border-radius: 4px; } +.docfields pre { + background-color: transparent; + border: none; +} +.screenshot { + border: 1px solid #d1d8dd; + box-shadow: 1px 1px 7px rgba(0, 0, 0, 0.15); + margin: 15px 0px; + max-width: 100%; +} diff --git a/frappe/public/js/frappe/defaults.js b/frappe/public/js/frappe/defaults.js index 24ba5e630e..570ae61e2a 100644 --- a/frappe/public/js/frappe/defaults.js +++ b/frappe/public/js/frappe/defaults.js @@ -77,10 +77,6 @@ frappe.defaults = { }, get_user_permissions: function() { - return frappe.defaults.user_permissions; + return frappe.boot.user_permissions; }, - set_user_permissions: function(user_permissions) { - if(!user_permissions) return; - frappe.defaults.user_permissions = $.extend(frappe.defaults.user_permissions || {}, user_permissions); - } } diff --git a/frappe/public/js/frappe/form/dashboard.js b/frappe/public/js/frappe/form/dashboard.js index d27aa619e0..ca2b4ab9bd 100644 --- a/frappe/public/js/frappe/form/dashboard.js +++ b/frappe/public/js/frappe/form/dashboard.js @@ -418,9 +418,8 @@ frappe.ui.form.Dashboard = Class.extend({ this.graph_area.empty().removeClass('hidden'); $.extend(args, { parent: me.graph_area, - width: 710, - height: 140, - mode: 'line-graph' + mode: 'line', + height: 140 }); new frappe.ui.Graph(args); diff --git a/frappe/public/js/frappe/form/footer/timeline.html b/frappe/public/js/frappe/form/footer/timeline.html index 381b540300..f4f56e3371 100644 --- a/frappe/public/js/frappe/form/footer/timeline.html +++ b/frappe/public/js/frappe/form/footer/timeline.html @@ -1,17 +1,6 @@
    -
    -
    - {%= __("Add a comment") %} - -
    -
    - - -
    {{ __("Ctrl+Enter to add comment") }}
    -
    -
    +
    +
    {% if(doctype === "Communication") { %} \
    ').appendTo(me.list); } @@ -161,43 +162,45 @@ frappe.ui.form.Timeline = Class.extend({ .on("click", ".close", function() { var name = $timeline_item.data('name'); me.delete_comment(name); - return false; }) - .on('click', '.edit', function() { - var is_editing = 'is-editing'; - var content = $timeline_item.find('.timeline-item-content'); + .on('click', '.edit', function(e) { + e.preventDefault(); var name = $timeline_item.data('name'); - var update_comment = function() { - var val = content.find('textarea').val(); - // set content to new val so that on save and refresh the new content is shown - c.content = val; - - frappe.timeline.update_communication(c); - me.update_comment(name, val); - - // all changes to the timeline_item for editing are reset after calling refresh - me.refresh(); - } - - if(content.hasClass(is_editing)) { - update_comment(); + if($timeline_item.hasClass('is-editing')) { + me.editing_area.submit(); + me.$editing_area.detach(); } else { var $edit_btn = $(this); - var editing_textarea = me.input.clone() - .removeClass('comment-input'); - - editing_textarea.keydown("meta+return ctrl+return", function(e) { - update_comment(); - }); - - frappe.db.get_value('Communication', {name: name}, 'content', function(r) { - $edit_btn.find('i').removeClass('octicon-pencil').addClass('octicon-check'); - editing_textarea.val(r.content); - content.html(editing_textarea); - content.addClass(is_editing); - }); + var content = $timeline_item.find('.timeline-item-content').html(); + + $edit_btn + .find('i') + .removeClass('octicon-pencil') + .addClass('octicon-check'); + + me.editing_area.setup_summernote(); + me.editing_area.val(content); + me.editing_area.on_submit = (value) => { + me.editing_area.destroy(); + value = value.trim(); + // set content to new val so that on save and refresh the new content is shown + c.content = value; + frappe.timeline.update_communication(c); + me.update_comment(name, value); + // all changes to the timeline_item for editing are reset after calling refresh + me.refresh(); + }; + + $timeline_item + .find('.timeline-item-content') + .hide(); + $timeline_item + .find('.timeline-content-show') + .append(me.$editing_area); + $timeline_item + .addClass('is-editing'); } return false; @@ -295,11 +298,14 @@ frappe.ui.form.Timeline = Class.extend({ } else { c.content_html = c.content; c.content_html = frappe.utils.strip_whitespace(c.content_html); - c.content_html = c.content_html.replace(/</g,"<").replace(/>/g,">") + c.content_html = c.content_html.replace(/</g,"<").replace(/>/g,">"); } // bold @mentions - if(c.comment_type==="Comment") { + if(c.comment_type==="Comment" && + // avoid adding tag a 2nd time + !c.content_html.match(/(^|\W)(@\w+)<\/b>/) + ) { c.content_html = c.content_html.replace(/(^|\W)(@\w+)/g, "$1$2"); } @@ -519,16 +525,8 @@ frappe.ui.form.Timeline = Class.extend({ }; }, - add_comment: function(btn) { - var txt = this.input.val(); - - if(txt) { - this.insert_comment("Comment", txt, btn, this.input); - } - }, - insert_comment: function(comment_type, comment, btn, input) { + insert_comment: function(comment_type, comment, btn) { var me = this; - if(input) input.prop('disabled', true); return frappe.call({ method: "frappe.desk.form.utils.add_comment", args: { @@ -545,8 +543,7 @@ frappe.ui.form.Timeline = Class.extend({ btn: btn, callback: function(r) { if(!r.exc) { - me.input.val(""); - + me.comment_area.val(''); frappe.utils.play_sound("click"); var comment = r.message; @@ -565,9 +562,6 @@ frappe.ui.form.Timeline = Class.extend({ me.frm.get_docinfo().communications = comments.concat([r.message]); me.refresh(true); } - }, - always: function() { - if(input) input.prop('disabled', false); } }); @@ -620,7 +614,9 @@ frappe.ui.form.Timeline = Class.extend({ fieldname: 'content', value: content, }, callback: function(r) { - frappe.utils.play_sound('click'); + if(!r.exc) { + frappe.utils.play_sound('click'); + } } }); }, @@ -655,35 +651,11 @@ frappe.ui.form.Timeline = Class.extend({ return last_email; }, - setup_mentions: function() { - this.setup_awesomplete_for_mentions(); - }, + get_usernames_for_mentions: function() { + var valid_users = Object.keys(frappe.boot.user_info) + .filter(user => !["Administrator", "Guest"].includes(user)); - setup_awesomplete_for_mentions: function() { - var me = this; - var username_user_map = {}; - for (var name in frappe.boot.user_info) { - if(name !== "Administrator" && name !== "Guest") { - var _user = frappe.boot.user_info[name]; - username_user_map[_user.username] = _user; - } - } - var source = Object.keys(username_user_map); - - this.awesomplete = new Awesomplete(this.input.get(0), { - minChars: 0, - maxItems: 99, - autoFirst: true, - list: source, - filter: function(text, input) { - if(input.indexOf("@") === -1) return false; - return Awesomplete.FILTER_STARTSWITH(text, input.match(/[^@]*$/)[0]); - }, - replace: function(text) { - var before = this.input.value.match(/^.*@\s*|/)[0]; - this.input.value = before + text + " "; - } - }); + return valid_users.map(user => frappe.boot.user_info[user].username); }, setup_comment_like: function() { diff --git a/frappe/public/js/frappe/form/print.js b/frappe/public/js/frappe/form/print.js index 18a5891f36..0e7e75299a 100644 --- a/frappe/public/js/frappe/form/print.js +++ b/frappe/public/js/frappe/form/print.js @@ -125,9 +125,20 @@ frappe.ui.form.PrintPreview = Class.extend({ var me = this; this.get_print_html(function (out) { me.wrapper.find(".print-format").html(out.html); + me.show_footer(); me.set_style(out.style); }); }, + show_footer: function() { + // footer is hidden by default as reqd by pdf generation + // simple hack to show it in print preview + this.wrapper.find('.print-format').css('position', 'relative'); + this.wrapper.find('#footer-html').attr('style', ` + display: block !important; + position: absolute; + bottom: 0.75in; + `); + }, printit: function () { this.new_page_preview(true); }, diff --git a/frappe/public/js/frappe/list/list_renderer.js b/frappe/public/js/frappe/list/list_renderer.js index 394d7d3f16..1ec5b3e37e 100644 --- a/frappe/public/js/frappe/list/list_renderer.js +++ b/frappe/public/js/frappe/list/list_renderer.js @@ -428,8 +428,7 @@ frappe.views.ListRenderer = Class.extend({ }, get_indicator_from_doc: function (doc) { var workflow = frappe.workflow.workflows[this.doctype]; - var override = workflow ? workflow['override_status'] : true; - return frappe.get_indicator(doc, this.doctype, override); + return frappe.get_indicator(doc, this.doctype, (workflow && workflow['override_status']) || true); }, prepare_data: function (data) { if (data.modified) diff --git a/frappe/public/js/frappe/list/list_sidebar.js b/frappe/public/js/frappe/list/list_sidebar.js index 375bd0d6c4..4a9a5753c2 100644 --- a/frappe/public/js/frappe/list/list_sidebar.js +++ b/frappe/public/js/frappe/list/list_sidebar.js @@ -195,7 +195,7 @@ frappe.views.ListSidebar = Class.extend({ var custom_column = values.custom_column !== undefined ? values.custom_column : 1; - + if(custom_column) { var field_name = 'kanban_column'; } else { @@ -350,43 +350,35 @@ frappe.views.ListSidebar = Class.extend({ var stats = [] var label = frappe.meta.docfield_map[this.doctype][field] ? frappe.meta.docfield_map[this.doctype][field].label : field; - var show_tags = ''; stat = (stat || []).sort(function(a, b) { return b[1] - a[1] }); $.each(stat, function(i,v) { sum = sum + v[1]; }) - if(tags) - { + if(tags) { for (var t in tags) { var nfound = -1; for (var i in stat) { if (tags[t] ===stat[i][0]) { stats.push(stat[i]); nfound = i; - break + break; } } - if (nfound<0) - { - stats.push([tags[t],0]) - } - else - { + if (nfound<0) { + stats.push([tags[t],0]); + } else { me.tempstats["_user_tags"].splice(nfound,1); } } - field = "_user_tags" - } - else - { - stats = stat + field = "_user_tags"; + } else { + stats = stat; } var context = { field: field, stat: stats, sum: sum, - label: field==='_user_tags' ? tags ? __(label)+ show_tags:(__("UnCategorised Tags") + show_tags): __(label), + label: field==='_user_tags' ? (tags ? __(label) : __("Tags")) : __(label), }; var sidebar_stat = $(frappe.render_template("list_sidebar_stat", context)) .on("click", ".stat-link", function() { diff --git a/frappe/public/js/frappe/list/list_sidebar_stat.html b/frappe/public/js/frappe/list/list_sidebar_stat.html index 5f23d439eb..10af8bcf07 100644 --- a/frappe/public/js/frappe/list/list_sidebar_stat.html +++ b/frappe/public/js/frappe/list/list_sidebar_stat.html @@ -1,19 +1,22 @@ + diff --git a/frappe/public/js/frappe/list/list_view.js b/frappe/public/js/frappe/list/list_view.js index 05facf346d..a7024a9fb7 100644 --- a/frappe/public/js/frappe/list/list_view.js +++ b/frappe/public/js/frappe/list/list_view.js @@ -599,9 +599,9 @@ frappe.views.ListView = frappe.ui.BaseList.extend({ }, true); } if (frappe.model.can_set_user_permissions(this.doctype)) { - this.page.add_menu_item(__('User Permissions Manager'), function () { - frappe.set_route('user-permissions', { - doctype: me.doctype + this.page.add_menu_item(__('User Permissions'), function () { + frappe.set_route('List', 'User Permission', { + allow: me.doctype }); }, true); } diff --git a/frappe/public/js/frappe/misc/preview_email.js b/frappe/public/js/frappe/misc/preview_email.js new file mode 100644 index 0000000000..b3e3bfddce --- /dev/null +++ b/frappe/public/js/frappe/misc/preview_email.js @@ -0,0 +1,16 @@ +frappe.preview_email = function(template, args, header) { + frappe.call({ + method: 'frappe.email.email_body.get_email_html', + args: { + subject: 'Test', + template, + args, + header + } + }).then((r) => { + var html = r.message; + html = html.replace(/embed=/, 'src='); + var d = frappe.msgprint(html); + d.$wrapper.find('.modal-dialog').css('width', '70%'); + }); +}; diff --git a/frappe/public/js/frappe/model/create_new.js b/frappe/public/js/frappe/model/create_new.js index 04496c6238..a25c3e70ca 100644 --- a/frappe/public/js/frappe/model/create_new.js +++ b/frappe/public/js/frappe/model/create_new.js @@ -127,8 +127,10 @@ $.extend(frappe.model, { var user_default = ""; var user_permissions = frappe.defaults.get_user_permissions(); var meta = frappe.get_meta(doc.doctype); - var has_user_permissions = (df.fieldtype==="Link" && user_permissions - && df.ignore_user_permissions != 1 && user_permissions[df.options]); + var has_user_permissions = (df.fieldtype==="Link" + && user_permissions + && df.ignore_user_permissions != 1 + && user_permissions[df.options]); // don't set defaults for "User" link field using User Permissions! if (df.fieldtype==="Link" && df.options!=="User") { diff --git a/frappe/public/js/frappe/model/indicator.js b/frappe/public/js/frappe/model/indicator.js index d033b3e8c9..d4c0807d7c 100644 --- a/frappe/public/js/frappe/model/indicator.js +++ b/frappe/public/js/frappe/model/indicator.js @@ -14,7 +14,7 @@ frappe.has_indicator = function(doctype) { return false; } -frappe.get_indicator = function(doc, doctype) { +frappe.get_indicator = function(doc, doctype, without_workflow = true) { if(doc.__unsaved) { return [__("Not Saved"), "orange"]; } @@ -27,7 +27,7 @@ frappe.get_indicator = function(doc, doctype) { workflow_fieldname = frappe.workflow.get_state_fieldname(doctype); // workflow - if(workflow_fieldname) { + if(workflow_fieldname && !without_workflow) { var value = doc[workflow_fieldname]; if(value) { var colour = ""; diff --git a/frappe/public/js/frappe/model/model.js b/frappe/public/js/frappe/model/model.js index adb2e845a6..7b22c5fd47 100644 --- a/frappe/public/js/frappe/model/model.js +++ b/frappe/public/js/frappe/model/model.js @@ -112,7 +112,6 @@ $.extend(frappe.model, { localStorage["_doctype:" + doctype] = JSON.stringify(r.docs); } frappe.model.init_doctype(doctype); - frappe.defaults.set_user_permissions(r.user_permissions); if(r.user_settings) { // remember filters and other settings from last view diff --git a/frappe/public/js/frappe/ui/comment.js b/frappe/public/js/frappe/ui/comment.js new file mode 100644 index 0000000000..e37724ddb3 --- /dev/null +++ b/frappe/public/js/frappe/ui/comment.js @@ -0,0 +1,192 @@ +/** + * CommentArea: A small rich text editor with + * support for @mentions and :emojis: + * @example + * let comment_area = new frappe.ui.CommentArea({ + * parent: '.comment-area', + * mentions: ['john', 'mary', 'kate'], + * on_submit: (value) => save_to_database(value) + * }); + */ + +frappe.ui.CommentArea = class CommentArea { + + constructor({ parent = null, mentions = [], on_submit = null, no_wrapper = false }) { + this.parent = $(parent); + this.mentions = mentions; + this.on_submit = on_submit; + this.no_wrapper = no_wrapper; + + this.make(); + } + + make() { + this.setup_dom(); + this.setup_summernote(); + this.bind_events(); + } + + setup_dom() { + const header = !this.no_wrapper ? + `
    + ${__("Add a comment")} + +
    ` : ''; + + const footer = !this.no_wrapper ? + `
    + ${__("Ctrl+Enter to add comment")} +
    ` : ''; + + this.wrapper = $(` +
    + ${ header } +
    +
    + ${ footer } +
    +
    + `); + this.wrapper.appendTo(this.parent); + this.input = this.parent.find('.comment-input'); + this.button = this.parent.find('.btn-comment'); + } + + setup_summernote() { + const { input, button } = this; + + input.summernote({ + height: 100, + toolbar: false, + airMode: true, + hint: { + mentions: this.mentions, + match: /\B([@:]\w*)/, + search: function (keyword, callback) { + let items = []; + if (keyword.startsWith('@')) { + keyword = keyword.substr(1); + items = this.mentions; + } else if (keyword.startsWith(':')) { + items = frappe.ui.emoji_keywords + .filter(k => k.startsWith(keyword)) + .slice(0, 7); + } + callback($.grep(items, function (item) { + return item.indexOf(keyword) == 0; + })); + }, + template: function (item) { + if (item.startsWith(':')) { + return frappe.ui.get_emoji(item) + ' ' + item; + } else { + return item; + } + }, + content: function (item) { + if(item.startsWith(':')) { + return frappe.ui.get_emoji(item); + } else { + return '@' + item; + } + } + }, + callbacks: { + onChange: () => { + if(input.summernote('isEmpty')) { + button + .removeClass('btn-primary') + .addClass('btn-default'); + } else { + button + .removeClass('btn-default') + .addClass('btn-primary'); + } + }, + onKeydown: (e) => { + var key = frappe.ui.keys.get_key(e); + if(key === 'ctrl+enter') { + e.preventDefault(); + this.submit(); + } + e.stopPropagation(); + }, + }, + icons: { + 'align': 'fa fa-align', + 'alignCenter': 'fa fa-align-center', + 'alignJustify': 'fa fa-align-justify', + 'alignLeft': 'fa fa-align-left', + 'alignRight': 'fa fa-align-right', + 'indent': 'fa fa-indent', + 'outdent': 'fa fa-outdent', + 'arrowsAlt': 'fa fa-arrows-alt', + 'bold': 'fa fa-bold', + 'caret': 'caret', + 'circle': 'fa fa-circle', + 'close': 'fa fa-close', + 'code': 'fa fa-code', + 'eraser': 'fa fa-eraser', + 'font': 'fa fa-font', + 'frame': 'fa fa-frame', + 'italic': 'fa fa-italic', + 'link': 'fa fa-link', + 'unlink': 'fa fa-chain-broken', + 'magic': 'fa fa-magic', + 'menuCheck': 'fa fa-check', + 'minus': 'fa fa-minus', + 'orderedlist': 'fa fa-list-ol', + 'pencil': 'fa fa-pencil', + 'picture': 'fa fa-image', + 'question': 'fa fa-question', + 'redo': 'fa fa-redo', + 'square': 'fa fa-square', + 'strikethrough': 'fa fa-strikethrough', + 'subscript': 'fa fa-subscript', + 'superscript': 'fa fa-superscript', + 'table': 'fa fa-table', + 'textHeight': 'fa fa-text-height', + 'trash': 'fa fa-trash', + 'underline': 'fa fa-underline', + 'undo': 'fa fa-undo', + 'unorderedlist': 'fa fa-list-ul', + 'video': 'fa fa-video-camera' + } + }); + + this.note_editor = this.wrapper.find('.note-editor'); + this.note_editor.css({ + 'border': '1px solid #ebeff2', + 'border-radius': '3px', + 'padding': '10px', + 'margin-bottom': '10px', + 'min-height': '80px', + 'cursor': 'text' + }); + this.note_editor.on('click', () => input.summernote('focus')); + } + + destroy() { + this.input.summernote('destroy'); + } + + bind_events() { + this.button.on('click', this.submit.bind(this)); + } + + val(value) { + // Return html if no value specified + if(value === undefined) { + return this.input.summernote('code'); + } + // Set html if value is specified + this.input.summernote('code', value); + } + + submit() { + // Pass comment's value (html) to submit handler + this.on_submit && this.on_submit(this.val()); + } +}; diff --git a/frappe/public/js/frappe/ui/emoji.js b/frappe/public/js/frappe/ui/emoji.js new file mode 100644 index 0000000000..f70fc926cb --- /dev/null +++ b/frappe/public/js/frappe/ui/emoji.js @@ -0,0 +1,4973 @@ +frappe.ui.get_emoji = function(keyword) { + if (!keyword.includes(':')) + keyword = ':' + keyword + ':'; + return frappe.ui.emoji_map[keyword]; +}; + +frappe.ui.emojis = [ + "🀄", + "🃏", + "🅰", + "🅱", + "🅾", + "🅿", + "🆎", + "🆑", + "🆒", + "🆓", + "🆔", + "🆕", + "🆖", + "🆗", + "🆘", + "🆙", + "🆚", + "🇦🇨", + "🇦🇩", + "🇦🇪", + "🇦🇫", + "🇦🇬", + "🇦🇮", + "🇦🇱", + "🇦🇲", + "🇦🇴", + "🇦🇶", + "🇦🇷", + "🇦🇸", + "🇦🇹", + "🇦🇺", + "🇦🇼", + "🇦🇽", + "🇦🇿", + "🇦", + "🇧🇦", + "🇧🇧", + "🇧🇩", + "🇧🇪", + "🇧🇫", + "🇧🇬", + "🇧🇭", + "🇧🇮", + "🇧🇯", + "🇧🇱", + "🇧🇲", + "🇧🇳", + "🇧🇴", + "🇧🇶", + "🇧🇷", + "🇧🇸", + "🇧🇹", + "🇧🇻", + "🇧🇼", + "🇧🇾", + "🇧🇿", + "🇧", + "🇨🇦", + "🇨🇨", + "🇨🇩", + "🇨🇫", + "🇨🇬", + "🇨🇭", + "🇨🇮", + "🇨🇰", + "🇨🇱", + "🇨🇲", + "🇨🇳", + "🇨🇴", + "🇨🇵", + "🇨🇷", + "🇨🇺", + "🇨🇻", + "🇨🇼", + "🇨🇽", + "🇨🇾", + "🇨🇿", + "🇨", + "🇩🇪", + "🇩🇬", + "🇩🇯", + "🇩🇰", + "🇩🇲", + "🇩🇴", + "🇩🇿", + "🇩", + "🇪🇦", + "🇪🇨", + "🇪🇪", + "🇪🇬", + "🇪🇭", + "🇪🇷", + "🇪🇸", + "🇪🇹", + "🇪🇺", + "🇪", + "🇫🇮", + "🇫🇯", + "🇫🇰", + "🇫🇲", + "🇫🇴", + "🇫🇷", + "🇫", + "🇬🇦", + "🇬🇧", + "🇬🇩", + "🇬🇪", + "🇬🇫", + "🇬🇬", + "🇬🇭", + "🇬🇮", + "🇬🇱", + "🇬🇲", + "🇬🇳", + "🇬🇵", + "🇬🇶", + "🇬🇷", + "🇬🇸", + "🇬🇹", + "🇬🇺", + "🇬🇼", + "🇬🇾", + "🇬", + "🇭🇰", + "🇭🇲", + "🇭🇳", + "🇭🇷", + "🇭🇹", + "🇭🇺", + "🇭", + "🇮🇨", + "🇮🇩", + "🇮🇪", + "🇮🇱", + "🇮🇲", + "🇮🇳", + "🇮🇴", + "🇮🇶", + "🇮🇷", + "🇮🇸", + "🇮🇹", + "🇮", + "🇯🇪", + "🇯🇲", + "🇯🇴", + "🇯🇵", + "🇯", + "🇰🇪", + "🇰🇬", + "🇰🇭", + "🇰🇮", + "🇰🇲", + "🇰🇳", + "🇰🇵", + "🇰🇷", + "🇰🇼", + "🇰🇾", + "🇰🇿", + "🇰", + "🇱🇦", + "🇱🇧", + "🇱🇨", + "🇱🇮", + "🇱🇰", + "🇱🇷", + "🇱🇸", + "🇱🇹", + "🇱🇺", + "🇱🇻", + "🇱🇾", + "🇱", + "🇲🇦", + "🇲🇨", + "🇲🇩", + "🇲🇪", + "🇲🇫", + "🇲🇬", + "🇲🇭", + "🇲🇰", + "🇲🇱", + "🇲🇲", + "🇲🇳", + "🇲🇴", + "🇲🇵", + "🇲🇶", + "🇲🇷", + "🇲🇸", + "🇲🇹", + "🇲🇺", + "🇲🇻", + "🇲🇼", + "🇲🇽", + "🇲🇾", + "🇲🇿", + "🇲", + "🇳🇦", + "🇳🇨", + "🇳🇪", + "🇳🇫", + "🇳🇬", + "🇳🇮", + "🇳🇱", + "🇳🇴", + "🇳🇵", + "🇳🇷", + "🇳🇺", + "🇳🇿", + "🇳", + "🇴🇲", + "🇴", + "🇵🇦", + "🇵🇪", + "🇵🇫", + "🇵🇬", + "🇵🇭", + "🇵🇰", + "🇵🇱", + "🇵🇲", + "🇵🇳", + "🇵🇷", + "🇵🇸", + "🇵🇹", + "🇵🇼", + "🇵🇾", + "🇵", + "🇶🇦", + "🇶", + "🇷🇪", + "🇷🇴", + "🇷🇸", + "🇷🇺", + "🇷🇼", + "🇷", + "🇸🇦", + "🇸🇧", + "🇸🇨", + "🇸🇩", + "🇸🇪", + "🇸🇬", + "🇸🇭", + "🇸🇮", + "🇸🇯", + "🇸🇰", + "🇸🇱", + "🇸🇲", + "🇸🇳", + "🇸🇴", + "🇸🇷", + "🇸🇸", + "🇸🇹", + "🇸🇻", + "🇸🇽", + "🇸🇾", + "🇸🇿", + "🇸", + "🇹🇦", + "🇹🇨", + "🇹🇩", + "🇹🇫", + "🇹🇬", + "🇹🇭", + "🇹🇯", + "🇹🇰", + "🇹🇱", + "🇹🇲", + "🇹🇳", + "🇹🇴", + "🇹🇷", + "🇹🇹", + "🇹🇻", + "🇹🇼", + "🇹🇿", + "🇹", + "🇺🇦", + "🇺🇬", + "🇺🇲", + "🇺🇳", + "🇺🇸", + "🇺🇾", + "🇺🇿", + "🇺", + "🇻🇦", + "🇻🇨", + "🇻🇪", + "🇻🇬", + "🇻🇮", + "🇻🇳", + "🇻🇺", + "🇻", + "🇼🇫", + "🇼🇸", + "🇼", + "🇽🇰", + "🇽", + "🇾🇪", + "🇾🇹", + "🇾", + "🇿🇦", + "🇿🇲", + "🇿🇼", + "🇿", + "🈁", + "🈂", + "🈚", + "🈯", + "🈲", + "🈳", + "🈴", + "🈵", + "🈶", + "🈷", + "🈸", + "🈹", + "🈺", + "🉐", + "🉑", + "🌀", + "🌁", + "🌂", + "🌃", + "🌄", + "🌅", + "🌆", + "🌇", + "🌈", + "🌉", + "🌊", + "🌋", + "🌌", + "🌍", + "🌎", + "🌏", + "🌐", + "🌑", + "🌒", + "🌓", + "🌔", + "🌕", + "🌖", + "🌗", + "🌘", + "🌙", + "🌚", + "🌛", + "🌜", + "🌝", + "🌞", + "🌟", + "🌠", + "🌡", + "🌤", + "🌥", + "🌦", + "🌧", + "🌨", + "🌩", + "🌪", + "🌫", + "🌬", + "🌭", + "🌮", + "🌯", + "🌰", + "🌱", + "🌲", + "🌳", + "🌴", + "🌵", + "🌶", + "🌷", + "🌸", + "🌹", + "🌺", + "🌻", + "🌼", + "🌽", + "🌾", + "🌿", + "🍀", + "🍁", + "🍂", + "🍃", + "🍄", + "🍅", + "🍆", + "🍇", + "🍈", + "🍉", + "🍊", + "🍋", + "🍌", + "🍍", + "🍎", + "🍏", + "🍐", + "🍑", + "🍒", + "🍓", + "🍔", + "🍕", + "🍖", + "🍗", + "🍘", + "🍙", + "🍚", + "🍛", + "🍜", + "🍝", + "🍞", + "🍟", + "🍠", + "🍡", + "🍢", + "🍣", + "🍤", + "🍥", + "🍦", + "🍧", + "🍨", + "🍩", + "🍪", + "🍫", + "🍬", + "🍭", + "🍮", + "🍯", + "🍰", + "🍱", + "🍲", + "🍳", + "🍴", + "🍵", + "🍶", + "🍷", + "🍸", + "🍹", + "🍺", + "🍻", + "🍼", + "🍽", + "🍾", + "🍿", + "🎀", + "🎁", + "🎂", + "🎃", + "🎄", + "🎅🏻", + "🎅🏼", + "🎅🏽", + "🎅🏾", + "🎅🏿", + "🎅", + "🎆", + "🎇", + "🎈", + "🎉", + "🎊", + "🎋", + "🎌", + "🎍", + "🎎", + "🎏", + "🎐", + "🎑", + "🎒", + "🎓", + "🎖", + "🎗", + "🎙", + "🎚", + "🎛", + "🎞", + "🎟", + "🎠", + "🎡", + "🎢", + "🎣", + "🎤", + "🎥", + "🎦", + "🎧", + "🎨", + "🎩", + "🎪", + "🎫", + "🎬", + "🎭", + "🎮", + "🎯", + "🎰", + "🎱", + "🎲", + "🎳", + "🎴", + "🎵", + "🎶", + "🎷", + "🎸", + "🎹", + "🎺", + "🎻", + "🎼", + "🎽", + "🎾", + "🎿", + "🏀", + "🏁", + "🏂🏻", + "🏂🏼", + "🏂🏽", + "🏂🏾", + "🏂🏿", + "🏂", + "🏃🏻‍♀️", + "🏃🏻‍♂️", + "🏃🏻", + "🏃🏼‍♀️", + "🏃🏼‍♂️", + "🏃🏼", + "🏃🏽‍♀️", + "🏃🏽‍♂️", + "🏃🏽", + "🏃🏾‍♀️", + "🏃🏾‍♂️", + "🏃🏾", + "🏃🏿‍♀️", + "🏃🏿‍♂️", + "🏃🏿", + "🏃‍♀️", + "🏃‍♂️", + "🏃", + "🏄🏻‍♀️", + "🏄🏻‍♂️", + "🏄🏻", + "🏄🏼‍♀️", + "🏄🏼‍♂️", + "🏄🏼", + "🏄🏽‍♀️", + "🏄🏽‍♂️", + "🏄🏽", + "🏄🏾‍♀️", + "🏄🏾‍♂️", + "🏄🏾", + "🏄🏿‍♀️", + "🏄🏿‍♂️", + "🏄🏿", + "🏄‍♀️", + "🏄‍♂️", + "🏄", + "🏅", + "🏆", + "🏇🏻", + "🏇🏼", + "🏇🏽", + "🏇🏾", + "🏇🏿", + "🏇", + "🏈", + "🏉", + "🏊🏻‍♀️", + "🏊🏻‍♂️", + "🏊🏻", + "🏊🏼‍♀️", + "🏊🏼‍♂️", + "🏊🏼", + "🏊🏽‍♀️", + "🏊🏽‍♂️", + "🏊🏽", + "🏊🏾‍♀️", + "🏊🏾‍♂️", + "🏊🏾", + "🏊🏿‍♀️", + "🏊🏿‍♂️", + "🏊🏿", + "🏊‍♀️", + "🏊‍♂️", + "🏊", + "🏋🏻‍♀️", + "🏋🏻‍♂️", + "🏋🏻", + "🏋🏼‍♀️", + "🏋🏼‍♂️", + "🏋🏼", + "🏋🏽‍♀️", + "🏋🏽‍♂️", + "🏋🏽", + "🏋🏾‍♀️", + "🏋🏾‍♂️", + "🏋🏾", + "🏋🏿‍♀️", + "🏋🏿‍♂️", + "🏋🏿", + "🏋️‍♀️", + "🏋️‍♂️", + "🏋", + "🏌🏻‍♀️", + "🏌🏻‍♂️", + "🏌🏻", + "🏌🏼‍♀️", + "🏌🏼‍♂️", + "🏌🏼", + "🏌🏽‍♀️", + "🏌🏽‍♂️", + "🏌🏽", + "🏌🏾‍♀️", + "🏌🏾‍♂️", + "🏌🏾", + "🏌🏿‍♀️", + "🏌🏿‍♂️", + "🏌🏿", + "🏌️‍♀️", + "🏌️‍♂️", + "🏌", + "🏍", + "🏎", + "🏏", + "🏐", + "🏑", + "🏒", + "🏓", + "🏔", + "🏕", + "🏖", + "🏗", + "🏘", + "🏙", + "🏚", + "🏛", + "🏜", + "🏝", + "🏞", + "🏟", + "🏠", + "🏡", + "🏢", + "🏣", + "🏤", + "🏥", + "🏦", + "🏧", + "🏨", + "🏩", + "🏪", + "🏫", + "🏬", + "🏭", + "🏮", + "🏯", + "🏰", + "🏳️‍🌈", + "🏳", + "🏴‍☠️", + "🏴", + "🏵", + "🏷", + "🏸", + "🏹", + "🏺", + "🏻", + "🏼", + "🏽", + "🏾", + "🏿", + "🐀", + "🐁", + "🐂", + "🐃", + "🐄", + "🐅", + "🐆", + "🐇", + "🐈", + "🐉", + "🐊", + "🐋", + "🐌", + "🐍", + "🐎", + "🐏", + "🐐", + "🐑", + "🐒", + "🐓", + "🐔", + "🐕", + "🐖", + "🐗", + "🐘", + "🐙", + "🐚", + "🐛", + "🐜", + "🐝", + "🐞", + "🐟", + "🐠", + "🐡", + "🐢", + "🐣", + "🐤", + "🐥", + "🐦", + "🐧", + "🐨", + "🐩", + "🐪", + "🐫", + "🐬", + "🐭", + "🐮", + "🐯", + "🐰", + "🐱", + "🐲", + "🐳", + "🐴", + "🐵", + "🐶", + "🐷", + "🐸", + "🐹", + "🐺", + "🐻", + "🐼", + "🐽", + "🐾", + "🐿", + "👀", + "👁‍🗨", + "👁", + "👂🏻", + "👂🏼", + "👂🏽", + "👂🏾", + "👂🏿", + "👂", + "👃🏻", + "👃🏼", + "👃🏽", + "👃🏾", + "👃🏿", + "👃", + "👄", + "👅", + "👆🏻", + "👆🏼", + "👆🏽", + "👆🏾", + "👆🏿", + "👆", + "👇🏻", + "👇🏼", + "👇🏽", + "👇🏾", + "👇🏿", + "👇", + "👈🏻", + "👈🏼", + "👈🏽", + "👈🏾", + "👈🏿", + "👈", + "👉🏻", + "👉🏼", + "👉🏽", + "👉🏾", + "👉🏿", + "👉", + "👊🏻", + "👊🏼", + "👊🏽", + "👊🏾", + "👊🏿", + "👊", + "👋🏻", + "👋🏼", + "👋🏽", + "👋🏾", + "👋🏿", + "👋", + "👌🏻", + "👌🏼", + "👌🏽", + "👌🏾", + "👌🏿", + "👌", + "👍🏻", + "👍🏼", + "👍🏽", + "👍🏾", + "👍🏿", + "👍", + "👎🏻", + "👎🏼", + "👎🏽", + "👎🏾", + "👎🏿", + "👎", + "👏🏻", + "👏🏼", + "👏🏽", + "👏🏾", + "👏🏿", + "👏", + "👐🏻", + "👐🏼", + "👐🏽", + "👐🏾", + "👐🏿", + "👐", + "👑", + "👒", + "👓", + "👔", + "👕", + "👖", + "👗", + "👘", + "👙", + "👚", + "👛", + "👜", + "👝", + "👞", + "👟", + "👠", + "👡", + "👢", + "👣", + "👤", + "👥", + "👦🏻", + "👦🏼", + "👦🏽", + "👦🏾", + "👦🏿", + "👦", + "👧🏻", + "👧🏼", + "👧🏽", + "👧🏾", + "👧🏿", + "👧", + "👨🏻‍🌾", + "👨🏻‍🍳", + "👨🏻‍🎓", + "👨🏻‍🎤", + "👨🏻‍🎨", + "👨🏻‍🏫", + "👨🏻‍🏭", + "👨🏻‍💻", + "👨🏻‍💼", + "👨🏻‍🔧", + "👨🏻‍🔬", + "👨🏻‍🚀", + "👨🏻‍🚒", + "👨🏻‍⚕️", + "👨🏻‍⚖️", + "👨🏻‍✈️", + "👨🏻", + "👨🏼‍🌾", + "👨🏼‍🍳", + "👨🏼‍🎓", + "👨🏼‍🎤", + "👨🏼‍🎨", + "👨🏼‍🏫", + "👨🏼‍🏭", + "👨🏼‍💻", + "👨🏼‍💼", + "👨🏼‍🔧", + "👨🏼‍🔬", + "👨🏼‍🚀", + "👨🏼‍🚒", + "👨🏼‍⚕️", + "👨🏼‍⚖️", + "👨🏼‍✈️", + "👨🏼", + "👨🏽‍🌾", + "👨🏽‍🍳", + "👨🏽‍🎓", + "👨🏽‍🎤", + "👨🏽‍🎨", + "👨🏽‍🏫", + "👨🏽‍🏭", + "👨🏽‍💻", + "👨🏽‍💼", + "👨🏽‍🔧", + "👨🏽‍🔬", + "👨🏽‍🚀", + "👨🏽‍🚒", + "👨🏽‍⚕️", + "👨🏽‍⚖️", + "👨🏽‍✈️", + "👨🏽", + "👨🏾‍🌾", + "👨🏾‍🍳", + "👨🏾‍🎓", + "👨🏾‍🎤", + "👨🏾‍🎨", + "👨🏾‍🏫", + "👨🏾‍🏭", + "👨🏾‍💻", + "👨🏾‍💼", + "👨🏾‍🔧", + "👨🏾‍🔬", + "👨🏾‍🚀", + "👨🏾‍🚒", + "👨🏾‍⚕️", + "👨🏾‍⚖️", + "👨🏾‍✈️", + "👨🏾", + "👨🏿‍🌾", + "👨🏿‍🍳", + "👨🏿‍🎓", + "👨🏿‍🎤", + "👨🏿‍🎨", + "👨🏿‍🏫", + "👨🏿‍🏭", + "👨🏿‍💻", + "👨🏿‍💼", + "👨🏿‍🔧", + "👨🏿‍🔬", + "👨🏿‍🚀", + "👨🏿‍🚒", + "👨🏿‍⚕️", + "👨🏿‍⚖️", + "👨🏿‍✈️", + "👨🏿", + "👨‍🌾", + "👨‍🍳", + "👨‍🎓", + "👨‍🎤", + "👨‍🎨", + "👨‍🏫", + "👨‍🏭", + "👨‍👦‍👦", + "👨‍👦", + "👨‍👧‍👦", + "👨‍👧‍👧", + "👨‍👧", + "👨‍👨‍👦‍👦", + "👨‍👨‍👦", + "👨‍👨‍👧‍👦", + "👨‍👨‍👧‍👧", + "👨‍👨‍👧", + "👨‍👩‍👦‍👦", + "👨‍👩‍👦", + "👨‍👩‍👧‍👦", + "👨‍👩‍👧‍👧", + "👨‍👩‍👧", + "👨‍💻", + "👨‍💼", + "👨‍🔧", + "👨‍🔬", + "👨‍🚀", + "👨‍🚒", + "👨‍⚕️", + "👨‍⚖️", + "👨‍✈️", + "👨‍❤️‍👨", + "👨‍❤️‍💋‍👨", + "👨", + "👩🏻‍🌾", + "👩🏻‍🍳", + "👩🏻‍🎓", + "👩🏻‍🎤", + "👩🏻‍🎨", + "👩🏻‍🏫", + "👩🏻‍🏭", + "👩🏻‍💻", + "👩🏻‍💼", + "👩🏻‍🔧", + "👩🏻‍🔬", + "👩🏻‍🚀", + "👩🏻‍🚒", + "👩🏻‍⚕️", + "👩🏻‍⚖️", + "👩🏻‍✈️", + "👩🏻", + "👩🏼‍🌾", + "👩🏼‍🍳", + "👩🏼‍🎓", + "👩🏼‍🎤", + "👩🏼‍🎨", + "👩🏼‍🏫", + "👩🏼‍🏭", + "👩🏼‍💻", + "👩🏼‍💼", + "👩🏼‍🔧", + "👩🏼‍🔬", + "👩🏼‍🚀", + "👩🏼‍🚒", + "👩🏼‍⚕️", + "👩🏼‍⚖️", + "👩🏼‍✈️", + "👩🏼", + "👩🏽‍🌾", + "👩🏽‍🍳", + "👩🏽‍🎓", + "👩🏽‍🎤", + "👩🏽‍🎨", + "👩🏽‍🏫", + "👩🏽‍🏭", + "👩🏽‍💻", + "👩🏽‍💼", + "👩🏽‍🔧", + "👩🏽‍🔬", + "👩🏽‍🚀", + "👩🏽‍🚒", + "👩🏽‍⚕️", + "👩🏽‍⚖️", + "👩🏽‍✈️", + "👩🏽", + "👩🏾‍🌾", + "👩🏾‍🍳", + "👩🏾‍🎓", + "👩🏾‍🎤", + "👩🏾‍🎨", + "👩🏾‍🏫", + "👩🏾‍🏭", + "👩🏾‍💻", + "👩🏾‍💼", + "👩🏾‍🔧", + "👩🏾‍🔬", + "👩🏾‍🚀", + "👩🏾‍🚒", + "👩🏾‍⚕️", + "👩🏾‍⚖️", + "👩🏾‍✈️", + "👩🏾", + "👩🏿‍🌾", + "👩🏿‍🍳", + "👩🏿‍🎓", + "👩🏿‍🎤", + "👩🏿‍🎨", + "👩🏿‍🏫", + "👩🏿‍🏭", + "👩🏿‍💻", + "👩🏿‍💼", + "👩🏿‍🔧", + "👩🏿‍🔬", + "👩🏿‍🚀", + "👩🏿‍🚒", + "👩🏿‍⚕️", + "👩🏿‍⚖️", + "👩🏿‍✈️", + "👩🏿", + "👩‍🌾", + "👩‍🍳", + "👩‍🎓", + "👩‍🎤", + "👩‍🎨", + "👩‍🏫", + "👩‍🏭", + "👩‍👦‍👦", + "👩‍👦", + "👩‍👧‍👦", + "👩‍👧‍👧", + "👩‍👧", + "👩‍👩‍👦‍👦", + "👩‍👩‍👦", + "👩‍👩‍👧‍👦", + "👩‍👩‍👧‍👧", + "👩‍👩‍👧", + "👩‍💻", + "👩‍💼", + "👩‍🔧", + "👩‍🔬", + "👩‍🚀", + "👩‍🚒", + "👩‍⚕️", + "👩‍⚖️", + "👩‍✈️", + "👩‍❤️‍👨", + "👩‍❤️‍👩", + "👩‍❤️‍💋‍👨", + "👩‍❤️‍💋‍👩", + "👩", + "👪🏻", + "👪🏼", + "👪🏽", + "👪🏾", + "👪🏿", + "👪", + "👫🏻", + "👫🏼", + "👫🏽", + "👫🏾", + "👫🏿", + "👫", + "👬🏻", + "👬🏼", + "👬🏽", + "👬🏾", + "👬🏿", + "👬", + "👭🏻", + "👭🏼", + "👭🏽", + "👭🏾", + "👭🏿", + "👭", + "👮🏻‍♀️", + "👮🏻‍♂️", + "👮🏻", + "👮🏼‍♀️", + "👮🏼‍♂️", + "👮🏼", + "👮🏽‍♀️", + "👮🏽‍♂️", + "👮🏽", + "👮🏾‍♀️", + "👮🏾‍♂️", + "👮🏾", + "👮🏿‍♀️", + "👮🏿‍♂️", + "👮🏿", + "👮‍♀️", + "👮‍♂️", + "👮", + "👯🏻‍♀️", + "👯🏻‍♂️", + "👯🏻", + "👯🏼‍♀️", + "👯🏼‍♂️", + "👯🏼", + "👯🏽‍♀️", + "👯🏽‍♂️", + "👯🏽", + "👯🏾‍♀️", + "👯🏾‍♂️", + "👯🏾", + "👯🏿‍♀️", + "👯🏿‍♂️", + "👯🏿", + "👯‍♀️", + "👯‍♂️", + "👯", + "👰🏻", + "👰🏼", + "👰🏽", + "👰🏾", + "👰🏿", + "👰", + "👱🏻‍♀️", + "👱🏻‍♂️", + "👱🏻", + "👱🏼‍♀️", + "👱🏼‍♂️", + "👱🏼", + "👱🏽‍♀️", + "👱🏽‍♂️", + "👱🏽", + "👱🏾‍♀️", + "👱🏾‍♂️", + "👱🏾", + "👱🏿‍♀️", + "👱🏿‍♂️", + "👱🏿", + "👱‍♀️", + "👱‍♂️", + "👱", + "👲🏻", + "👲🏼", + "👲🏽", + "👲🏾", + "👲🏿", + "👲", + "👳🏻‍♀️", + "👳🏻‍♂️", + "👳🏻", + "👳🏼‍♀️", + "👳🏼‍♂️", + "👳🏼", + "👳🏽‍♀️", + "👳🏽‍♂️", + "👳🏽", + "👳🏾‍♀️", + "👳🏾‍♂️", + "👳🏾", + "👳🏿‍♀️", + "👳🏿‍♂️", + "👳🏿", + "👳‍♀️", + "👳‍♂️", + "👳", + "👴🏻", + "👴🏼", + "👴🏽", + "👴🏾", + "👴🏿", + "👴", + "👵🏻", + "👵🏼", + "👵🏽", + "👵🏾", + "👵🏿", + "👵", + "👶🏻", + "👶🏼", + "👶🏽", + "👶🏾", + "👶🏿", + "👶", + "👷🏻‍♀️", + "👷🏻‍♂️", + "👷🏻", + "👷🏼‍♀️", + "👷🏼‍♂️", + "👷🏼", + "👷🏽‍♀️", + "👷🏽‍♂️", + "👷🏽", + "👷🏾‍♀️", + "👷🏾‍♂️", + "👷🏾", + "👷🏿‍♀️", + "👷🏿‍♂️", + "👷🏿", + "👷‍♀️", + "👷‍♂️", + "👷", + "👸🏻", + "👸🏼", + "👸🏽", + "👸🏾", + "👸🏿", + "👸", + "👹", + "👺", + "👻", + "👼🏻", + "👼🏼", + "👼🏽", + "👼🏾", + "👼🏿", + "👼", + "👽", + "👾", + "👿", + "💀", + "💁🏻‍♀️", + "💁🏻‍♂️", + "💁🏻", + "💁🏼‍♀️", + "💁🏼‍♂️", + "💁🏼", + "💁🏽‍♀️", + "💁🏽‍♂️", + "💁🏽", + "💁🏾‍♀️", + "💁🏾‍♂️", + "💁🏾", + "💁🏿‍♀️", + "💁🏿‍♂️", + "💁🏿", + "💁‍♀️", + "💁‍♂️", + "💁", + "💂🏻‍♀️", + "💂🏻‍♂️", + "💂🏻", + "💂🏼‍♀️", + "💂🏼‍♂️", + "💂🏼", + "💂🏽‍♀️", + "💂🏽‍♂️", + "💂🏽", + "💂🏾‍♀️", + "💂🏾‍♂️", + "💂🏾", + "💂🏿‍♀️", + "💂🏿‍♂️", + "💂🏿", + "💂‍♀️", + "💂‍♂️", + "💂", + "💃🏻", + "💃🏼", + "💃🏽", + "💃🏾", + "💃🏿", + "💃", + "💄", + "💅🏻", + "💅🏼", + "💅🏽", + "💅🏾", + "💅🏿", + "💅", + "💆🏻‍♀️", + "💆🏻‍♂️", + "💆🏻", + "💆🏼‍♀️", + "💆🏼‍♂️", + "💆🏼", + "💆🏽‍♀️", + "💆🏽‍♂️", + "💆🏽", + "💆🏾‍♀️", + "💆🏾‍♂️", + "💆🏾", + "💆🏿‍♀️", + "💆🏿‍♂️", + "💆🏿", + "💆‍♀️", + "💆‍♂️", + "💆", + "💇🏻‍♀️", + "💇🏻‍♂️", + "💇🏻", + "💇🏼‍♀️", + "💇🏼‍♂️", + "💇🏼", + "💇🏽‍♀️", + "💇🏽‍♂️", + "💇🏽", + "💇🏾‍♀️", + "💇🏾‍♂️", + "💇🏾", + "💇🏿‍♀️", + "💇🏿‍♂️", + "💇🏿", + "💇‍♀️", + "💇‍♂️", + "💇", + "💈", + "💉", + "💊", + "💋", + "💌", + "💍", + "💎", + "💏", + "💐", + "💑", + "💒", + "💓", + "💔", + "💕", + "💖", + "💗", + "💘", + "💙", + "💚", + "💛", + "💜", + "💝", + "💞", + "💟", + "💠", + "💡", + "💢", + "💣", + "💤", + "💥", + "💦", + "💧", + "💨", + "💩", + "💪🏻", + "💪🏼", + "💪🏽", + "💪🏾", + "💪🏿", + "💪", + "💫", + "💬", + "💭", + "💮", + "💯", + "💰", + "💱", + "💲", + "💳", + "💴", + "💵", + "💶", + "💷", + "💸", + "💹", + "💺", + "💻", + "💼", + "💽", + "💾", + "💿", + "📀", + "📁", + "📂", + "📃", + "📄", + "📅", + "📆", + "📇", + "📈", + "📉", + "📊", + "📋", + "📌", + "📍", + "📎", + "📏", + "📐", + "📑", + "📒", + "📓", + "📔", + "📕", + "📖", + "📗", + "📘", + "📙", + "📚", + "📛", + "📜", + "📝", + "📞", + "📟", + "📠", + "📡", + "📢", + "📣", + "📤", + "📥", + "📦", + "📧", + "📨", + "📩", + "📪", + "📫", + "📬", + "📭", + "📮", + "📯", + "📰", + "📱", + "📲", + "📳", + "📴", + "📵", + "📶", + "📷", + "📸", + "📹", + "📺", + "📻", + "📼", + "📽", + "📿", + "🔀", + "🔁", + "🔂", + "🔃", + "🔄", + "🔅", + "🔆", + "🔇", + "🔈", + "🔉", + "🔊", + "🔋", + "🔌", + "🔍", + "🔎", + "🔏", + "🔐", + "🔑", + "🔒", + "🔓", + "🔔", + "🔕", + "🔖", + "🔗", + "🔘", + "🔙", + "🔚", + "🔛", + "🔜", + "🔝", + "🔞", + "🔟", + "🔠", + "🔡", + "🔢", + "🔣", + "🔤", + "🔥", + "🔦", + "🔧", + "🔨", + "🔩", + "🔪", + "🔫", + "🔬", + "🔭", + "🔮", + "🔯", + "🔰", + "🔱", + "🔲", + "🔳", + "🔴", + "🔵", + "🔶", + "🔷", + "🔸", + "🔹", + "🔺", + "🔻", + "🔼", + "🔽", + "🕉", + "🕊", + "🕋", + "🕌", + "🕍", + "🕎", + "🕐", + "🕑", + "🕒", + "🕓", + "🕔", + "🕕", + "🕖", + "🕗", + "🕘", + "🕙", + "🕚", + "🕛", + "🕜", + "🕝", + "🕞", + "🕟", + "🕠", + "🕡", + "🕢", + "🕣", + "🕤", + "🕥", + "🕦", + "🕧", + "🕯", + "🕰", + "🕳", + "🕴🏻", + "🕴🏼", + "🕴🏽", + "🕴🏾", + "🕴🏿", + "🕴", + "🕵🏻‍♀️", + "🕵🏻‍♂️", + "🕵🏻", + "🕵🏼‍♀️", + "🕵🏼‍♂️", + "🕵🏼", + "🕵🏽‍♀️", + "🕵🏽‍♂️", + "🕵🏽", + "🕵🏾‍♀️", + "🕵🏾‍♂️", + "🕵🏾", + "🕵🏿‍♀️", + "🕵🏿‍♂️", + "🕵🏿", + "🕵️‍♀️", + "🕵️‍♂️", + "🕵", + "🕶", + "🕷", + "🕸", + "🕹", + "🕺🏻", + "🕺🏼", + "🕺🏽", + "🕺🏾", + "🕺🏿", + "🕺", + "🖇", + "🖊", + "🖋", + "🖌", + "🖍", + "🖐🏻", + "🖐🏼", + "🖐🏽", + "🖐🏾", + "🖐🏿", + "🖐", + "🖕🏻", + "🖕🏼", + "🖕🏽", + "🖕🏾", + "🖕🏿", + "🖕", + "🖖🏻", + "🖖🏼", + "🖖🏽", + "🖖🏾", + "🖖🏿", + "🖖", + "🖤", + "🖥", + "🖨", + "🖱", + "🖲", + "🖼", + "🗂", + "🗃", + "🗄", + "🗑", + "🗒", + "🗓", + "🗜", + "🗝", + "🗞", + "🗡", + "🗣", + "🗨", + "🗯", + "🗳", + "🗺", + "🗻", + "🗼", + "🗽", + "🗾", + "🗿", + "😀", + "😁", + "😂", + "😃", + "😄", + "😅", + "😆", + "😇", + "😈", + "😉", + "😊", + "😋", + "😌", + "😍", + "😎", + "😏", + "😐", + "😑", + "😒", + "😓", + "😔", + "😕", + "😖", + "😗", + "😘", + "😙", + "😚", + "😛", + "😜", + "😝", + "😞", + "😟", + "😠", + "😡", + "😢", + "😣", + "😤", + "😥", + "😦", + "😧", + "😨", + "😩", + "😪", + "😫", + "😬", + "😭", + "😮", + "😯", + "😰", + "😱", + "😲", + "😳", + "😴", + "😵", + "😶", + "😷", + "😸", + "😹", + "😺", + "😻", + "😼", + "😽", + "😾", + "😿", + "🙀", + "🙁", + "🙂", + "🙃", + "🙄", + "🙅🏻‍♀️", + "🙅🏻‍♂️", + "🙅🏻", + "🙅🏼‍♀️", + "🙅🏼‍♂️", + "🙅🏼", + "🙅🏽‍♀️", + "🙅🏽‍♂️", + "🙅🏽", + "🙅🏾‍♀️", + "🙅🏾‍♂️", + "🙅🏾", + "🙅🏿‍♀️", + "🙅🏿‍♂️", + "🙅🏿", + "🙅‍♀️", + "🙅‍♂️", + "🙅", + "🙆🏻‍♀️", + "🙆🏻‍♂️", + "🙆🏻", + "🙆🏼‍♀️", + "🙆🏼‍♂️", + "🙆🏼", + "🙆🏽‍♀️", + "🙆🏽‍♂️", + "🙆🏽", + "🙆🏾‍♀️", + "🙆🏾‍♂️", + "🙆🏾", + "🙆🏿‍♀️", + "🙆🏿‍♂️", + "🙆🏿", + "🙆‍♀️", + "🙆‍♂️", + "🙆", + "🙇🏻‍♀️", + "🙇🏻‍♂️", + "🙇🏻", + "🙇🏼‍♀️", + "🙇🏼‍♂️", + "🙇🏼", + "🙇🏽‍♀️", + "🙇🏽‍♂️", + "🙇🏽", + "🙇🏾‍♀️", + "🙇🏾‍♂️", + "🙇🏾", + "🙇🏿‍♀️", + "🙇🏿‍♂️", + "🙇🏿", + "🙇‍♀️", + "🙇‍♂️", + "🙇", + "🙈", + "🙉", + "🙊", + "🙋🏻‍♀️", + "🙋🏻‍♂️", + "🙋🏻", + "🙋🏼‍♀️", + "🙋🏼‍♂️", + "🙋🏼", + "🙋🏽‍♀️", + "🙋🏽‍♂️", + "🙋🏽", + "🙋🏾‍♀️", + "🙋🏾‍♂️", + "🙋🏾", + "🙋🏿‍♀️", + "🙋🏿‍♂️", + "🙋🏿", + "🙋‍♀️", + "🙋‍♂️", + "🙋", + "🙌🏻", + "🙌🏼", + "🙌🏽", + "🙌🏾", + "🙌🏿", + "🙌", + "🙍🏻‍♀️", + "🙍🏻‍♂️", + "🙍🏻", + "🙍🏼‍♀️", + "🙍🏼‍♂️", + "🙍🏼", + "🙍🏽‍♀️", + "🙍🏽‍♂️", + "🙍🏽", + "🙍🏾‍♀️", + "🙍🏾‍♂️", + "🙍🏾", + "🙍🏿‍♀️", + "🙍🏿‍♂️", + "🙍🏿", + "🙍‍♀️", + "🙍‍♂️", + "🙍", + "🙎🏻‍♀️", + "🙎🏻‍♂️", + "🙎🏻", + "🙎🏼‍♀️", + "🙎🏼‍♂️", + "🙎🏼", + "🙎🏽‍♀️", + "🙎🏽‍♂️", + "🙎🏽", + "🙎🏾‍♀️", + "🙎🏾‍♂️", + "🙎🏾", + "🙎🏿‍♀️", + "🙎🏿‍♂️", + "🙎🏿", + "🙎‍♀️", + "🙎‍♂️", + "🙎", + "🙏🏻", + "🙏🏼", + "🙏🏽", + "🙏🏾", + "🙏🏿", + "🙏", + "🚀", + "🚁", + "🚂", + "🚃", + "🚄", + "🚅", + "🚆", + "🚇", + "🚈", + "🚉", + "🚊", + "🚋", + "🚌", + "🚍", + "🚎", + "🚏", + "🚐", + "🚑", + "🚒", + "🚓", + "🚔", + "🚕", + "🚖", + "🚗", + "🚘", + "🚙", + "🚚", + "🚛", + "🚜", + "🚝", + "🚞", + "🚟", + "🚠", + "🚡", + "🚢", + "🚣🏻‍♀️", + "🚣🏻‍♂️", + "🚣🏻", + "🚣🏼‍♀️", + "🚣🏼‍♂️", + "🚣🏼", + "🚣🏽‍♀️", + "🚣🏽‍♂️", + "🚣🏽", + "🚣🏾‍♀️", + "🚣🏾‍♂️", + "🚣🏾", + "🚣🏿‍♀️", + "🚣🏿‍♂️", + "🚣🏿", + "🚣‍♀️", + "🚣‍♂️", + "🚣", + "🚤", + "🚥", + "🚦", + "🚧", + "🚨", + "🚩", + "🚪", + "🚫", + "🚬", + "🚭", + "🚮", + "🚯", + "🚰", + "🚱", + "🚲", + "🚳", + "🚴🏻‍♀️", + "🚴🏻‍♂️", + "🚴🏻", + "🚴🏼‍♀️", + "🚴🏼‍♂️", + "🚴🏼", + "🚴🏽‍♀️", + "🚴🏽‍♂️", + "🚴🏽", + "🚴🏾‍♀️", + "🚴🏾‍♂️", + "🚴🏾", + "🚴🏿‍♀️", + "🚴🏿‍♂️", + "🚴🏿", + "🚴‍♀️", + "🚴‍♂️", + "🚴", + "🚵🏻‍♀️", + "🚵🏻‍♂️", + "🚵🏻", + "🚵🏼‍♀️", + "🚵🏼‍♂️", + "🚵🏼", + "🚵🏽‍♀️", + "🚵🏽‍♂️", + "🚵🏽", + "🚵🏾‍♀️", + "🚵🏾‍♂️", + "🚵🏾", + "🚵🏿‍♀️", + "🚵🏿‍♂️", + "🚵🏿", + "🚵‍♀️", + "🚵‍♂️", + "🚵", + "🚶🏻‍♀️", + "🚶🏻‍♂️", + "🚶🏻", + "🚶🏼‍♀️", + "🚶🏼‍♂️", + "🚶🏼", + "🚶🏽‍♀️", + "🚶🏽‍♂️", + "🚶🏽", + "🚶🏾‍♀️", + "🚶🏾‍♂️", + "🚶🏾", + "🚶🏿‍♀️", + "🚶🏿‍♂️", + "🚶🏿", + "🚶‍♀️", + "🚶‍♂️", + "🚶", + "🚷", + "🚸", + "🚹", + "🚺", + "🚻", + "🚼", + "🚽", + "🚾", + "🚿", + "🛀🏻", + "🛀🏼", + "🛀🏽", + "🛀🏾", + "🛀🏿", + "🛀", + "🛁", + "🛂", + "🛃", + "🛄", + "🛅", + "🛋", + "🛌🏻", + "🛌🏼", + "🛌🏽", + "🛌🏾", + "🛌🏿", + "🛌", + "🛍", + "🛎", + "🛏", + "🛐", + "🛑", + "🛒", + "🛠", + "🛡", + "🛢", + "🛣", + "🛤", + "🛥", + "🛩", + "🛫", + "🛬", + "🛰", + "🛳", + "🛴", + "🛵", + "🛶", + "🤐", + "🤑", + "🤒", + "🤓", + "🤔", + "🤕", + "🤖", + "🤗", + "🤘🏻", + "🤘🏼", + "🤘🏽", + "🤘🏾", + "🤘🏿", + "🤘", + "🤙🏻", + "🤙🏼", + "🤙🏽", + "🤙🏾", + "🤙🏿", + "🤙", + "🤚🏻", + "🤚🏼", + "🤚🏽", + "🤚🏾", + "🤚🏿", + "🤚", + "🤛🏻", + "🤛🏼", + "🤛🏽", + "🤛🏾", + "🤛🏿", + "🤛", + "🤜🏻", + "🤜🏼", + "🤜🏽", + "🤜🏾", + "🤜🏿", + "🤜", + "🤝🏻", + "🤝🏼", + "🤝🏽", + "🤝🏾", + "🤝🏿", + "🤝", + "🤞🏻", + "🤞🏼", + "🤞🏽", + "🤞🏾", + "🤞🏿", + "🤞", + "🤠", + "🤡", + "🤢", + "🤣", + "🤤", + "🤥", + "🤦🏻‍♀️", + "🤦🏻‍♂️", + "🤦🏻", + "🤦🏼‍♀️", + "🤦🏼‍♂️", + "🤦🏼", + "🤦🏽‍♀️", + "🤦🏽‍♂️", + "🤦🏽", + "🤦🏾‍♀️", + "🤦🏾‍♂️", + "🤦🏾", + "🤦🏿‍♀️", + "🤦🏿‍♂️", + "🤦🏿", + "🤦‍♀️", + "🤦‍♂️", + "🤦", + "🤧", + "🤰🏻", + "🤰🏼", + "🤰🏽", + "🤰🏾", + "🤰🏿", + "🤰", + "🤳🏻", + "🤳🏼", + "🤳🏽", + "🤳🏾", + "🤳🏿", + "🤳", + "🤴🏻", + "🤴🏼", + "🤴🏽", + "🤴🏾", + "🤴🏿", + "🤴", + "🤵🏻", + "🤵🏼", + "🤵🏽", + "🤵🏾", + "🤵🏿", + "🤵", + "🤶🏻", + "🤶🏼", + "🤶🏽", + "🤶🏾", + "🤶🏿", + "🤶", + "🤷🏻‍♀️", + "🤷🏻‍♂️", + "🤷🏻", + "🤷🏼‍♀️", + "🤷🏼‍♂️", + "🤷🏼", + "🤷🏽‍♀️", + "🤷🏽‍♂️", + "🤷🏽", + "🤷🏾‍♀️", + "🤷🏾‍♂️", + "🤷🏾", + "🤷🏿‍♀️", + "🤷🏿‍♂️", + "🤷🏿", + "🤷‍♀️", + "🤷‍♂️", + "🤷", + "🤸🏻‍♀️", + "🤸🏻‍♂️", + "🤸🏻", + "🤸🏼‍♀️", + "🤸🏼‍♂️", + "🤸🏼", + "🤸🏽‍♀️", + "🤸🏽‍♂️", + "🤸🏽", + "🤸🏾‍♀️", + "🤸🏾‍♂️", + "🤸🏾", + "🤸🏿‍♀️", + "🤸🏿‍♂️", + "🤸🏿", + "🤸‍♀️", + "🤸‍♂️", + "🤸", + "🤹🏻‍♀️", + "🤹🏻‍♂️", + "🤹🏻", + "🤹🏼‍♀️", + "🤹🏼‍♂️", + "🤹🏼", + "🤹🏽‍♀️", + "🤹🏽‍♂️", + "🤹🏽", + "🤹🏾‍♀️", + "🤹🏾‍♂️", + "🤹🏾", + "🤹🏿‍♀️", + "🤹🏿‍♂️", + "🤹🏿", + "🤹‍♀️", + "🤹‍♂️", + "🤹", + "🤺", + "🤼🏻‍♀️", + "🤼🏻‍♂️", + "🤼🏻", + "🤼🏼‍♀️", + "🤼🏼‍♂️", + "🤼🏼", + "🤼🏽‍♀️", + "🤼🏽‍♂️", + "🤼🏽", + "🤼🏾‍♀️", + "🤼🏾‍♂️", + "🤼🏾", + "🤼🏿‍♀️", + "🤼🏿‍♂️", + "🤼🏿", + "🤼‍♀️", + "🤼‍♂️", + "🤼", + "🤽🏻‍♀️", + "🤽🏻‍♂️", + "🤽🏻", + "🤽🏼‍♀️", + "🤽🏼‍♂️", + "🤽🏼", + "🤽🏽‍♀️", + "🤽🏽‍♂️", + "🤽🏽", + "🤽🏾‍♀️", + "🤽🏾‍♂️", + "🤽🏾", + "🤽🏿‍♀️", + "🤽🏿‍♂️", + "🤽🏿", + "🤽‍♀️", + "🤽‍♂️", + "🤽", + "🤾🏻‍♀️", + "🤾🏻‍♂️", + "🤾🏻", + "🤾🏼‍♀️", + "🤾🏼‍♂️", + "🤾🏼", + "🤾🏽‍♀️", + "🤾🏽‍♂️", + "🤾🏽", + "🤾🏾‍♀️", + "🤾🏾‍♂️", + "🤾🏾", + "🤾🏿‍♀️", + "🤾🏿‍♂️", + "🤾🏿", + "🤾‍♀️", + "🤾‍♂️", + "🤾", + "🥀", + "🥁", + "🥂", + "🥃", + "🥄", + "🥅", + "🥇", + "🥈", + "🥉", + "🥊", + "🥋", + "🥐", + "🥑", + "🥒", + "🥓", + "🥔", + "🥕", + "🥖", + "🥗", + "🥘", + "🥙", + "🥚", + "🥛", + "🥜", + "🥝", + "🥞", + "🦀", + "🦁", + "🦂", + "🦃", + "🦄", + "🦅", + "🦆", + "🦇", + "🦈", + "🦉", + "🦊", + "🦋", + "🦌", + "🦍", + "🦎", + "🦏", + "🦐", + "🦑", + "🧀", + "‼", + "⁉", + "™", + "ℹ", + "↔", + "↕", + "↖", + "↗", + "↘", + "↙", + "↩", + "↪", + "#⃣", + "⌚", + "⌛", + "⌨", + "⏏", + "⏩", + "⏪", + "⏫", + "⏬", + "⏭", + "⏮", + "⏯", + "⏰", + "⏱", + "⏲", + "⏳", + "⏸", + "⏹", + "⏺", + "Ⓜ", + "▪", + "▫", + "▶", + "◀", + "◻", + "◼", + "◽", + "◾", + "☀", + "☁", + "☂", + "☃", + "☄", + "☎", + "☑", + "☔", + "☕", + "☘", + "☝🏻", + "☝🏼", + "☝🏽", + "☝🏾", + "☝🏿", + "☝", + "☠", + "☢", + "☣", + "☦", + "☪", + "☮", + "☯", + "☸", + "☹", + "☺", + "♀", + "♂", + "♈", + "♉", + "♊", + "♋", + "♌", + "♍", + "♎", + "♏", + "♐", + "♑", + "♒", + "♓", + "♠", + "♣", + "♥", + "♦", + "♨", + "♻", + "♿", + "⚒", + "⚓", + "⚔", + "⚕", + "⚖", + "⚗", + "⚙", + "⚛", + "⚜", + "⚠", + "⚡", + "⚪", + "⚫", + "⚰", + "⚱", + "⚽", + "⚾", + "⛄", + "⛅", + "⛈", + "⛎", + "⛏", + "⛑", + "⛓", + "⛔", + "⛩", + "⛪", + "⛰", + "⛱", + "⛲", + "⛳", + "⛴", + "⛵", + "⛷🏻", + "⛷🏼", + "⛷🏽", + "⛷🏾", + "⛷🏿", + "⛷", + "⛸", + "⛹🏻‍♀️", + "⛹🏻‍♂️", + "⛹🏻", + "⛹🏼‍♀️", + "⛹🏼‍♂️", + "⛹🏼", + "⛹🏽‍♀️", + "⛹🏽‍♂️", + "⛹🏽", + "⛹🏾‍♀️", + "⛹🏾‍♂️", + "⛹🏾", + "⛹🏿‍♀️", + "⛹🏿‍♂️", + "⛹🏿", + "⛹️‍♀️", + "⛹️‍♂️", + "⛹", + "⛺", + "⛽", + "✂", + "✅", + "✈", + "✉", + "✊🏻", + "✊🏼", + "✊🏽", + "✊🏾", + "✊🏿", + "✊", + "✋🏻", + "✋🏼", + "✋🏽", + "✋🏾", + "✋🏿", + "✋", + "✌🏻", + "✌🏼", + "✌🏽", + "✌🏾", + "✌🏿", + "✌", + "✍🏻", + "✍🏼", + "✍🏽", + "✍🏾", + "✍🏿", + "✍", + "✏", + "✒", + "✔", + "✖", + "✝", + "✡", + "✨", + "✳", + "✴", + "❄", + "❇", + "❌", + "❎", + "❓", + "❔", + "❕", + "❗", + "❣", + "❤", + "➕", + "➖", + "➗", + "➡", + "➰", + "➿", + "⤴", + "⤵", + "*⃣", + "⬅", + "⬆", + "⬇", + "⬛", + "⬜", + "⭐", + "⭕", + "0⃣", + "〰", + "〽", + "1⃣", + "2⃣", + "㊗", + "㊙", + "3⃣", + "4⃣", + "5⃣", + "6⃣", + "7⃣", + "8⃣", + "9⃣", + "©", + "®", + "" +]; + +frappe.ui.emoji_keywords = [ + ":mahjong:", + ":black_joker:", + ":a:", + ":b:", + ":o2:", + ":parking:", + ":ab:", + ":cl:", + ":cool:", + ":free:", + ":id:", + ":new:", + ":ng:", + ":ok:", + ":sos:", + ":up:", + ":vs:", + ":flag_ac:", + ":flag_ad:", + ":flag_ae:", + ":flag_af:", + ":flag_ag:", + ":flag_ai:", + ":flag_al:", + ":flag_am:", + ":flag_ao:", + ":flag_aq:", + ":flag_ar:", + ":flag_as:", + ":flag_at:", + ":flag_au:", + ":flag_aw:", + ":flag_ax:", + ":flag_az:", + ":regional_indicator_a:", + ":flag_ba:", + ":flag_bb:", + ":flag_bd:", + ":flag_be:", + ":flag_bf:", + ":flag_bg:", + ":flag_bh:", + ":flag_bi:", + ":flag_bj:", + ":flag_bl:", + ":flag_bm:", + ":flag_bn:", + ":flag_bo:", + ":flag_bq:", + ":flag_br:", + ":flag_bs:", + ":flag_bt:", + ":flag_bv:", + ":flag_bw:", + ":flag_by:", + ":flag_bz:", + ":regional_indicator_b:", + ":flag_ca:", + ":flag_cc:", + ":flag_cd:", + ":flag_cf:", + ":flag_cg:", + ":flag_ch:", + ":flag_ci:", + ":flag_ck:", + ":flag_cl:", + ":flag_cm:", + ":flag_cn:", + ":flag_co:", + ":flag_cp:", + ":flag_cr:", + ":flag_cu:", + ":flag_cv:", + ":flag_cw:", + ":flag_cx:", + ":flag_cy:", + ":flag_cz:", + ":regional_indicator_c:", + ":flag_de:", + ":flag_dg:", + ":flag_dj:", + ":flag_dk:", + ":flag_dm:", + ":flag_do:", + ":flag_dz:", + ":regional_indicator_d:", + ":flag_ea:", + ":flag_ec:", + ":flag_ee:", + ":flag_eg:", + ":flag_eh:", + ":flag_er:", + ":flag_es:", + ":flag_et:", + ":flag_eu:", + ":regional_indicator_e:", + ":flag_fi:", + ":flag_fj:", + ":flag_fk:", + ":flag_fm:", + ":flag_fo:", + ":flag_fr:", + ":regional_indicator_f:", + ":flag_ga:", + ":flag_gb:", + ":flag_gd:", + ":flag_ge:", + ":flag_gf:", + ":flag_gg:", + ":flag_gh:", + ":flag_gi:", + ":flag_gl:", + ":flag_gm:", + ":flag_gn:", + ":flag_gp:", + ":flag_gq:", + ":flag_gr:", + ":flag_gs:", + ":flag_gt:", + ":flag_gu:", + ":flag_gw:", + ":flag_gy:", + ":regional_indicator_g:", + ":flag_hk:", + ":flag_hm:", + ":flag_hn:", + ":flag_hr:", + ":flag_ht:", + ":flag_hu:", + ":regional_indicator_h:", + ":flag_ic:", + ":flag_id:", + ":flag_ie:", + ":flag_il:", + ":flag_im:", + ":flag_in:", + ":flag_io:", + ":flag_iq:", + ":flag_ir:", + ":flag_is:", + ":flag_it:", + ":regional_indicator_i:", + ":flag_je:", + ":flag_jm:", + ":flag_jo:", + ":flag_jp:", + ":regional_indicator_j:", + ":flag_ke:", + ":flag_kg:", + ":flag_kh:", + ":flag_ki:", + ":flag_km:", + ":flag_kn:", + ":flag_kp:", + ":flag_kr:", + ":flag_kw:", + ":flag_ky:", + ":flag_kz:", + ":regional_indicator_k:", + ":flag_la:", + ":flag_lb:", + ":flag_lc:", + ":flag_li:", + ":flag_lk:", + ":flag_lr:", + ":flag_ls:", + ":flag_lt:", + ":flag_lu:", + ":flag_lv:", + ":flag_ly:", + ":regional_indicator_l:", + ":flag_ma:", + ":flag_mc:", + ":flag_md:", + ":flag_me:", + ":flag_mf:", + ":flag_mg:", + ":flag_mh:", + ":flag_mk:", + ":flag_ml:", + ":flag_mm:", + ":flag_mn:", + ":flag_mo:", + ":flag_mp:", + ":flag_mq:", + ":flag_mr:", + ":flag_ms:", + ":flag_mt:", + ":flag_mu:", + ":flag_mv:", + ":flag_mw:", + ":flag_mx:", + ":flag_my:", + ":flag_mz:", + ":regional_indicator_m:", + ":flag_na:", + ":flag_nc:", + ":flag_ne:", + ":flag_nf:", + ":flag_ng:", + ":flag_ni:", + ":flag_nl:", + ":flag_no:", + ":flag_np:", + ":flag_nr:", + ":flag_nu:", + ":flag_nz:", + ":regional_indicator_n:", + ":flag_om:", + ":regional_indicator_o:", + ":flag_pa:", + ":flag_pe:", + ":flag_pf:", + ":flag_pg:", + ":flag_ph:", + ":flag_pk:", + ":flag_pl:", + ":flag_pm:", + ":flag_pn:", + ":flag_pr:", + ":flag_ps:", + ":flag_pt:", + ":flag_pw:", + ":flag_py:", + ":regional_indicator_p:", + ":flag_qa:", + ":regional_indicator_q:", + ":flag_re:", + ":flag_ro:", + ":flag_rs:", + ":flag_ru:", + ":flag_rw:", + ":regional_indicator_r:", + ":flag_sa:", + ":flag_sb:", + ":flag_sc:", + ":flag_sd:", + ":flag_se:", + ":flag_sg:", + ":flag_sh:", + ":flag_si:", + ":flag_sj:", + ":flag_sk:", + ":flag_sl:", + ":flag_sm:", + ":flag_sn:", + ":flag_so:", + ":flag_sr:", + ":flag_ss:", + ":flag_st:", + ":flag_sv:", + ":flag_sx:", + ":flag_sy:", + ":flag_sz:", + ":regional_indicator_s:", + ":flag_ta:", + ":flag_tc:", + ":flag_td:", + ":flag_tf:", + ":flag_tg:", + ":flag_th:", + ":flag_tj:", + ":flag_tk:", + ":flag_tl:", + ":flag_tm:", + ":flag_tn:", + ":flag_to:", + ":flag_tr:", + ":flag_tt:", + ":flag_tv:", + ":flag_tw:", + ":flag_tz:", + ":regional_indicator_t:", + ":flag_ua:", + ":flag_ug:", + ":flag_um:", + ":regional_indicator_u::regional_indicator_n:", + ":flag_us:", + ":flag_uy:", + ":flag_uz:", + ":regional_indicator_u:", + ":flag_va:", + ":flag_vc:", + ":flag_ve:", + ":flag_vg:", + ":flag_vi:", + ":flag_vn:", + ":flag_vu:", + ":regional_indicator_v:", + ":flag_wf:", + ":flag_ws:", + ":regional_indicator_w:", + ":flag_xk:", + ":regional_indicator_x:", + ":flag_ye:", + ":flag_yt:", + ":regional_indicator_y:", + ":flag_za:", + ":flag_zm:", + ":flag_zw:", + ":regional_indicator_z:", + ":koko:", + ":sa:", + ":u7121:", + ":u6307:", + ":u7981:", + ":u7a7a:", + ":u5408:", + ":u6e80:", + ":u6709:", + ":u6708:", + ":u7533:", + ":u5272:", + ":u55b6:", + ":ideograph_advantage:", + ":accept:", + ":cyclone:", + ":foggy:", + ":closed_umbrella:", + ":night_with_stars:", + ":sunrise_over_mountains:", + ":sunrise:", + ":city_dusk:", + ":city_sunset:", + ":rainbow:", + ":bridge_at_night:", + ":ocean:", + ":volcano:", + ":milky_way:", + ":earth_africa:", + ":earth_americas:", + ":earth_asia:", + ":globe_with_meridians:", + ":new_moon:", + ":waxing_crescent_moon:", + ":first_quarter_moon:", + ":waxing_gibbous_moon:", + ":full_moon:", + ":waning_gibbous_moon:", + ":last_quarter_moon:", + ":waning_crescent_moon:", + ":crescent_moon:", + ":new_moon_with_face:", + ":first_quarter_moon_with_face:", + ":last_quarter_moon_with_face:", + ":full_moon_with_face:", + ":sun_with_face:", + ":star2:", + ":stars:", + ":thermometer:", + ":white_sun_small_cloud:", + ":white_sun_cloud:", + ":white_sun_rain_cloud:", + ":cloud_rain:", + ":cloud_snow:", + ":cloud_lightning:", + ":cloud_tornado:", + ":fog:", + ":wind_blowing_face:", + ":hotdog:", + ":taco:", + ":burrito:", + ":chestnut:", + ":seedling:", + ":evergreen_tree:", + ":deciduous_tree:", + ":palm_tree:", + ":cactus:", + ":hot_pepper:", + ":tulip:", + ":cherry_blossom:", + ":rose:", + ":hibiscus:", + ":sunflower:", + ":blossom:", + ":corn:", + ":ear_of_rice:", + ":herb:", + ":four_leaf_clover:", + ":maple_leaf:", + ":fallen_leaf:", + ":leaves:", + ":mushroom:", + ":tomato:", + ":eggplant:", + ":grapes:", + ":melon:", + ":watermelon:", + ":tangerine:", + ":lemon:", + ":banana:", + ":pineapple:", + ":apple:", + ":green_apple:", + ":pear:", + ":peach:", + ":cherries:", + ":strawberry:", + ":hamburger:", + ":pizza:", + ":meat_on_bone:", + ":poultry_leg:", + ":rice_cracker:", + ":rice_ball:", + ":rice:", + ":curry:", + ":ramen:", + ":spaghetti:", + ":bread:", + ":fries:", + ":sweet_potato:", + ":dango:", + ":oden:", + ":sushi:", + ":fried_shrimp:", + ":fish_cake:", + ":icecream:", + ":shaved_ice:", + ":ice_cream:", + ":doughnut:", + ":cookie:", + ":chocolate_bar:", + ":candy:", + ":lollipop:", + ":custard:", + ":honey_pot:", + ":cake:", + ":bento:", + ":stew:", + ":cooking:", + ":fork_and_knife:", + ":tea:", + ":sake:", + ":wine_glass:", + ":cocktail:", + ":tropical_drink:", + ":beer:", + ":beers:", + ":baby_bottle:", + ":fork_knife_plate:", + ":champagne:", + ":popcorn:", + ":ribbon:", + ":gift:", + ":birthday:", + ":jack_o_lantern:", + ":christmas_tree:", + ":santa_tone1:", + ":santa_tone2:", + ":santa_tone3:", + ":santa_tone4:", + ":santa_tone5:", + ":santa:", + ":fireworks:", + ":sparkler:", + ":balloon:", + ":tada:", + ":confetti_ball:", + ":tanabata_tree:", + ":crossed_flags:", + ":bamboo:", + ":dolls:", + ":flags:", + ":wind_chime:", + ":rice_scene:", + ":school_satchel:", + ":mortar_board:", + ":military_medal:", + ":reminder_ribbon:", + ":microphone2:", + ":level_slider:", + ":control_knobs:", + ":film_frames:", + ":tickets:", + ":carousel_horse:", + ":ferris_wheel:", + ":roller_coaster:", + ":fishing_pole_and_fish:", + ":microphone:", + ":movie_camera:", + ":cinema:", + ":headphones:", + ":art:", + ":tophat:", + ":circus_tent:", + ":ticket:", + ":clapper:", + ":performing_arts:", + ":video_game:", + ":dart:", + ":slot_machine:", + ":8ball:", + ":game_die:", + ":bowling:", + ":flower_playing_cards:", + ":musical_note:", + ":notes:", + ":saxophone:", + ":guitar:", + ":musical_keyboard:", + ":trumpet:", + ":violin:", + ":musical_score:", + ":running_shirt_with_sash:", + ":tennis:", + ":ski:", + ":basketball:", + ":checkered_flag:", + ":snowboarder::tone1:", + ":snowboarder::tone2:", + ":snowboarder::tone3:", + ":snowboarder::tone4:", + ":snowboarder::tone5:", + ":snowboarder:", + ":runner_tone1:‍♀️", + ":runner_tone1:‍♂️", + ":runner_tone1:", + ":runner_tone2:‍♀️", + ":runner_tone2:‍♂️", + ":runner_tone2:", + ":runner_tone3:‍♀️", + ":runner_tone3:‍♂️", + ":runner_tone3:", + ":runner_tone4:‍♀️", + ":runner_tone4:‍♂️", + ":runner_tone4:", + ":runner_tone5:‍♀️", + ":runner_tone5:‍♂️", + ":runner_tone5:", + ":runner:‍♀️", + ":runner:‍♂️", + ":runner:", + ":surfer_tone1:‍♀️", + ":surfer_tone1:‍♂️", + ":surfer_tone1:", + ":surfer_tone2:‍♀️", + ":surfer_tone2:‍♂️", + ":surfer_tone2:", + ":surfer_tone3:‍♀️", + ":surfer_tone3:‍♂️", + ":surfer_tone3:", + ":surfer_tone4:‍♀️", + ":surfer_tone4:‍♂️", + ":surfer_tone4:", + ":surfer_tone5:‍♀️", + ":surfer_tone5:‍♂️", + ":surfer_tone5:", + ":surfer:‍♀️", + ":surfer:‍♂️", + ":surfer:", + ":medal:", + ":trophy:", + ":horse_racing_tone1:", + ":horse_racing_tone2:", + ":horse_racing_tone3:", + ":horse_racing_tone4:", + ":horse_racing_tone5:", + ":horse_racing:", + ":football:", + ":rugby_football:", + ":swimmer_tone1:‍♀️", + ":swimmer_tone1:‍♂️", + ":swimmer_tone1:", + ":swimmer_tone2:‍♀️", + ":swimmer_tone2:‍♂️", + ":swimmer_tone2:", + ":swimmer_tone3:‍♀️", + ":swimmer_tone3:‍♂️", + ":swimmer_tone3:", + ":swimmer_tone4:‍♀️", + ":swimmer_tone4:‍♂️", + ":swimmer_tone4:", + ":swimmer_tone5:‍♀️", + ":swimmer_tone5:‍♂️", + ":swimmer_tone5:", + ":swimmer:‍♀️", + ":swimmer:‍♂️", + ":swimmer:", + ":lifter_tone1:‍♀️", + ":lifter_tone1:‍♂️", + ":lifter_tone1:", + ":lifter_tone2:‍♀️", + ":lifter_tone2:‍♂️", + ":lifter_tone2:", + ":lifter_tone3:‍♀️", + ":lifter_tone3:‍♂️", + ":lifter_tone3:", + ":lifter_tone4:‍♀️", + ":lifter_tone4:‍♂️", + ":lifter_tone4:", + ":lifter_tone5:‍♀️", + ":lifter_tone5:‍♂️", + ":lifter_tone5:", + ":lifter:‍♀️", + ":lifter:‍♂️", + ":lifter:", + ":golfer::tone1:‍♀️", + ":golfer::tone1:‍♂️", + ":golfer::tone1:", + ":golfer::tone2:‍♀️", + ":golfer::tone2:‍♂️", + ":golfer::tone2:", + ":golfer::tone3:‍♀️", + ":golfer::tone3:‍♂️", + ":golfer::tone3:", + ":golfer::tone4:‍♀️", + ":golfer::tone4:‍♂️", + ":golfer::tone4:", + ":golfer::tone5:‍♀️", + ":golfer::tone5:‍♂️", + ":golfer::tone5:", + ":golfer:‍♀️", + ":golfer:‍♂️", + ":golfer:", + ":motorcycle:", + ":race_car:", + ":cricket:", + ":volleyball:", + ":field_hockey:", + ":hockey:", + ":ping_pong:", + ":mountain_snow:", + ":camping:", + ":beach:", + ":construction_site:", + ":homes:", + ":cityscape:", + ":house_abandoned:", + ":classical_building:", + ":desert:", + ":island:", + ":park:", + ":stadium:", + ":house:", + ":house_with_garden:", + ":office:", + ":post_office:", + ":european_post_office:", + ":hospital:", + ":bank:", + ":atm:", + ":hotel:", + ":love_hotel:", + ":convenience_store:", + ":school:", + ":department_store:", + ":factory:", + ":izakaya_lantern:", + ":japanese_castle:", + ":european_castle:", + ":flag_white:‍:rainbow:", + ":flag_white:", + ":flag_black:‍:skull_crossbones:", + ":flag_black:", + ":rosette:", + ":label:", + ":badminton:", + ":bow_and_arrow:", + ":amphora:", + ":tone1:", + ":tone2:", + ":tone3:", + ":tone4:", + ":tone5:", + ":rat:", + ":mouse2:", + ":ox:", + ":water_buffalo:", + ":cow2:", + ":tiger2:", + ":leopard:", + ":rabbit2:", + ":cat2:", + ":dragon:", + ":crocodile:", + ":whale2:", + ":snail:", + ":snake:", + ":racehorse:", + ":ram:", + ":goat:", + ":sheep:", + ":monkey:", + ":rooster:", + ":chicken:", + ":dog2:", + ":pig2:", + ":boar:", + ":elephant:", + ":octopus:", + ":shell:", + ":bug:", + ":ant:", + ":bee:", + ":beetle:", + ":fish:", + ":tropical_fish:", + ":blowfish:", + ":turtle:", + ":hatching_chick:", + ":baby_chick:", + ":hatched_chick:", + ":bird:", + ":penguin:", + ":koala:", + ":poodle:", + ":dromedary_camel:", + ":camel:", + ":dolphin:", + ":mouse:", + ":cow:", + ":tiger:", + ":rabbit:", + ":cat:", + ":dragon_face:", + ":whale:", + ":horse:", + ":monkey_face:", + ":dog:", + ":pig:", + ":frog:", + ":hamster:", + ":wolf:", + ":bear:", + ":panda_face:", + ":pig_nose:", + ":feet:", + ":chipmunk:", + ":eyes:", + ":eye_in_speech_bubble:", + ":eye:", + ":ear_tone1:", + ":ear_tone2:", + ":ear_tone3:", + ":ear_tone4:", + ":ear_tone5:", + ":ear:", + ":nose_tone1:", + ":nose_tone2:", + ":nose_tone3:", + ":nose_tone4:", + ":nose_tone5:", + ":nose:", + ":lips:", + ":tongue:", + ":point_up_2_tone1:", + ":point_up_2_tone2:", + ":point_up_2_tone3:", + ":point_up_2_tone4:", + ":point_up_2_tone5:", + ":point_up_2:", + ":point_down_tone1:", + ":point_down_tone2:", + ":point_down_tone3:", + ":point_down_tone4:", + ":point_down_tone5:", + ":point_down:", + ":point_left_tone1:", + ":point_left_tone2:", + ":point_left_tone3:", + ":point_left_tone4:", + ":point_left_tone5:", + ":point_left:", + ":point_right_tone1:", + ":point_right_tone2:", + ":point_right_tone3:", + ":point_right_tone4:", + ":point_right_tone5:", + ":point_right:", + ":punch_tone1:", + ":punch_tone2:", + ":punch_tone3:", + ":punch_tone4:", + ":punch_tone5:", + ":punch:", + ":wave_tone1:", + ":wave_tone2:", + ":wave_tone3:", + ":wave_tone4:", + ":wave_tone5:", + ":wave:", + ":ok_hand_tone1:", + ":ok_hand_tone2:", + ":ok_hand_tone3:", + ":ok_hand_tone4:", + ":ok_hand_tone5:", + ":ok_hand:", + ":thumbsup_tone1:", + ":thumbsup_tone2:", + ":thumbsup_tone3:", + ":thumbsup_tone4:", + ":thumbsup_tone5:", + ":thumbsup:", + ":thumbsdown_tone1:", + ":thumbsdown_tone2:", + ":thumbsdown_tone3:", + ":thumbsdown_tone4:", + ":thumbsdown_tone5:", + ":thumbsdown:", + ":clap_tone1:", + ":clap_tone2:", + ":clap_tone3:", + ":clap_tone4:", + ":clap_tone5:", + ":clap:", + ":open_hands_tone1:", + ":open_hands_tone2:", + ":open_hands_tone3:", + ":open_hands_tone4:", + ":open_hands_tone5:", + ":open_hands:", + ":crown:", + ":womans_hat:", + ":eyeglasses:", + ":necktie:", + ":shirt:", + ":jeans:", + ":dress:", + ":kimono:", + ":bikini:", + ":womans_clothes:", + ":purse:", + ":handbag:", + ":pouch:", + ":mans_shoe:", + ":athletic_shoe:", + ":high_heel:", + ":sandal:", + ":boot:", + ":footprints:", + ":bust_in_silhouette:", + ":busts_in_silhouette:", + ":boy_tone1:", + ":boy_tone2:", + ":boy_tone3:", + ":boy_tone4:", + ":boy_tone5:", + ":boy:", + ":girl_tone1:", + ":girl_tone2:", + ":girl_tone3:", + ":girl_tone4:", + ":girl_tone5:", + ":girl:", + ":man_tone1:‍:ear_of_rice:", + ":man_tone1:‍:cooking:", + ":man_tone1:‍:mortar_board:", + ":man_tone1:‍:microphone:", + ":man_tone1:‍:art:", + ":man_tone1:‍:school:", + ":man_tone1:‍:factory:", + ":man_tone1:‍:computer:", + ":man_tone1:‍:briefcase:", + ":man_tone1:‍:wrench:", + ":man_tone1:‍:microscope:", + ":man_tone1:‍:rocket:", + ":man_tone1:‍:fire_engine:", + ":man_tone1:‍⚕️", + ":man_tone1:‍:scales:", + ":man_tone1:‍:airplane:", + ":man_tone1:", + ":man_tone2:‍:ear_of_rice:", + ":man_tone2:‍:cooking:", + ":man_tone2:‍:mortar_board:", + ":man_tone2:‍:microphone:", + ":man_tone2:‍:art:", + ":man_tone2:‍:school:", + ":man_tone2:‍:factory:", + ":man_tone2:‍:computer:", + ":man_tone2:‍:briefcase:", + ":man_tone2:‍:wrench:", + ":man_tone2:‍:microscope:", + ":man_tone2:‍:rocket:", + ":man_tone2:‍:fire_engine:", + ":man_tone2:‍⚕️", + ":man_tone2:‍:scales:", + ":man_tone2:‍:airplane:", + ":man_tone2:", + ":man_tone3:‍:ear_of_rice:", + ":man_tone3:‍:cooking:", + ":man_tone3:‍:mortar_board:", + ":man_tone3:‍:microphone:", + ":man_tone3:‍:art:", + ":man_tone3:‍:school:", + ":man_tone3:‍:factory:", + ":man_tone3:‍:computer:", + ":man_tone3:‍:briefcase:", + ":man_tone3:‍:wrench:", + ":man_tone3:‍:microscope:", + ":man_tone3:‍:rocket:", + ":man_tone3:‍:fire_engine:", + ":man_tone3:‍⚕️", + ":man_tone3:‍:scales:", + ":man_tone3:‍:airplane:", + ":man_tone3:", + ":man_tone4:‍:ear_of_rice:", + ":man_tone4:‍:cooking:", + ":man_tone4:‍:mortar_board:", + ":man_tone4:‍:microphone:", + ":man_tone4:‍:art:", + ":man_tone4:‍:school:", + ":man_tone4:‍:factory:", + ":man_tone4:‍:computer:", + ":man_tone4:‍:briefcase:", + ":man_tone4:‍:wrench:", + ":man_tone4:‍:microscope:", + ":man_tone4:‍:rocket:", + ":man_tone4:‍:fire_engine:", + ":man_tone4:‍⚕️", + ":man_tone4:‍:scales:", + ":man_tone4:‍:airplane:", + ":man_tone4:", + ":man_tone5:‍:ear_of_rice:", + ":man_tone5:‍:cooking:", + ":man_tone5:‍:mortar_board:", + ":man_tone5:‍:microphone:", + ":man_tone5:‍:art:", + ":man_tone5:‍:school:", + ":man_tone5:‍:factory:", + ":man_tone5:‍:computer:", + ":man_tone5:‍:briefcase:", + ":man_tone5:‍:wrench:", + ":man_tone5:‍:microscope:", + ":man_tone5:‍:rocket:", + ":man_tone5:‍:fire_engine:", + ":man_tone5:‍⚕️", + ":man_tone5:‍:scales:", + ":man_tone5:‍:airplane:", + ":man_tone5:", + ":man:‍:ear_of_rice:", + ":man:‍:cooking:", + ":man:‍:mortar_board:", + ":man:‍:microphone:", + ":man:‍:art:", + ":man:‍:school:", + ":man:‍:factory:", + ":man:‍:boy:‍:boy:", + ":man:‍:boy:", + ":man:‍:girl:‍:boy:", + ":man:‍:girl:‍:girl:", + ":man:‍:girl:", + ":family_mmbb:", + ":family_mmb:", + ":family_mmgb:", + ":family_mmgg:", + ":family_mmg:", + ":family_mwbb:", + ":man:‍:woman:‍:boy:", + ":family_mwgb:", + ":family_mwgg:", + ":family_mwg:", + ":man:‍:computer:", + ":man:‍:briefcase:", + ":man:‍:wrench:", + ":man:‍:microscope:", + ":man:‍:rocket:", + ":man:‍:fire_engine:", + ":man:‍⚕️", + ":man:‍:scales:", + ":man:‍:airplane:", + ":couple_mm:", + ":kiss_mm:", + ":man:", + ":woman_tone1:‍:ear_of_rice:", + ":woman_tone1:‍:cooking:", + ":woman_tone1:‍:mortar_board:", + ":woman_tone1:‍:microphone:", + ":woman_tone1:‍:art:", + ":woman_tone1:‍:school:", + ":woman_tone1:‍:factory:", + ":woman_tone1:‍:computer:", + ":woman_tone1:‍:briefcase:", + ":woman_tone1:‍:wrench:", + ":woman_tone1:‍:microscope:", + ":woman_tone1:‍:rocket:", + ":woman_tone1:‍:fire_engine:", + ":woman_tone1:‍⚕️", + ":woman_tone1:‍:scales:", + ":woman_tone1:‍:airplane:", + ":woman_tone1:", + ":woman_tone2:‍:ear_of_rice:", + ":woman_tone2:‍:cooking:", + ":woman_tone2:‍:mortar_board:", + ":woman_tone2:‍:microphone:", + ":woman_tone2:‍:art:", + ":woman_tone2:‍:school:", + ":woman_tone2:‍:factory:", + ":woman_tone2:‍:computer:", + ":woman_tone2:‍:briefcase:", + ":woman_tone2:‍:wrench:", + ":woman_tone2:‍:microscope:", + ":woman_tone2:‍:rocket:", + ":woman_tone2:‍:fire_engine:", + ":woman_tone2:‍⚕️", + ":woman_tone2:‍:scales:", + ":woman_tone2:‍:airplane:", + ":woman_tone2:", + ":woman_tone3:‍:ear_of_rice:", + ":woman_tone3:‍:cooking:", + ":woman_tone3:‍:mortar_board:", + ":woman_tone3:‍:microphone:", + ":woman_tone3:‍:art:", + ":woman_tone3:‍:school:", + ":woman_tone3:‍:factory:", + ":woman_tone3:‍:computer:", + ":woman_tone3:‍:briefcase:", + ":woman_tone3:‍:wrench:", + ":woman_tone3:‍:microscope:", + ":woman_tone3:‍:rocket:", + ":woman_tone3:‍:fire_engine:", + ":woman_tone3:‍⚕️", + ":woman_tone3:‍:scales:", + ":woman_tone3:‍:airplane:", + ":woman_tone3:", + ":woman_tone4:‍:ear_of_rice:", + ":woman_tone4:‍:cooking:", + ":woman_tone4:‍:mortar_board:", + ":woman_tone4:‍:microphone:", + ":woman_tone4:‍:art:", + ":woman_tone4:‍:school:", + ":woman_tone4:‍:factory:", + ":woman_tone4:‍:computer:", + ":woman_tone4:‍:briefcase:", + ":woman_tone4:‍:wrench:", + ":woman_tone4:‍:microscope:", + ":woman_tone4:‍:rocket:", + ":woman_tone4:‍:fire_engine:", + ":woman_tone4:‍⚕️", + ":woman_tone4:‍:scales:", + ":woman_tone4:‍:airplane:", + ":woman_tone4:", + ":woman_tone5:‍:ear_of_rice:", + ":woman_tone5:‍:cooking:", + ":woman_tone5:‍:mortar_board:", + ":woman_tone5:‍:microphone:", + ":woman_tone5:‍:art:", + ":woman_tone5:‍:school:", + ":woman_tone5:‍:factory:", + ":woman_tone5:‍:computer:", + ":woman_tone5:‍:briefcase:", + ":woman_tone5:‍:wrench:", + ":woman_tone5:‍:microscope:", + ":woman_tone5:‍:rocket:", + ":woman_tone5:‍:fire_engine:", + ":woman_tone5:‍⚕️", + ":woman_tone5:‍:scales:", + ":woman_tone5:‍:airplane:", + ":woman_tone5:", + ":woman:‍:ear_of_rice:", + ":woman:‍:cooking:", + ":woman:‍:mortar_board:", + ":woman:‍:microphone:", + ":woman:‍:art:", + ":woman:‍:school:", + ":woman:‍:factory:", + ":woman:‍:boy:‍:boy:", + ":woman:‍:boy:", + ":woman:‍:girl:‍:boy:", + ":woman:‍:girl:‍:girl:", + ":woman:‍:girl:", + ":family_wwbb:", + ":family_wwb:", + ":family_wwgb:", + ":family_wwgg:", + ":family_wwg:", + ":woman:‍:computer:", + ":woman:‍:briefcase:", + ":woman:‍:wrench:", + ":woman:‍:microscope:", + ":woman:‍:rocket:", + ":woman:‍:fire_engine:", + ":woman:‍⚕️", + ":woman:‍:scales:", + ":woman:‍:airplane:", + ":woman:‍:heart:‍:man:", + ":couple_ww:", + ":woman:‍:heart:‍:kiss:‍:man:", + ":kiss_ww:", + ":woman:", + ":family::tone1:", + ":family::tone2:", + ":family::tone3:", + ":family::tone4:", + ":family::tone5:", + ":family:", + ":couple::tone1:", + ":couple::tone2:", + ":couple::tone3:", + ":couple::tone4:", + ":couple::tone5:", + ":couple:", + ":two_men_holding_hands::tone1:", + ":two_men_holding_hands::tone2:", + ":two_men_holding_hands::tone3:", + ":two_men_holding_hands::tone4:", + ":two_men_holding_hands::tone5:", + ":two_men_holding_hands:", + ":two_women_holding_hands::tone1:", + ":two_women_holding_hands::tone2:", + ":two_women_holding_hands::tone3:", + ":two_women_holding_hands::tone4:", + ":two_women_holding_hands::tone5:", + ":two_women_holding_hands:", + ":cop_tone1:‍♀️", + ":cop_tone1:‍♂️", + ":cop_tone1:", + ":cop_tone2:‍♀️", + ":cop_tone2:‍♂️", + ":cop_tone2:", + ":cop_tone3:‍♀️", + ":cop_tone3:‍♂️", + ":cop_tone3:", + ":cop_tone4:‍♀️", + ":cop_tone4:‍♂️", + ":cop_tone4:", + ":cop_tone5:‍♀️", + ":cop_tone5:‍♂️", + ":cop_tone5:", + ":cop:‍♀️", + ":cop:‍♂️", + ":cop:", + ":dancers::tone1:‍♀️", + ":dancers::tone1:‍♂️", + ":dancers::tone1:", + ":dancers::tone2:‍♀️", + ":dancers::tone2:‍♂️", + ":dancers::tone2:", + ":dancers::tone3:‍♀️", + ":dancers::tone3:‍♂️", + ":dancers::tone3:", + ":dancers::tone4:‍♀️", + ":dancers::tone4:‍♂️", + ":dancers::tone4:", + ":dancers::tone5:‍♀️", + ":dancers::tone5:‍♂️", + ":dancers::tone5:", + ":dancers:‍♀️", + ":dancers:‍♂️", + ":dancers:", + ":bride_with_veil_tone1:", + ":bride_with_veil_tone2:", + ":bride_with_veil_tone3:", + ":bride_with_veil_tone4:", + ":bride_with_veil_tone5:", + ":bride_with_veil:", + ":person_with_blond_hair_tone1:‍♀️", + ":person_with_blond_hair_tone1:‍♂️", + ":person_with_blond_hair_tone1:", + ":person_with_blond_hair_tone2:‍♀️", + ":person_with_blond_hair_tone2:‍♂️", + ":person_with_blond_hair_tone2:", + ":person_with_blond_hair_tone3:‍♀️", + ":person_with_blond_hair_tone3:‍♂️", + ":person_with_blond_hair_tone3:", + ":person_with_blond_hair_tone4:‍♀️", + ":person_with_blond_hair_tone4:‍♂️", + ":person_with_blond_hair_tone4:", + ":person_with_blond_hair_tone5:‍♀️", + ":person_with_blond_hair_tone5:‍♂️", + ":person_with_blond_hair_tone5:", + ":person_with_blond_hair:‍♀️", + ":person_with_blond_hair:‍♂️", + ":person_with_blond_hair:", + ":man_with_gua_pi_mao_tone1:", + ":man_with_gua_pi_mao_tone2:", + ":man_with_gua_pi_mao_tone3:", + ":man_with_gua_pi_mao_tone4:", + ":man_with_gua_pi_mao_tone5:", + ":man_with_gua_pi_mao:", + ":man_with_turban_tone1:‍♀️", + ":man_with_turban_tone1:‍♂️", + ":man_with_turban_tone1:", + ":man_with_turban_tone2:‍♀️", + ":man_with_turban_tone2:‍♂️", + ":man_with_turban_tone2:", + ":man_with_turban_tone3:‍♀️", + ":man_with_turban_tone3:‍♂️", + ":man_with_turban_tone3:", + ":man_with_turban_tone4:‍♀️", + ":man_with_turban_tone4:‍♂️", + ":man_with_turban_tone4:", + ":man_with_turban_tone5:‍♀️", + ":man_with_turban_tone5:‍♂️", + ":man_with_turban_tone5:", + ":man_with_turban:‍♀️", + ":man_with_turban:‍♂️", + ":man_with_turban:", + ":older_man_tone1:", + ":older_man_tone2:", + ":older_man_tone3:", + ":older_man_tone4:", + ":older_man_tone5:", + ":older_man:", + ":older_woman_tone1:", + ":older_woman_tone2:", + ":older_woman_tone3:", + ":older_woman_tone4:", + ":older_woman_tone5:", + ":older_woman:", + ":baby_tone1:", + ":baby_tone2:", + ":baby_tone3:", + ":baby_tone4:", + ":baby_tone5:", + ":baby:", + ":construction_worker_tone1:‍♀️", + ":construction_worker_tone1:‍♂️", + ":construction_worker_tone1:", + ":construction_worker_tone2:‍♀️", + ":construction_worker_tone2:‍♂️", + ":construction_worker_tone2:", + ":construction_worker_tone3:‍♀️", + ":construction_worker_tone3:‍♂️", + ":construction_worker_tone3:", + ":construction_worker_tone4:‍♀️", + ":construction_worker_tone4:‍♂️", + ":construction_worker_tone4:", + ":construction_worker_tone5:‍♀️", + ":construction_worker_tone5:‍♂️", + ":construction_worker_tone5:", + ":construction_worker:‍♀️", + ":construction_worker:‍♂️", + ":construction_worker:", + ":princess_tone1:", + ":princess_tone2:", + ":princess_tone3:", + ":princess_tone4:", + ":princess_tone5:", + ":princess:", + ":japanese_ogre:", + ":japanese_goblin:", + ":ghost:", + ":angel_tone1:", + ":angel_tone2:", + ":angel_tone3:", + ":angel_tone4:", + ":angel_tone5:", + ":angel:", + ":alien:", + ":space_invader:", + ":imp:", + ":skull:", + ":information_desk_person_tone1:‍♀️", + ":information_desk_person_tone1:‍♂️", + ":information_desk_person_tone1:", + ":information_desk_person_tone2:‍♀️", + ":information_desk_person_tone2:‍♂️", + ":information_desk_person_tone2:", + ":information_desk_person_tone3:‍♀️", + ":information_desk_person_tone3:‍♂️", + ":information_desk_person_tone3:", + ":information_desk_person_tone4:‍♀️", + ":information_desk_person_tone4:‍♂️", + ":information_desk_person_tone4:", + ":information_desk_person_tone5:‍♀️", + ":information_desk_person_tone5:‍♂️", + ":information_desk_person_tone5:", + ":information_desk_person:‍♀️", + ":information_desk_person:‍♂️", + ":information_desk_person:", + ":guardsman_tone1:‍♀️", + ":guardsman_tone1:‍♂️", + ":guardsman_tone1:", + ":guardsman_tone2:‍♀️", + ":guardsman_tone2:‍♂️", + ":guardsman_tone2:", + ":guardsman_tone3:‍♀️", + ":guardsman_tone3:‍♂️", + ":guardsman_tone3:", + ":guardsman_tone4:‍♀️", + ":guardsman_tone4:‍♂️", + ":guardsman_tone4:", + ":guardsman_tone5:‍♀️", + ":guardsman_tone5:‍♂️", + ":guardsman_tone5:", + ":guardsman:‍♀️", + ":guardsman:‍♂️", + ":guardsman:", + ":dancer_tone1:", + ":dancer_tone2:", + ":dancer_tone3:", + ":dancer_tone4:", + ":dancer_tone5:", + ":dancer:", + ":lipstick:", + ":nail_care_tone1:", + ":nail_care_tone2:", + ":nail_care_tone3:", + ":nail_care_tone4:", + ":nail_care_tone5:", + ":nail_care:", + ":massage_tone1:‍♀️", + ":massage_tone1:‍♂️", + ":massage_tone1:", + ":massage_tone2:‍♀️", + ":massage_tone2:‍♂️", + ":massage_tone2:", + ":massage_tone3:‍♀️", + ":massage_tone3:‍♂️", + ":massage_tone3:", + ":massage_tone4:‍♀️", + ":massage_tone4:‍♂️", + ":massage_tone4:", + ":massage_tone5:‍♀️", + ":massage_tone5:‍♂️", + ":massage_tone5:", + ":massage:‍♀️", + ":massage:‍♂️", + ":massage:", + ":haircut_tone1:‍♀️", + ":haircut_tone1:‍♂️", + ":haircut_tone1:", + ":haircut_tone2:‍♀️", + ":haircut_tone2:‍♂️", + ":haircut_tone2:", + ":haircut_tone3:‍♀️", + ":haircut_tone3:‍♂️", + ":haircut_tone3:", + ":haircut_tone4:‍♀️", + ":haircut_tone4:‍♂️", + ":haircut_tone4:", + ":haircut_tone5:‍♀️", + ":haircut_tone5:‍♂️", + ":haircut_tone5:", + ":haircut:‍♀️", + ":haircut:‍♂️", + ":haircut:", + ":barber:", + ":syringe:", + ":pill:", + ":kiss:", + ":love_letter:", + ":ring:", + ":gem:", + ":couplekiss:", + ":bouquet:", + ":couple_with_heart:", + ":wedding:", + ":heartbeat:", + ":broken_heart:", + ":two_hearts:", + ":sparkling_heart:", + ":heartpulse:", + ":cupid:", + ":blue_heart:", + ":green_heart:", + ":yellow_heart:", + ":purple_heart:", + ":gift_heart:", + ":revolving_hearts:", + ":heart_decoration:", + ":diamond_shape_with_a_dot_inside:", + ":bulb:", + ":anger:", + ":bomb:", + ":zzz:", + ":boom:", + ":sweat_drops:", + ":droplet:", + ":dash:", + ":poop:", + ":muscle_tone1:", + ":muscle_tone2:", + ":muscle_tone3:", + ":muscle_tone4:", + ":muscle_tone5:", + ":muscle:", + ":dizzy:", + ":speech_balloon:", + ":thought_balloon:", + ":white_flower:", + ":100:", + ":moneybag:", + ":currency_exchange:", + ":heavy_dollar_sign:", + ":credit_card:", + ":yen:", + ":dollar:", + ":euro:", + ":pound:", + ":money_with_wings:", + ":chart:", + ":seat:", + ":computer:", + ":briefcase:", + ":minidisc:", + ":floppy_disk:", + ":cd:", + ":dvd:", + ":file_folder:", + ":open_file_folder:", + ":page_with_curl:", + ":page_facing_up:", + ":date:", + ":calendar:", + ":card_index:", + ":chart_with_upwards_trend:", + ":chart_with_downwards_trend:", + ":bar_chart:", + ":clipboard:", + ":pushpin:", + ":round_pushpin:", + ":paperclip:", + ":straight_ruler:", + ":triangular_ruler:", + ":bookmark_tabs:", + ":ledger:", + ":notebook:", + ":notebook_with_decorative_cover:", + ":closed_book:", + ":book:", + ":green_book:", + ":blue_book:", + ":orange_book:", + ":books:", + ":name_badge:", + ":scroll:", + ":pencil:", + ":telephone_receiver:", + ":pager:", + ":fax:", + ":satellite:", + ":loudspeaker:", + ":mega:", + ":outbox_tray:", + ":inbox_tray:", + ":package:", + ":e-mail:", + ":incoming_envelope:", + ":envelope_with_arrow:", + ":mailbox_closed:", + ":mailbox:", + ":mailbox_with_mail:", + ":mailbox_with_no_mail:", + ":postbox:", + ":postal_horn:", + ":newspaper:", + ":iphone:", + ":calling:", + ":vibration_mode:", + ":mobile_phone_off:", + ":no_mobile_phones:", + ":signal_strength:", + ":camera:", + ":camera_with_flash:", + ":video_camera:", + ":tv:", + ":radio:", + ":vhs:", + ":projector:", + ":prayer_beads:", + ":twisted_rightwards_arrows:", + ":repeat:", + ":repeat_one:", + ":arrows_clockwise:", + ":arrows_counterclockwise:", + ":low_brightness:", + ":high_brightness:", + ":mute:", + ":speaker:", + ":sound:", + ":loud_sound:", + ":battery:", + ":electric_plug:", + ":mag:", + ":mag_right:", + ":lock_with_ink_pen:", + ":closed_lock_with_key:", + ":key:", + ":lock:", + ":unlock:", + ":bell:", + ":no_bell:", + ":bookmark:", + ":link:", + ":radio_button:", + ":back:", + ":end:", + ":on:", + ":soon:", + ":top:", + ":underage:", + ":keycap_ten:", + ":capital_abcd:", + ":abcd:", + ":1234:", + ":symbols:", + ":abc:", + ":fire:", + ":flashlight:", + ":wrench:", + ":hammer:", + ":nut_and_bolt:", + ":knife:", + ":gun:", + ":microscope:", + ":telescope:", + ":crystal_ball:", + ":six_pointed_star:", + ":beginner:", + ":trident:", + ":black_square_button:", + ":white_square_button:", + ":red_circle:", + ":large_blue_circle:", + ":large_orange_diamond:", + ":large_blue_diamond:", + ":small_orange_diamond:", + ":small_blue_diamond:", + ":small_red_triangle:", + ":small_red_triangle_down:", + ":arrow_up_small:", + ":arrow_down_small:", + ":om_symbol:", + ":dove:", + ":kaaba:", + ":mosque:", + ":synagogue:", + ":menorah:", + ":clock1:", + ":clock2:", + ":clock3:", + ":clock4:", + ":clock5:", + ":clock6:", + ":clock7:", + ":clock8:", + ":clock9:", + ":clock10:", + ":clock11:", + ":clock12:", + ":clock130:", + ":clock230:", + ":clock330:", + ":clock430:", + ":clock530:", + ":clock630:", + ":clock730:", + ":clock830:", + ":clock930:", + ":clock1030:", + ":clock1130:", + ":clock1230:", + ":candle:", + ":clock:", + ":hole:", + ":levitate::tone1:", + ":levitate::tone2:", + ":levitate::tone3:", + ":levitate::tone4:", + ":levitate::tone5:", + ":levitate:", + ":spy_tone1:‍♀️", + ":spy_tone1:‍♂️", + ":spy_tone1:", + ":spy_tone2:‍♀️", + ":spy_tone2:‍♂️", + ":spy_tone2:", + ":spy_tone3:‍♀️", + ":spy_tone3:‍♂️", + ":spy_tone3:", + ":spy_tone4:‍♀️", + ":spy_tone4:‍♂️", + ":spy_tone4:", + ":spy_tone5:‍♀️", + ":spy_tone5:‍♂️", + ":spy_tone5:", + ":spy:‍♀️", + ":spy:‍♂️", + ":spy:", + ":dark_sunglasses:", + ":spider:", + ":spider_web:", + ":joystick:", + ":man_dancing_tone1:", + ":man_dancing_tone2:", + ":man_dancing_tone3:", + ":man_dancing_tone4:", + ":man_dancing_tone5:", + ":man_dancing:", + ":paperclips:", + ":pen_ballpoint:", + ":pen_fountain:", + ":paintbrush:", + ":crayon:", + ":hand_splayed_tone1:", + ":hand_splayed_tone2:", + ":hand_splayed_tone3:", + ":hand_splayed_tone4:", + ":hand_splayed_tone5:", + ":hand_splayed:", + ":middle_finger_tone1:", + ":middle_finger_tone2:", + ":middle_finger_tone3:", + ":middle_finger_tone4:", + ":middle_finger_tone5:", + ":middle_finger:", + ":vulcan_tone1:", + ":vulcan_tone2:", + ":vulcan_tone3:", + ":vulcan_tone4:", + ":vulcan_tone5:", + ":vulcan:", + ":black_heart:", + ":desktop:", + ":printer:", + ":mouse_three_button:", + ":trackball:", + ":frame_photo:", + ":dividers:", + ":card_box:", + ":file_cabinet:", + ":wastebasket:", + ":notepad_spiral:", + ":calendar_spiral:", + ":compression:", + ":key2:", + ":newspaper2:", + ":dagger:", + ":speaking_head:", + ":speech_left:", + ":anger_right:", + ":ballot_box:", + ":map:", + ":mount_fuji:", + ":tokyo_tower:", + ":statue_of_liberty:", + ":japan:", + ":moyai:", + ":grinning:", + ":grin:", + ":joy:", + ":smiley:", + ":smile:", + ":sweat_smile:", + ":laughing:", + ":innocent:", + ":smiling_imp:", + ":wink:", + ":blush:", + ":yum:", + ":relieved:", + ":heart_eyes:", + ":sunglasses:", + ":smirk:", + ":neutral_face:", + ":expressionless:", + ":unamused:", + ":sweat:", + ":pensive:", + ":confused:", + ":confounded:", + ":kissing:", + ":kissing_heart:", + ":kissing_smiling_eyes:", + ":kissing_closed_eyes:", + ":stuck_out_tongue:", + ":stuck_out_tongue_winking_eye:", + ":stuck_out_tongue_closed_eyes:", + ":disappointed:", + ":worried:", + ":angry:", + ":rage:", + ":cry:", + ":persevere:", + ":triumph:", + ":disappointed_relieved:", + ":frowning:", + ":anguished:", + ":fearful:", + ":weary:", + ":sleepy:", + ":tired_face:", + ":grimacing:", + ":sob:", + ":open_mouth:", + ":hushed:", + ":cold_sweat:", + ":scream:", + ":astonished:", + ":flushed:", + ":sleeping:", + ":dizzy_face:", + ":no_mouth:", + ":mask:", + ":smile_cat:", + ":joy_cat:", + ":smiley_cat:", + ":heart_eyes_cat:", + ":smirk_cat:", + ":kissing_cat:", + ":pouting_cat:", + ":crying_cat_face:", + ":scream_cat:", + ":slight_frown:", + ":slight_smile:", + ":upside_down:", + ":rolling_eyes:", + ":no_good_tone1:‍♀️", + ":no_good_tone1:‍♂️", + ":no_good_tone1:", + ":no_good_tone2:‍♀️", + ":no_good_tone2:‍♂️", + ":no_good_tone2:", + ":no_good_tone3:‍♀️", + ":no_good_tone3:‍♂️", + ":no_good_tone3:", + ":no_good_tone4:‍♀️", + ":no_good_tone4:‍♂️", + ":no_good_tone4:", + ":no_good_tone5:‍♀️", + ":no_good_tone5:‍♂️", + ":no_good_tone5:", + ":no_good:‍♀️", + ":no_good:‍♂️", + ":no_good:", + ":ok_woman_tone1:‍♀️", + ":ok_woman_tone1:‍♂️", + ":ok_woman_tone1:", + ":ok_woman_tone2:‍♀️", + ":ok_woman_tone2:‍♂️", + ":ok_woman_tone2:", + ":ok_woman_tone3:‍♀️", + ":ok_woman_tone3:‍♂️", + ":ok_woman_tone3:", + ":ok_woman_tone4:‍♀️", + ":ok_woman_tone4:‍♂️", + ":ok_woman_tone4:", + ":ok_woman_tone5:‍♀️", + ":ok_woman_tone5:‍♂️", + ":ok_woman_tone5:", + ":ok_woman:‍♀️", + ":ok_woman:‍♂️", + ":ok_woman:", + ":bow_tone1:‍♀️", + ":bow_tone1:‍♂️", + ":bow_tone1:", + ":bow_tone2:‍♀️", + ":bow_tone2:‍♂️", + ":bow_tone2:", + ":bow_tone3:‍♀️", + ":bow_tone3:‍♂️", + ":bow_tone3:", + ":bow_tone4:‍♀️", + ":bow_tone4:‍♂️", + ":bow_tone4:", + ":bow_tone5:‍♀️", + ":bow_tone5:‍♂️", + ":bow_tone5:", + ":bow:‍♀️", + ":bow:‍♂️", + ":bow:", + ":see_no_evil:", + ":hear_no_evil:", + ":speak_no_evil:", + ":raising_hand_tone1:‍♀️", + ":raising_hand_tone1:‍♂️", + ":raising_hand_tone1:", + ":raising_hand_tone2:‍♀️", + ":raising_hand_tone2:‍♂️", + ":raising_hand_tone2:", + ":raising_hand_tone3:‍♀️", + ":raising_hand_tone3:‍♂️", + ":raising_hand_tone3:", + ":raising_hand_tone4:‍♀️", + ":raising_hand_tone4:‍♂️", + ":raising_hand_tone4:", + ":raising_hand_tone5:‍♀️", + ":raising_hand_tone5:‍♂️", + ":raising_hand_tone5:", + ":raising_hand:‍♀️", + ":raising_hand:‍♂️", + ":raising_hand:", + ":raised_hands_tone1:", + ":raised_hands_tone2:", + ":raised_hands_tone3:", + ":raised_hands_tone4:", + ":raised_hands_tone5:", + ":raised_hands:", + ":person_frowning_tone1:‍♀️", + ":person_frowning_tone1:‍♂️", + ":person_frowning_tone1:", + ":person_frowning_tone2:‍♀️", + ":person_frowning_tone2:‍♂️", + ":person_frowning_tone2:", + ":person_frowning_tone3:‍♀️", + ":person_frowning_tone3:‍♂️", + ":person_frowning_tone3:", + ":person_frowning_tone4:‍♀️", + ":person_frowning_tone4:‍♂️", + ":person_frowning_tone4:", + ":person_frowning_tone5:‍♀️", + ":person_frowning_tone5:‍♂️", + ":person_frowning_tone5:", + ":person_frowning:‍♀️", + ":person_frowning:‍♂️", + ":person_frowning:", + ":person_with_pouting_face_tone1:‍♀️", + ":person_with_pouting_face_tone1:‍♂️", + ":person_with_pouting_face_tone1:", + ":person_with_pouting_face_tone2:‍♀️", + ":person_with_pouting_face_tone2:‍♂️", + ":person_with_pouting_face_tone2:", + ":person_with_pouting_face_tone3:‍♀️", + ":person_with_pouting_face_tone3:‍♂️", + ":person_with_pouting_face_tone3:", + ":person_with_pouting_face_tone4:‍♀️", + ":person_with_pouting_face_tone4:‍♂️", + ":person_with_pouting_face_tone4:", + ":person_with_pouting_face_tone5:‍♀️", + ":person_with_pouting_face_tone5:‍♂️", + ":person_with_pouting_face_tone5:", + ":person_with_pouting_face:‍♀️", + ":person_with_pouting_face:‍♂️", + ":person_with_pouting_face:", + ":pray_tone1:", + ":pray_tone2:", + ":pray_tone3:", + ":pray_tone4:", + ":pray_tone5:", + ":pray:", + ":rocket:", + ":helicopter:", + ":steam_locomotive:", + ":railway_car:", + ":bullettrain_side:", + ":bullettrain_front:", + ":train2:", + ":metro:", + ":light_rail:", + ":station:", + ":tram:", + ":train:", + ":bus:", + ":oncoming_bus:", + ":trolleybus:", + ":busstop:", + ":minibus:", + ":ambulance:", + ":fire_engine:", + ":police_car:", + ":oncoming_police_car:", + ":taxi:", + ":oncoming_taxi:", + ":red_car:", + ":oncoming_automobile:", + ":blue_car:", + ":truck:", + ":articulated_lorry:", + ":tractor:", + ":monorail:", + ":mountain_railway:", + ":suspension_railway:", + ":mountain_cableway:", + ":aerial_tramway:", + ":ship:", + ":rowboat_tone1:‍♀️", + ":rowboat_tone1:‍♂️", + ":rowboat_tone1:", + ":rowboat_tone2:‍♀️", + ":rowboat_tone2:‍♂️", + ":rowboat_tone2:", + ":rowboat_tone3:‍♀️", + ":rowboat_tone3:‍♂️", + ":rowboat_tone3:", + ":rowboat_tone4:‍♀️", + ":rowboat_tone4:‍♂️", + ":rowboat_tone4:", + ":rowboat_tone5:‍♀️", + ":rowboat_tone5:‍♂️", + ":rowboat_tone5:", + ":rowboat:‍♀️", + ":rowboat:‍♂️", + ":rowboat:", + ":speedboat:", + ":traffic_light:", + ":vertical_traffic_light:", + ":construction:", + ":rotating_light:", + ":triangular_flag_on_post:", + ":door:", + ":no_entry_sign:", + ":smoking:", + ":no_smoking:", + ":put_litter_in_its_place:", + ":do_not_litter:", + ":potable_water:", + ":non-potable_water:", + ":bike:", + ":no_bicycles:", + ":bicyclist_tone1:‍♀️", + ":bicyclist_tone1:‍♂️", + ":bicyclist_tone1:", + ":bicyclist_tone2:‍♀️", + ":bicyclist_tone2:‍♂️", + ":bicyclist_tone2:", + ":bicyclist_tone3:‍♀️", + ":bicyclist_tone3:‍♂️", + ":bicyclist_tone3:", + ":bicyclist_tone4:‍♀️", + ":bicyclist_tone4:‍♂️", + ":bicyclist_tone4:", + ":bicyclist_tone5:‍♀️", + ":bicyclist_tone5:‍♂️", + ":bicyclist_tone5:", + ":bicyclist:‍♀️", + ":bicyclist:‍♂️", + ":bicyclist:", + ":mountain_bicyclist_tone1:‍♀️", + ":mountain_bicyclist_tone1:‍♂️", + ":mountain_bicyclist_tone1:", + ":mountain_bicyclist_tone2:‍♀️", + ":mountain_bicyclist_tone2:‍♂️", + ":mountain_bicyclist_tone2:", + ":mountain_bicyclist_tone3:‍♀️", + ":mountain_bicyclist_tone3:‍♂️", + ":mountain_bicyclist_tone3:", + ":mountain_bicyclist_tone4:‍♀️", + ":mountain_bicyclist_tone4:‍♂️", + ":mountain_bicyclist_tone4:", + ":mountain_bicyclist_tone5:‍♀️", + ":mountain_bicyclist_tone5:‍♂️", + ":mountain_bicyclist_tone5:", + ":mountain_bicyclist:‍♀️", + ":mountain_bicyclist:‍♂️", + ":mountain_bicyclist:", + ":walking_tone1:‍♀️", + ":walking_tone1:‍♂️", + ":walking_tone1:", + ":walking_tone2:‍♀️", + ":walking_tone2:‍♂️", + ":walking_tone2:", + ":walking_tone3:‍♀️", + ":walking_tone3:‍♂️", + ":walking_tone3:", + ":walking_tone4:‍♀️", + ":walking_tone4:‍♂️", + ":walking_tone4:", + ":walking_tone5:‍♀️", + ":walking_tone5:‍♂️", + ":walking_tone5:", + ":walking:‍♀️", + ":walking:‍♂️", + ":walking:", + ":no_pedestrians:", + ":children_crossing:", + ":mens:", + ":womens:", + ":restroom:", + ":baby_symbol:", + ":toilet:", + ":wc:", + ":shower:", + ":bath_tone1:", + ":bath_tone2:", + ":bath_tone3:", + ":bath_tone4:", + ":bath_tone5:", + ":bath:", + ":bathtub:", + ":passport_control:", + ":customs:", + ":baggage_claim:", + ":left_luggage:", + ":couch:", + ":sleeping_accommodation::tone1:", + ":sleeping_accommodation::tone2:", + ":sleeping_accommodation::tone3:", + ":sleeping_accommodation::tone4:", + ":sleeping_accommodation::tone5:", + ":sleeping_accommodation:", + ":shopping_bags:", + ":bellhop:", + ":bed:", + ":place_of_worship:", + ":octagonal_sign:", + ":shopping_cart:", + ":tools:", + ":shield:", + ":oil:", + ":motorway:", + ":railway_track:", + ":motorboat:", + ":airplane_small:", + ":airplane_departure:", + ":airplane_arriving:", + ":satellite_orbital:", + ":cruise_ship:", + ":scooter:", + ":motor_scooter:", + ":canoe:", + ":zipper_mouth:", + ":money_mouth:", + ":thermometer_face:", + ":nerd:", + ":thinking:", + ":head_bandage:", + ":robot:", + ":hugging:", + ":metal_tone1:", + ":metal_tone2:", + ":metal_tone3:", + ":metal_tone4:", + ":metal_tone5:", + ":metal:", + ":call_me_tone1:", + ":call_me_tone2:", + ":call_me_tone3:", + ":call_me_tone4:", + ":call_me_tone5:", + ":call_me:", + ":raised_back_of_hand_tone1:", + ":raised_back_of_hand_tone2:", + ":raised_back_of_hand_tone3:", + ":raised_back_of_hand_tone4:", + ":raised_back_of_hand_tone5:", + ":raised_back_of_hand:", + ":left_facing_fist_tone1:", + ":left_facing_fist_tone2:", + ":left_facing_fist_tone3:", + ":left_facing_fist_tone4:", + ":left_facing_fist_tone5:", + ":left_facing_fist:", + ":right_facing_fist_tone1:", + ":right_facing_fist_tone2:", + ":right_facing_fist_tone3:", + ":right_facing_fist_tone4:", + ":right_facing_fist_tone5:", + ":right_facing_fist:", + ":handshake_tone1:", + ":handshake_tone2:", + ":handshake_tone3:", + ":handshake_tone4:", + ":handshake_tone5:", + ":handshake:", + ":fingers_crossed_tone1:", + ":fingers_crossed_tone2:", + ":fingers_crossed_tone3:", + ":fingers_crossed_tone4:", + ":fingers_crossed_tone5:", + ":fingers_crossed:", + ":cowboy:", + ":clown:", + ":nauseated_face:", + ":rofl:", + ":drooling_face:", + ":lying_face:", + ":face_palm_tone1:‍♀️", + ":face_palm_tone1:‍♂️", + ":face_palm_tone1:", + ":face_palm_tone2:‍♀️", + ":face_palm_tone2:‍♂️", + ":face_palm_tone2:", + ":face_palm_tone3:‍♀️", + ":face_palm_tone3:‍♂️", + ":face_palm_tone3:", + ":face_palm_tone4:‍♀️", + ":face_palm_tone4:‍♂️", + ":face_palm_tone4:", + ":face_palm_tone5:‍♀️", + ":face_palm_tone5:‍♂️", + ":face_palm_tone5:", + ":face_palm:‍♀️", + ":face_palm:‍♂️", + ":face_palm:", + ":sneezing_face:", + ":pregnant_woman_tone1:", + ":pregnant_woman_tone2:", + ":pregnant_woman_tone3:", + ":pregnant_woman_tone4:", + ":pregnant_woman_tone5:", + ":pregnant_woman:", + ":selfie_tone1:", + ":selfie_tone2:", + ":selfie_tone3:", + ":selfie_tone4:", + ":selfie_tone5:", + ":selfie:", + ":prince_tone1:", + ":prince_tone2:", + ":prince_tone3:", + ":prince_tone4:", + ":prince_tone5:", + ":prince:", + ":man_in_tuxedo_tone1:", + ":man_in_tuxedo_tone2:", + ":man_in_tuxedo_tone3:", + ":man_in_tuxedo_tone4:", + ":man_in_tuxedo_tone5:", + ":man_in_tuxedo:", + ":mrs_claus_tone1:", + ":mrs_claus_tone2:", + ":mrs_claus_tone3:", + ":mrs_claus_tone4:", + ":mrs_claus_tone5:", + ":mrs_claus:", + ":shrug_tone1:‍♀️", + ":shrug_tone1:‍♂️", + ":shrug_tone1:", + ":shrug_tone2:‍♀️", + ":shrug_tone2:‍♂️", + ":shrug_tone2:", + ":shrug_tone3:‍♀️", + ":shrug_tone3:‍♂️", + ":shrug_tone3:", + ":shrug_tone4:‍♀️", + ":shrug_tone4:‍♂️", + ":shrug_tone4:", + ":shrug_tone5:‍♀️", + ":shrug_tone5:‍♂️", + ":shrug_tone5:", + ":shrug:‍♀️", + ":shrug:‍♂️", + ":shrug:", + ":cartwheel_tone1:‍♀️", + ":cartwheel_tone1:‍♂️", + ":cartwheel_tone1:", + ":cartwheel_tone2:‍♀️", + ":cartwheel_tone2:‍♂️", + ":cartwheel_tone2:", + ":cartwheel_tone3:‍♀️", + ":cartwheel_tone3:‍♂️", + ":cartwheel_tone3:", + ":cartwheel_tone4:‍♀️", + ":cartwheel_tone4:‍♂️", + ":cartwheel_tone4:", + ":cartwheel_tone5:‍♀️", + ":cartwheel_tone5:‍♂️", + ":cartwheel_tone5:", + ":cartwheel:‍♀️", + ":cartwheel:‍♂️", + ":cartwheel:", + ":juggling_tone1:‍♀️", + ":juggling_tone1:‍♂️", + ":juggling_tone1:", + ":juggling_tone2:‍♀️", + ":juggling_tone2:‍♂️", + ":juggling_tone2:", + ":juggling_tone3:‍♀️", + ":juggling_tone3:‍♂️", + ":juggling_tone3:", + ":juggling_tone4:‍♀️", + ":juggling_tone4:‍♂️", + ":juggling_tone4:", + ":juggling_tone5:‍♀️", + ":juggling_tone5:‍♂️", + ":juggling_tone5:", + ":juggling:‍♀️", + ":juggling:‍♂️", + ":juggling:", + ":fencer:", + ":wrestlers_tone1:‍♀️", + ":wrestlers_tone1:‍♂️", + ":wrestlers_tone1:", + ":wrestlers_tone2:‍♀️", + ":wrestlers_tone2:‍♂️", + ":wrestlers_tone2:", + ":wrestlers_tone3:‍♀️", + ":wrestlers_tone3:‍♂️", + ":wrestlers_tone3:", + ":wrestlers_tone4:‍♀️", + ":wrestlers_tone4:‍♂️", + ":wrestlers_tone4:", + ":wrestlers_tone5:‍♀️", + ":wrestlers_tone5:‍♂️", + ":wrestlers_tone5:", + ":wrestlers:‍♀️", + ":wrestlers:‍♂️", + ":wrestlers:", + ":water_polo_tone1:‍♀️", + ":water_polo_tone1:‍♂️", + ":water_polo_tone1:", + ":water_polo_tone2:‍♀️", + ":water_polo_tone2:‍♂️", + ":water_polo_tone2:", + ":water_polo_tone3:‍♀️", + ":water_polo_tone3:‍♂️", + ":water_polo_tone3:", + ":water_polo_tone4:‍♀️", + ":water_polo_tone4:‍♂️", + ":water_polo_tone4:", + ":water_polo_tone5:‍♀️", + ":water_polo_tone5:‍♂️", + ":water_polo_tone5:", + ":water_polo:‍♀️", + ":water_polo:‍♂️", + ":water_polo:", + ":handball_tone1:‍♀️", + ":handball_tone1:‍♂️", + ":handball_tone1:", + ":handball_tone2:‍♀️", + ":handball_tone2:‍♂️", + ":handball_tone2:", + ":handball_tone3:‍♀️", + ":handball_tone3:‍♂️", + ":handball_tone3:", + ":handball_tone4:‍♀️", + ":handball_tone4:‍♂️", + ":handball_tone4:", + ":handball_tone5:‍♀️", + ":handball_tone5:‍♂️", + ":handball_tone5:", + ":handball:‍♀️", + ":handball:‍♂️", + ":handball:", + ":wilted_rose:", + ":drum:", + ":champagne_glass:", + ":tumbler_glass:", + ":spoon:", + ":goal:", + ":first_place:", + ":second_place:", + ":third_place:", + ":boxing_glove:", + ":martial_arts_uniform:", + ":croissant:", + ":avocado:", + ":cucumber:", + ":bacon:", + ":potato:", + ":carrot:", + ":french_bread:", + ":salad:", + ":shallow_pan_of_food:", + ":stuffed_flatbread:", + ":egg:", + ":milk:", + ":peanuts:", + ":kiwi:", + ":pancakes:", + ":crab:", + ":lion_face:", + ":scorpion:", + ":turkey:", + ":unicorn:", + ":eagle:", + ":duck:", + ":bat:", + ":shark:", + ":owl:", + ":fox:", + ":butterfly:", + ":deer:", + ":gorilla:", + ":lizard:", + ":rhino:", + ":shrimp:", + ":squid:", + ":cheese:", + ":bangbang:", + ":interrobang:", + ":tm:", + ":information_source:", + ":left_right_arrow:", + ":arrow_up_down:", + ":arrow_upper_left:", + ":arrow_upper_right:", + ":arrow_lower_right:", + ":arrow_lower_left:", + ":leftwards_arrow_with_hook:", + ":arrow_right_hook:", + ":hash:", + ":watch:", + ":hourglass:", + ":keyboard:", + ":eject:", + ":fast_forward:", + ":rewind:", + ":arrow_double_up:", + ":arrow_double_down:", + ":track_next:", + ":track_previous:", + ":play_pause:", + ":alarm_clock:", + ":stopwatch:", + ":timer:", + ":hourglass_flowing_sand:", + ":pause_button:", + ":stop_button:", + ":record_button:", + ":m:", + ":black_small_square:", + ":white_small_square:", + ":arrow_forward:", + ":arrow_backward:", + ":white_medium_square:", + ":black_medium_square:", + ":white_medium_small_square:", + ":black_medium_small_square:", + ":sunny:", + ":cloud:", + ":umbrella2:", + ":snowman2:", + ":comet:", + ":telephone:", + ":ballot_box_with_check:", + ":umbrella:", + ":coffee:", + ":shamrock:", + ":point_up_tone1:", + ":point_up_tone2:", + ":point_up_tone3:", + ":point_up_tone4:", + ":point_up_tone5:", + ":point_up:", + ":skull_crossbones:", + ":radioactive:", + ":biohazard:", + ":orthodox_cross:", + ":star_and_crescent:", + ":peace:", + ":yin_yang:", + ":wheel_of_dharma:", + ":frowning2:", + ":relaxed:", + "♀", + "♂", + ":aries:", + ":taurus:", + ":gemini:", + ":cancer:", + ":leo:", + ":virgo:", + ":libra:", + ":scorpius:", + ":sagittarius:", + ":capricorn:", + ":aquarius:", + ":pisces:", + ":spades:", + ":clubs:", + ":hearts:", + ":diamonds:", + ":hotsprings:", + ":recycle:", + ":wheelchair:", + ":hammer_pick:", + ":anchor:", + ":crossed_swords:", + "⚕", + ":scales:", + ":alembic:", + ":gear:", + ":atom:", + ":fleur-de-lis:", + ":warning:", + ":zap:", + ":white_circle:", + ":black_circle:", + ":coffin:", + ":urn:", + ":soccer:", + ":baseball:", + ":snowman:", + ":partly_sunny:", + ":thunder_cloud_rain:", + ":ophiuchus:", + ":pick:", + ":helmet_with_cross:", + ":chains:", + ":no_entry:", + ":shinto_shrine:", + ":church:", + ":mountain:", + ":beach_umbrella:", + ":fountain:", + ":golf:", + ":ferry:", + ":sailboat:", + ":skier::tone1:", + ":skier::tone2:", + ":skier::tone3:", + ":skier::tone4:", + ":skier::tone5:", + ":skier:", + ":ice_skate:", + ":basketball_player_tone1:‍♀️", + ":basketball_player_tone1:‍♂️", + ":basketball_player_tone1:", + ":basketball_player_tone2:‍♀️", + ":basketball_player_tone2:‍♂️", + ":basketball_player_tone2:", + ":basketball_player_tone3:‍♀️", + ":basketball_player_tone3:‍♂️", + ":basketball_player_tone3:", + ":basketball_player_tone4:‍♀️", + ":basketball_player_tone4:‍♂️", + ":basketball_player_tone4:", + ":basketball_player_tone5:‍♀️", + ":basketball_player_tone5:‍♂️", + ":basketball_player_tone5:", + ":basketball_player:‍♀️", + ":basketball_player:‍♂️", + ":basketball_player:", + ":tent:", + ":fuelpump:", + ":scissors:", + ":white_check_mark:", + ":airplane:", + ":envelope:", + ":fist_tone1:", + ":fist_tone2:", + ":fist_tone3:", + ":fist_tone4:", + ":fist_tone5:", + ":fist:", + ":raised_hand_tone1:", + ":raised_hand_tone2:", + ":raised_hand_tone3:", + ":raised_hand_tone4:", + ":raised_hand_tone5:", + ":raised_hand:", + ":v_tone1:", + ":v_tone2:", + ":v_tone3:", + ":v_tone4:", + ":v_tone5:", + ":v:", + ":writing_hand_tone1:", + ":writing_hand_tone2:", + ":writing_hand_tone3:", + ":writing_hand_tone4:", + ":writing_hand_tone5:", + ":writing_hand:", + ":pencil2:", + ":black_nib:", + ":heavy_check_mark:", + ":heavy_multiplication_x:", + ":cross:", + ":star_of_david:", + ":sparkles:", + ":eight_spoked_asterisk:", + ":eight_pointed_black_star:", + ":snowflake:", + ":sparkle:", + ":x:", + ":negative_squared_cross_mark:", + ":question:", + ":grey_question:", + ":grey_exclamation:", + ":exclamation:", + ":heart_exclamation:", + ":heart:", + ":heavy_plus_sign:", + ":heavy_minus_sign:", + ":heavy_division_sign:", + ":arrow_right:", + ":curly_loop:", + ":loop:", + ":arrow_heading_up:", + ":arrow_heading_down:", + ":asterisk:", + ":arrow_left:", + ":arrow_up:", + ":arrow_down:", + ":black_large_square:", + ":white_large_square:", + ":star:", + ":o:", + ":zero:", + ":wavy_dash:", + ":part_alternation_mark:", + ":one:", + ":two:", + ":congratulations:", + ":secret:", + ":three:", + ":four:", + ":five:", + ":six:", + ":seven:", + ":eight:", + ":nine:", + ":copyright:", + ":registered:", + "" +]; + +frappe.ui.emoji_map = (function() { + const map = {}; + for(let i = 0; i < frappe.ui.emojis.length; i++) { + map[frappe.ui.emoji_keywords[i]] = frappe.ui.emojis[i]; + } + return map; +})(); \ No newline at end of file diff --git a/frappe/public/js/frappe/ui/graph.js b/frappe/public/js/frappe/ui/graph.js deleted file mode 100644 index 2347f13b0d..0000000000 --- a/frappe/public/js/frappe/ui/graph.js +++ /dev/null @@ -1,308 +0,0 @@ -// specific_values = [ -// { -// name: "Average", -// line_type: "dashed", // "dashed" or "solid" -// value: 10 -// }, - -// summary_values = [ -// { -// name: "Total", -// color: 'blue', // Indicator colors: 'grey', 'blue', 'red', 'green', 'orange', -// // 'purple', 'darkgrey', 'black', 'yellow', 'lightblue' -// value: 80 -// } -// ] - -frappe.ui.Graph = class Graph { - constructor({ - parent = null, - - width = 0, height = 0, - title = '', subtitle = '', - - y_values = [], - x_points = [], - - specific_values = [], - summary_values = [], - - color = '', - mode = '', - } = {}) { - - if(Object.getPrototypeOf(this) === frappe.ui.Graph.prototype) { - if(mode === 'line-graph') { - return new frappe.ui.LineGraph(arguments[0]); - } else if(mode === 'bar-graph') { - return new frappe.ui.BarGraph(arguments[0]); - } - } - - this.parent = parent; - - this.width = width; - this.height = height; - - this.title = title; - this.subtitle = subtitle; - - this.y_values = y_values; - this.x_points = x_points; - - this.specific_values = specific_values; - this.summary_values = summary_values; - - this.color = color; - this.mode = mode; - - this.$graph = null; - - frappe.require("assets/frappe/js/lib/snap.svg-min.js", this.setup.bind(this)); - } - - setup() { - this.setup_container(); - this.refresh(); - } - - refresh() { - this.setup_values(); - this.setup_components(); - this.make_y_axis(); - this.make_x_axis(); - this.make_units(); - if(this.specific_values.length > 0) { - this.show_specific_values(); - } - this.setup_group(); - - if(this.summary_values.length > 0) { - this.show_summary(); - } - } - - setup_container() { - this.container = $('
    ') - .addClass('graph-container') - .append($(`
    ${this.title}
    `)) - .append($(`
    ${this.subtitle}
    `)) - .append($(`
    `)) - .append($(`
    `)) - .appendTo(this.parent); - - let $graphics = this.container.find('.graphics'); - this.$stats_container = this.container.find('.stats-container'); - - this.$graph = $('
    ') - .addClass(this.mode) - .appendTo($graphics); - - this.$svg = $(``); - this.$graph.append(this.$svg); - - this.snap = new Snap(this.$svg[0]); - } - - setup_values() { - this.upper_graph_bound = this.get_upper_limit_and_parts(this.y_values)[0]; - this.y_axis = this.get_y_axis(this.y_values); - this.avg_unit_width = (this.width-100)/(this.x_points.length - 1); - } - - setup_components() { - this.y_axis_group = this.snap.g().attr({ - class: "y axis" - }); - - this.x_axis_group = this.snap.g().attr({ - class: "x axis" - }); - - this.graph_list = this.snap.g().attr({ - class: "data-points", - }); - - this.specific_y_lines = this.snap.g().attr({ - class: "specific axis", - }); - } - - setup_group() { - this.snap.g( - this.y_axis_group, - this.x_axis_group, - this.graph_list, - this.specific_y_lines - ).attr({ - transform: "translate(60, 10)" // default - }); - } - - show_specific_values() { - this.specific_values.map(d => { - this.specific_y_lines.add(this.snap.g( - this.snap.line(0, 0, this.width - 70, 0).attr({ - class: d.line_type === "dashed" ? "dashed": "" - }), - this.snap.text(this.width - 95, 0, d.name.toUpperCase()).attr({ - dy: ".32em", - class: "specific-value", - }) - ).attr({ - class: "tick", - transform: `translate(0, ${100 - 100/(this.upper_graph_bound/d.value) })` - })); - }); - } - - show_summary() { - this.summary_values.map(d => { - this.$stats_container.append($(`
    - ${d.name}: ${d.value} -
    `)); - }); - } - - // Helpers - get_upper_limit_and_parts(array) { - let specific_values = this.specific_values.map(d => d.value); - let max_val = parseInt(Math.max(...array, ...specific_values)); - if((max_val+"").length <= 1) { - return [10, 5]; - } else { - let multiplier = Math.pow(10, ((max_val+"").length - 1)); - let significant = Math.ceil(max_val/multiplier); - if(significant % 2 !== 0) significant++; - let parts = (significant < 5) ? significant : significant/2; - return [significant * multiplier, parts]; - } - } - - get_y_axis(array) { - let upper_limit, parts; - [upper_limit, parts] = this.get_upper_limit_and_parts(array); - let y_axis = []; - for(var i = 0; i <= parts; i++){ - y_axis.push(upper_limit / parts * i); - } - return y_axis; - } -}; - -frappe.ui.BarGraph = class BarGraph extends frappe.ui.Graph { - constructor(args = {}) { - super(args); - } - - setup_values() { - super.setup_values(); - this.avg_unit_width = (this.width-50)/(this.x_points.length + 2); - } - - make_y_axis() { - this.y_axis.map((point) => { - this.y_axis_group.add(this.snap.g( - this.snap.line(0, 0, this.width, 0), - this.snap.text(-3, 0, point+"").attr({ - dy: ".32em", - class: "y-value-text" - }) - ).attr({ - class: "tick", - transform: `translate(0, ${100 - (100/(this.y_axis.length-1) * this.y_axis.indexOf(point)) })` - })); - }); - } - - make_x_axis() { - this.x_axis_group.attr({ - transform: "translate(0,100)" - }); - this.x_points.map((point, i) => { - this.x_axis_group.add(this.snap.g( - this.snap.line(0, 0, 0, 6), - this.snap.text(0, 9, point).attr({ - dy: ".71em", - class: "x-value-text" - }) - ).attr({ - class: "tick x-axis-label", - transform: `translate(${ ((this.avg_unit_width - 5)*3/2) + i * (this.avg_unit_width + 5) }, 0)` - })); - }); - } - - make_units() { - this.y_values.map((value, i) => { - this.graph_list.add(this.snap.g( - this.snap.rect( - 0, - (100 - 100/(this.upper_graph_bound/value)), - this.avg_unit_width - 5, - 100/(this.upper_graph_bound/value) - ) - ).attr({ - class: "bar mini", - transform: `translate(${ (this.avg_unit_width - 5) + i * (this.avg_unit_width + 5) }, 0)`, - })); - }); - } -}; - -frappe.ui.LineGraph = class LineGraph extends frappe.ui.Graph { - constructor(args = {}) { - super(args); - } - - make_y_axis() { - this.y_axis.map((point) => { - this.y_axis_group.add(this.snap.g( - this.snap.line(0, 0, -6, 0), - this.snap.text(-9, 0, point+"").attr({ - dy: ".32em", - class: "y-value-text" - }) - ).attr({ - class: "tick", - transform: `translate(0, ${100 - (100/(this.y_axis.length-1) - * this.y_axis.indexOf(point)) })` - })); - }); - } - - make_x_axis() { - this.x_axis_group.attr({ - transform: "translate(0,-7)" - }); - this.x_points.map((point, i) => { - this.x_axis_group.add(this.snap.g( - this.snap.line(0, 0, 0, this.height - 25), - this.snap.text(0, this.height - 15, point).attr({ - dy: ".71em", - class: "x-value-text" - }) - ).attr({ - class: "tick", - transform: `translate(${ i * this.avg_unit_width }, 0)` - })); - }); - } - - make_units() { - let points_list = []; - this.y_values.map((value, i) => { - let x = i * this.avg_unit_width; - let y = (100 - 100/(this.upper_graph_bound/value)); - this.graph_list.add(this.snap.circle( x, y, 4)); - points_list.push(x+","+y); - }); - - this.make_path("M"+points_list.join("L")); - } - - make_path(path_str) { - this.graph_list.prepend(this.snap.path(path_str)); - } - -}; diff --git a/frappe/public/js/frappe/ui/graphs.js b/frappe/public/js/frappe/ui/graphs.js new file mode 100644 index 0000000000..f45ced98ba --- /dev/null +++ b/frappe/public/js/frappe/ui/graphs.js @@ -0,0 +1,569 @@ +// specific_values = [ +// { +// name: "Average", +// line_type: "dashed", // "dashed" or "solid" +// value: 10 +// }, + +// summary = [ +// { +// name: "Total", +// color: 'blue', // Indicator colors: 'grey', 'blue', 'red', 'green', 'orange', +// // 'purple', 'darkgrey', 'black', 'yellow', 'lightblue' +// value: 80 +// } +// ] + +// Graph: Abstract object +frappe.ui.Graph = class Graph { + constructor({ + parent = null, + height = 240, + + title = '', subtitle = '', + + y = [], + x = [], + + specific_values = [], + summary = [], + + color = 'blue', + mode = '', + }) { + + if(Object.getPrototypeOf(this) === frappe.ui.Graph.prototype) { + if(mode === 'line') { + return new frappe.ui.LineGraph(arguments[0]); + } else if(mode === 'bar') { + return new frappe.ui.BarGraph(arguments[0]); + } else if(mode === 'percentage') { + return new frappe.ui.PercentageGraph(arguments[0]); + } + } + + this.parent = parent; + this.base_height = height; + this.height = height - 40; + + this.translate_x = 60; + this.translate_y = 10; + + this.title = title; + this.subtitle = subtitle; + + this.y = y; + this.x = x; + + this.specific_values = specific_values; + this.summary = summary; + + this.color = color; + this.mode = mode; + + this.$graph = null; + + // Validate all arguments + + frappe.require("assets/frappe/js/lib/snap.svg-min.js", this.setup.bind(this)); + } + + setup() { + this.bind_window_event(); + this.refresh(); + } + + bind_window_event() { + $(window).on('resize orientationChange', () => { + this.refresh(); + }); + } + + refresh() { + + this.base_width = this.parent.width() - 20; + this.width = this.base_width - 100; + + this.setup_container(); + + this.setup_values(); + + this.setup_utils(); + + this.setup_components(); + this.make_graph_components(); + + this.make_tooltip(); + + if(this.summary.length > 0) { + this.show_custom_summary(); + } else { + this.show_summary(); + } + } + + setup_container() { + // Graph needs a dedicated parent element + this.parent.empty(); + + this.container = $('
    ') + .addClass('graph-container') + .append($(`
    ${this.title}
    `)) + .append($(`
    ${this.subtitle}
    `)) + .append($(`
    `)) + .append($(`
    `)) + .appendTo(this.parent); + + this.$graphics = this.container.find('.graph-graphics'); + this.$stats_container = this.container.find('.graph-stats-container'); + + this.$graph = $('
    ') + .addClass(this.mode + '-graph') + .appendTo(this.$graphics); + + this.$graph.append(this.make_graph_area()); + } + + make_graph_area() { + this.$svg = $(``); + this.snap = new Snap(this.$svg[0]); + return this.$svg; + } + + setup_values() { + // Multiplier + let all_values = this.specific_values.map(d => d.value); + this.y.map(d => { + all_values = all_values.concat(d.values); + }); + [this.upper_limit, this.parts] = this.get_upper_limit_and_parts(all_values); + this.multiplier = this.height / this.upper_limit; + + // Baselines + this.set_avg_unit_width_and_x_offset(); + + this.x_axis_values = this.x.values.map((d, i) => this.x_offset + i * this.avg_unit_width); + this.y_axis_values = this.get_y_axis_values(this.upper_limit, this.parts); + + // Data points + this.y.map(d => { + d.y_tops = d.values.map( val => this.height - val * this.multiplier ); + d.data_units = []; + }); + + this.calc_min_tops(); + } + + set_avg_unit_width_and_x_offset() { + this.avg_unit_width = this.width/(this.x.values.length - 1); + this.x_offset = 0; + } + + calc_min_tops() { + this.y_min_tops = new Array(this.x_axis_values.length).fill(9999); + this.y.map(d => { + d.y_tops.map( (y_top, i) => { + if(y_top < this.y_min_tops[i]) { + this.y_min_tops[i] = y_top; + } + }); + }); + } + + setup_components() { + this.y_axis_group = this.snap.g().attr({ class: "y axis" }); + this.x_axis_group = this.snap.g().attr({ class: "x axis" }); + this.data_units = this.snap.g().attr({ class: "graph-data-points" }); + this.specific_y_lines = this.snap.g().attr({ class: "specific axis" }); + } + + make_graph_components() { + this.make_y_axis(); + this.make_x_axis(); + + this.y.map((d, i) => { + this.make_units(d.y_tops, d.color, i); + this.make_path(d); + }); + + if(this.specific_values.length > 0) { + this.show_specific_values(); + } + this.setup_group(); + } + + setup_group() { + this.snap.g( + this.y_axis_group, + this.x_axis_group, + this.data_units, + this.specific_y_lines + ).attr({ + transform: `translate(${this.translate_x}, ${this.translate_y})` + }); + } + + // make HORIZONTAL lines for y values + make_y_axis() { + let width, text_end_at = -9, label_class = '', start_at = 0; + if(this.y_axis_mode === 'span') { // long spanning lines + width = this.width + 6; + start_at = -6; + } else if(this.y_axis_mode === 'tick'){ // short label lines + width = -6; + label_class = 'y-axis-label'; + } + + this.y_axis_values.map((point) => { + this.y_axis_group.add(this.snap.g( + this.snap.line(start_at, 0, width, 0), + this.snap.text(text_end_at, 0, point+"").attr({ + dy: ".32em", + class: "y-value-text" + }) + ).attr({ + class: `tick ${label_class}`, + transform: `translate(0, ${this.height - point * this.multiplier })` + })); + }); + } + + // make VERTICAL lines for x values + make_x_axis() { + let start_at, height, text_start_at, label_class = ''; + if(this.x_axis_mode === 'span') { // long spanning lines + start_at = -7; + height = this.height + 15; + text_start_at = this.height + 25; + } else if(this.x_axis_mode === 'tick'){ // short label lines + start_at = this.height; + height = 6; + text_start_at = 9; + label_class = 'x-axis-label'; + } + + this.x_axis_group.attr({ + transform: `translate(0,${start_at})` + }); + this.x.values.map((point, i) => { + this.x_axis_group.add(this.snap.g( + this.snap.line(0, 0, 0, height), + this.snap.text(0, text_start_at, point).attr({ + dy: ".71em", + class: "x-value-text" + }) + ).attr({ + class: `tick ${label_class}`, + transform: `translate(${ this.x_axis_values[i] }, 0)` + })); + }); + } + + make_units(y_values, color, dataset_index) { + let d = this.unit_args; + y_values.map((y, i) => { + let data_unit = this.draw[d.type](this.x_axis_values[i], + y, d.args, color, dataset_index); + this.data_units.add(data_unit); + this.y[dataset_index].data_units.push(data_unit); + }); + } + + make_path() { } + + make_tooltip() { + this.tip = $(`
    + +
      +
    +
    `).attr({ + style: `top: 0px; left: 0px; opacity: 0; pointer-events: none;` + }).appendTo(this.$graphics); + + this.tip_title = this.tip.find('.title'); + this.tip_data_point_list = this.tip.find('.data-point-list'); + + this.bind_tooltip(); + } + + bind_tooltip() { + this.$graphics.on('mousemove', (e) => { + let offset = $(this.$graphics).offset(); + var relX = e.pageX - offset.left - this.translate_x; + var relY = e.pageY - offset.top - this.translate_y; + + if(relY < this.height) { + for(var i=this.x_axis_values.length - 1; i >= 0 ; i--) { + let x_val = this.x_axis_values[i]; + if(relX > x_val - this.avg_unit_width/2) { + let x = x_val - this.tip.width()/2 + this.translate_x; + let y = this.y_min_tops[i] - this.tip.height() + this.translate_y; + + this.fill_tooltip(i); + + this.tip.attr({ + style: `top: ${y}px; left: ${x-0.5}px; opacity: 1; pointer-events: none;` + }); + break; + } + } + } else { + this.tip.attr({ + style: `top: 0px; left: 0px; opacity: 0; pointer-events: none;` + }); + } + }); + + this.$graphics.on('mouseleave', () => { + this.tip.attr({ + style: `top: 0px; left: 0px; opacity: 0; pointer-events: none;` + }); + }); + } + + fill_tooltip(i) { + this.tip_title.html(this.x.formatted && this.x.formatted.length>0 + ? this.x.formatted[i] : this.x.values[i]); + this.tip_data_point_list.empty(); + this.y.map(y_set => { + let $li = $(`
  5. + + ${y_set.formatted ? y_set.formatted[i] : y_set.values[i]} + + ${y_set.title ? y_set.title : '' } +
  6. `).addClass(`border-top ${y_set.color}`); + this.tip_data_point_list.append($li); + }); + } + + show_specific_values() { + this.specific_values.map(d => { + this.specific_y_lines.add(this.snap.g( + this.snap.line(0, 0, this.width, 0).attr({ + class: d.line_type === "dashed" ? "graph-dashed": "" + }), + this.snap.text(this.width + 5, 0, d.name.toUpperCase()).attr({ + dy: ".32em", + class: "specific-value", + }) + ).attr({ + class: "tick", + transform: `translate(0, ${this.height - d.value * this.multiplier })` + })); + }); + } + + show_summary() { } + + show_custom_summary() { + this.summary.map(d => { + this.$stats_container.append($(`
    + ${d.name}: ${d.value} +
    `)); + }); + } + + change_values(new_y) { + let u = this.unit_args; + this.y.map((d, i) => { + let new_d = new_y[i]; + new_d.y_tops = new_d.values.map(val => this.height - val * this.multiplier); + + // below is equal to this.y[i].data_units.. + d.data_units.map((unit, j) => { + let current_y_top = d.y_tops[j]; + let current_height = this.height - current_y_top; + + let new_y_top = new_d.y_tops[j]; + let new_height = current_height - (new_y_top - current_y_top); + + this.animate[u.type](unit, new_y_top, {new_height: new_height}); + }); + }); + + // Replace values and formatted and tops + this.y.map((d, i) => { + let new_d = new_y[i]; + [d.values, d.formatted, d.y_tops] = [new_d.values, new_d.formatted, new_d.y_tops]; + }); + + this.calc_min_tops(); + + // create new x,y pair string and animate path + if(this.y[0].path) { + new_y.map((e, i) => { + let new_points_list = e.y_tops.map((y, i) => (this.x_axis_values[i] + ',' + y)); + let new_path_str = "M"+new_points_list.join("L"); + this.y[i].path.animate({d:new_path_str}, 300, mina.easein); + }); + } + } + + // Helpers + get_strwidth(string) { + return string.length * 8; + } + + get_upper_limit_and_parts(array) { + let max_val = parseInt(Math.max(...array)); + if((max_val+"").length <= 1) { + return [10, 5]; + } else { + let multiplier = Math.pow(10, ((max_val+"").length - 1)); + let significant = Math.ceil(max_val/multiplier); + if(significant % 2 !== 0) significant++; + let parts = (significant < 5) ? significant : significant/2; + return [significant * multiplier, parts]; + } + } + + get_y_axis_values(upper_limit, parts) { + let y_axis = []; + for(var i = 0; i <= parts; i++){ + y_axis.push(upper_limit / parts * i); + } + return y_axis; + } + + // Objects + setup_utils() { + this.draw = { + 'bar': (x, y, args, color, index) => { + let total_width = this.avg_unit_width - args.space_width; + let start_x = x - total_width/2; + + let width = total_width / args.no_of_datasets; + let current_x = start_x + width * index; + return this.snap.rect(current_x, y, width, this.height - y).attr({ + class: `bar mini fill ${color}` + }); + }, + 'dot': (x, y, args, color) => { + return this.snap.circle(x, y, args.radius).attr({ + class: `fill ${color}` + }); + } + }; + + this.animate = { + 'bar': (bar, new_y, args) => { + bar.animate({height: args.new_height, y: new_y}, 300, mina.easein); + }, + 'dot': (dot, new_y) => { + dot.animate({cy: new_y}, 300, mina.easein); + } + }; + } +}; + +frappe.ui.BarGraph = class BarGraph extends frappe.ui.Graph { + constructor(args = {}) { + super(args); + } + + setup_values() { + var me = this; + super.setup_values(); + this.x_offset = this.avg_unit_width; + this.y_axis_mode = 'span'; + this.x_axis_mode = 'tick'; + this.unit_args = { + type: 'bar', + args: { + space_width: this.y.length > 1 ? + me.avg_unit_width/2 : me.avg_unit_width/8, + no_of_datasets: this.y.length + } + }; + } + + set_avg_unit_width_and_x_offset() { + this.avg_unit_width = this.width/(this.x.values.length + 1); + this.x_offset = this.avg_unit_width; + } +}; + +frappe.ui.LineGraph = class LineGraph extends frappe.ui.Graph { + constructor(args = {}) { + super(args); + } + + setup_values() { + super.setup_values(); + this.y_axis_mode = 'tick'; + this.x_axis_mode = 'span'; + this.unit_args = { + type: 'dot', + args: { radius: 4 } + }; + } + + make_path(d) { + let points_list = d.y_tops.map((y, i) => (this.x_axis_values[i] + ',' + y)); + let path_str = "M"+points_list.join("L"); + d.path = this.snap.path(path_str).attr({class: `stroke ${d.color}`}); + this.data_units.prepend(d.path); + } +}; + +frappe.ui.PercentageGraph = class PercentageGraph extends frappe.ui.Graph { + constructor(args = {}) { + super(args); + } + + make_graph_area() { + this.$graphics.addClass('graph-focus-margin'); + this.$stats_container.addClass('graph-focus-margin').attr({ + style: `padding-top: 0px; margin-bottom: 30px;` + }); + this.$div = $(`
    +
    +
    `); + this.$chart = this.$div.find('.progress-chart'); + return this.$div; + } + + setup_values() { + this.x.totals = this.x.values.map((d, i) => { + let total = 0; + this.y.map(e => { + total += e.values[i]; + }); + return total; + }); + + // Calculate x unit distances for tooltips + } + + setup_utils() { } + setup_components() { + this.$percentage_bar = $(`
    +
    `).appendTo(this.$chart); + } + + make_graph_components() { + let grand_total = this.x.totals.reduce((a, b) => a + b, 0); + this.x.units = []; + this.x.totals.map((total, i) => { + let $part = $(`
    `); + this.x.units.push($part); + this.$percentage_bar.append($part); + }); + } + + make_tooltip() { } + + show_summary() { + let values = this.x.formatted.length > 0 ? this.x.formatted : this.x.values; + this.x.totals.map((d, i) => { + this.$stats_container.append($(`
    + + ${values[i]}: + ${d} + +
    `)); + }); + } +}; diff --git a/frappe/public/js/frappe/ui/messages.js b/frappe/public/js/frappe/ui/messages.js index 761f87a202..79c1345cfa 100644 --- a/frappe/public/js/frappe/ui/messages.js +++ b/frappe/public/js/frappe/ui/messages.js @@ -137,16 +137,21 @@ frappe.msgprint = function(msg, title) { data.message = ''; } - if(data.message.search(/
    |

    |

  7. /)==-1) + if(data.message.search(/
    |

    |

  8. /)==-1) { msg = replace_newlines(data.message); + } - - var msg_exists = msg_dialog.msg_area.html(); + var msg_exists = false; + if(data.clear) { + msg_dialog.msg_area.empty(); + } else { + msg_exists = msg_dialog.msg_area.html(); + } if(data.title || !msg_exists) { // set title only if it is explicitly given // and no existing title exists - msg_dialog.set_title(data.title || __('Message')) + msg_dialog.set_title(data.title || __('Message')); } // show / hide indicator diff --git a/frappe/public/js/frappe/ui/tags.js b/frappe/public/js/frappe/ui/tags.js index c7fd32a5fb..ede40f50ee 100644 --- a/frappe/public/js/frappe/ui/tags.js +++ b/frappe/public/js/frappe/ui/tags.js @@ -77,7 +77,8 @@ frappe.ui.TagEditor = Class.extend({ args:{ doctype: me.frm.doctype, txt: value.toLowerCase(), - cat_tags:JSON.stringify(me.list_sidebar.get_cat_tags()) + cat_tags: me.list_sidebar ? + JSON.stringify(me.list_sidebar.get_cat_tags()) : '[]' }, callback: function(r) { me.awesomplete.list = r.message; diff --git a/frappe/public/js/frappe/views/reports/query_report.js b/frappe/public/js/frappe/views/reports/query_report.js index 3162cd5db1..7e0bc14a13 100644 --- a/frappe/public/js/frappe/views/reports/query_report.js +++ b/frappe/public/js/frappe/views/reports/query_report.js @@ -43,7 +43,7 @@ frappe.views.QueryReport = Class.extend({ this.wrapper = $("
    ").appendTo(this.page.main); $('\ \ -
    \ +
    \
    @@ -76,7 +76,7 @@ Issues
  9. - + License
  10. @@ -95,14 +95,14 @@
  11. - - - - + + + + {%- block script %}{%- endblock %} diff --git a/frappe/templates/autodoc/dev_home.html b/frappe/templates/autodoc/dev_home.html index 20176d6769..5d9c3a0d06 100644 --- a/frappe/templates/autodoc/dev_home.html +++ b/frappe/templates/autodoc/dev_home.html @@ -7,9 +7,9 @@ {{ source_link(app, app.name, True) }}
    - +
    -
    + App Name @@ -37,10 +37,10 @@

    Contents

    diff --git a/frappe/templates/autodoc/docs_home.html b/frappe/templates/autodoc/docs_home.html deleted file mode 100644 index 602c9fb5b0..0000000000 --- a/frappe/templates/autodoc/docs_home.html +++ /dev/null @@ -1,62 +0,0 @@ - - - - - - -
    -
    -
    -
    -

    {{ app.headline }}

    -

    {{ app.sub_heading }}

    -
    -
    -
    - -
    -
    -
    -
    -
    - - -{% if app.long_description %} -
    -
    -
    - {{ app.long_description|markdown }} -
    -
    -
    -{% endif %} - -{% if not app.hide_install %} -
    -
    -
    -

    Install

    -

    From your site

    -

    To install this app, login to your site and click on "Installer". Search for {{ app.title }} and click on "Install"

    -

    Using Bench

    -

    Go to your bench folder and setup the new app

    -
    $ bench get-app {{app.name}} {{app.source_link}}
    -$ bench new-site testsite
    -$ bench --site testsite install-app {{app.name}}
    -

    Login to your site to configure the app.

    -

    Detailed Installation Steps

    -
    -
    -

    Author

    - -

    {{ app.publisher }} ({{ app.email }})

    -
    -
    -
    -{% endif %} - - - diff --git a/frappe/templates/autodoc/doctype.html b/frappe/templates/autodoc/doctype.html index 3236f0c994..b851fc52e8 100644 --- a/frappe/templates/autodoc/doctype.html +++ b/frappe/templates/autodoc/doctype.html @@ -1,10 +1,12 @@ + {% from "templates/autodoc/macros.html" import automodule, version, source_link, doctype_link %} {% set doc = frappe.get_doc("DocType", doctype) %} {% set controller = autodoc.get_controller(doctype) %} +

    {{ doctype }}

    {{ version(doctype) }} {{ source_link(app, app.name + "/" + scrub(doc.module) @@ -22,7 +24,7 @@

    Fields

    - +
    diff --git a/frappe/templates/autodoc/install.md b/frappe/templates/autodoc/install.md index 63d2a17a38..f71b8d453d 100644 --- a/frappe/templates/autodoc/install.md +++ b/frappe/templates/autodoc/install.md @@ -2,11 +2,11 @@ # Installation -{{ app.title }} is based on the Frappe Framework, a full stack web framework based on Python, MariaDB, Redis, Node. +{{ app.title }} is based on the Frappé Framework, a full stack web framework based on Python, MariaDB, Redis, Node. -To intall {{ app.title }}, you will have to install the Frappe Bench, the command-line, package manager and site manager for Frappe Framework. For more details, read the Bench README. +To intall {{ app.title }}, you will have to install the Frappé Bench, the command-line, package manager and site manager for Frappé Framework. For more details, read the Bench README. -After you have installed Frappe Bench, go to you bench folder, which is `frappe.bench` by default and setup **{{ app.name }}**. +After you have installed Frappé Bench, go to you bench folder, which is `frappe.bench` by default and setup **{{ app.name }}**. bench get-app {{ app.name }} {{ source_link }} diff --git a/frappe/templates/autodoc/macros.html b/frappe/templates/autodoc/macros.html index 29e1d520b8..e77f83fb58 100644 --- a/frappe/templates/autodoc/macros.html +++ b/frappe/templates/autodoc/macros.html @@ -59,7 +59,7 @@ {% macro doctype_link(app, doctype) %} {% set module = frappe.db.get_value("DocType", doctype, "module") %} {% if doctype and module %} -{{ doctype }} {% endif %} {% endmacro %} diff --git a/frappe/templates/autodoc/models_home.html b/frappe/templates/autodoc/models_home.html index c9f960d416..44c5057ca2 100644 --- a/frappe/templates/autodoc/models_home.html +++ b/frappe/templates/autodoc/models_home.html @@ -1,6 +1,9 @@ + {% from "templates/autodoc/macros.html" import source_link, version %} + +

    {{ app.title }}: Models (DocTypes)

    {{ version(app.name) }} {{ source_link(app, app.name, True) }} diff --git a/frappe/templates/autodoc/module_home.html b/frappe/templates/autodoc/module_home.html index 3e75f3aeca..c10994841b 100644 --- a/frappe/templates/autodoc/module_home.html +++ b/frappe/templates/autodoc/module_home.html @@ -1,6 +1,9 @@ + {% from "templates/autodoc/macros.html" import source_link, version %} + +

    Module {{ name }}

    {{ version(app.name) }} {{ source_link(app, app.name + "/" + scrub(name), True) }} diff --git a/frappe/templates/autodoc/package_index.html b/frappe/templates/autodoc/package_index.html index 0381e07af0..9107996dfa 100644 --- a/frappe/templates/autodoc/package_index.html +++ b/frappe/templates/autodoc/package_index.html @@ -1,6 +1,9 @@ + {% from "templates/autodoc/macros.html" import source_link, version %} + +

    {{ title }}

    {{ version(app.name) }} {{ source_link(app, title, True) }} diff --git a/frappe/templates/autodoc/pymodule.html b/frappe/templates/autodoc/pymodule.html index ab6301ae85..537b37ead0 100644 --- a/frappe/templates/autodoc/pymodule.html +++ b/frappe/templates/autodoc/pymodule.html @@ -1,7 +1,10 @@ + {%- from "templates/autodoc/macros.html" import automodule, source_link, version -%} + +

    {{ name }}

    {{ version(app.name) }} {{ source_link(app, full_module_name.replace(".", "/") + ".py") }} diff --git a/frappe/templates/emails/administrator_logged_in.html b/frappe/templates/emails/administrator_logged_in.html new file mode 100644 index 0000000000..d16034bfd1 --- /dev/null +++ b/frappe/templates/emails/administrator_logged_in.html @@ -0,0 +1,3 @@ +

    {{ _("Dear System Manager,") }}

    +

    {{ access_message }}

    +

    {{ _("If you think this is unauthorized, please change the Administrator password.") }}

    \ No newline at end of file diff --git a/frappe/templates/emails/email_footer.html b/frappe/templates/emails/email_footer.html new file mode 100644 index 0000000000..5352d817d5 --- /dev/null +++ b/frappe/templates/emails/email_footer.html @@ -0,0 +1,29 @@ + \ No newline at end of file diff --git a/frappe/templates/emails/feedback_request_url.html b/frappe/templates/emails/feedback_request_url.html index b6d4de7f32..e9cec87b4d 100644 --- a/frappe/templates/emails/feedback_request_url.html +++ b/frappe/templates/emails/feedback_request_url.html @@ -5,5 +5,5 @@ -

    {{ _("1 star being lowest & 5 stars being highest rating") }}

    +
    {{ _("1 star being lowest & 5 stars being highest rating") }}
    diff --git a/frappe/templates/emails/new_message.html b/frappe/templates/emails/new_message.html index a4a2ea933d..90f402f51d 100644 --- a/frappe/templates/emails/new_message.html +++ b/frappe/templates/emails/new_message.html @@ -1,5 +1,5 @@ -

    New Message

    You have a new message from: {{ from }}

    {{ message }}

    -
    -

    Login and view in Browser

    + diff --git a/frappe/templates/emails/upcoming_events.html b/frappe/templates/emails/upcoming_events.html new file mode 100644 index 0000000000..8f9482b98d --- /dev/null +++ b/frappe/templates/emails/upcoming_events.html @@ -0,0 +1,9 @@ +{% for e in events %} +

    + {{ e.starts_on }}: {{ e.subject }} +

    {{ e.description or '' }}
    +

    +{% endfor %} +

    + {{ _("Daily Event Digest is sent for Calendar Events where reminders are set.") }} +

    \ No newline at end of file diff --git a/frappe/templates/includes/blog/blog.html b/frappe/templates/includes/blog/blog.html index 18617a3cf7..01e3e44d6b 100644 --- a/frappe/templates/includes/blog/blog.html +++ b/frappe/templates/includes/blog/blog.html @@ -5,6 +5,25 @@ {% block hero %}{% endblock %} {% block page_content %} +
    diff --git a/frappe/templates/includes/comments/comment.html b/frappe/templates/includes/comments/comment.html index 2d7e1603ed..e0f923b2e0 100644 --- a/frappe/templates/includes/comments/comment.html +++ b/frappe/templates/includes/comments/comment.html @@ -1,4 +1,4 @@ -
    +
    diff --git a/frappe/templates/includes/full_index.html b/frappe/templates/includes/full_index.html index 1f798c66ed..a7443c482a 100644 --- a/frappe/templates/includes/full_index.html +++ b/frappe/templates/includes/full_index.html @@ -2,10 +2,12 @@
      {% for item in children_map[route] %}
    1. - {{ item.title }} + {{ item.title }} + {# {% if children_map[item.route] %} {{ make_item_list(item.route, children_map) }} {% endif %} + #}
    2. {% endfor %}
    diff --git a/frappe/templates/includes/integrations/third_party_apps.js b/frappe/templates/includes/integrations/third_party_apps.js new file mode 100644 index 0000000000..fe75f23db9 --- /dev/null +++ b/frappe/templates/includes/integrations/third_party_apps.js @@ -0,0 +1,9 @@ +frappe.ready(() => { + $(".btn-delete-app").on("click", function(event) { + frappe.call({ + method:"frappe.www.third_party_apps.delete_client", + args: {"client_id": $(this).data("client_id"), + } + }).done(r => location.href="/third_party_apps"); + }); +}); diff --git a/frappe/templates/includes/web_sidebar.html b/frappe/templates/includes/web_sidebar.html index 764ce4d277..d452e8af3f 100644 --- a/frappe/templates/includes/web_sidebar.html +++ b/frappe/templates/includes/web_sidebar.html @@ -22,7 +22,7 @@ {% endif %} {% for item in sidebar_items -%}
    diff --git a/frappe/www/message.html b/frappe/www/message.html index 0eb183fcfd..4697534a2e 100644 --- a/frappe/www/message.html +++ b/frappe/www/message.html @@ -3,7 +3,19 @@ {% block title %}{{ title or _("Message") }}{% endblock %} {% block page_content %} - +
    @@ -18,17 +30,11 @@ {% if error_code %}

    {{ _("Status: {0}").format(error_code) }}

    {% endif %} - + {% endblock %} diff --git a/frappe/www/rss.py b/frappe/www/rss.py index 7b57984199..722669a0ef 100644 --- a/frappe/www/rss.py +++ b/frappe/www/rss.py @@ -5,6 +5,7 @@ from __future__ import unicode_literals import frappe import urllib from frappe.utils import escape_html, get_request_site_address, now, cstr +from six.moves.urllib.parse import quote no_cache = 1 base_template_path = "templates/www/rss.xml" @@ -20,7 +21,7 @@ def get_context(context): order by published_on desc limit 20""", as_dict=1) for blog in blog_list: - blog_page = cstr(urllib.quote(blog.name.encode("utf-8"))) + blog_page = cstr(quote(blog.name.encode("utf-8"))) blog.link = urllib.basejoin(host, blog_page) blog.content = escape_html(blog.content or "") diff --git a/frappe/www/sitemap.py b/frappe/www/sitemap.py index 3740e57fdf..44201167ad 100644 --- a/frappe/www/sitemap.py +++ b/frappe/www/sitemap.py @@ -8,6 +8,7 @@ import frappe from frappe.utils import get_request_site_address, get_datetime, nowdate from frappe.website.router import get_pages, get_all_page_context_from_doctypes from six import iteritems +from six.moves.urllib.parse import quote no_cache = 1 no_sitemap = 1 @@ -20,13 +21,13 @@ def get_context(context): for route, page in iteritems(get_pages()): if not page.no_sitemap: links.append({ - "loc": urllib.basejoin(host, urllib.quote(page.name.encode("utf-8"))), + "loc": urllib.basejoin(host, quote(page.name.encode("utf-8"))), "lastmod": nowdate() }) for route, data in iteritems(get_all_page_context_from_doctypes()): links.append({ - "loc": urllib.basejoin(host, urllib.quote((route or "").encode("utf-8"))), + "loc": urllib.basejoin(host, quote((route or "").encode("utf-8"))), "lastmod": get_datetime(data.get("modified")).strftime("%Y-%m-%d") }) diff --git a/frappe/www/third_party_apps.html b/frappe/www/third_party_apps.html new file mode 100644 index 0000000000..baf62fa641 --- /dev/null +++ b/frappe/www/third_party_apps.html @@ -0,0 +1,66 @@ +{% extends "templates/web.html" %} + +{% block title %} {{ _("Third Party Apps") }} {% endblock %} +{% block header %} +

    {{ _("Third Party Apps") }}

    +{% endblock %} + +{% block page_sidebar %} +{% include "templates/pages/web_sidebar.html" %} +{% endblock %} + +{% block style %} +{% endblock %} + +{% block page_content %} + + + +
    + +{% if app %} +

    {{ app.app_name }}

    +
    +
    +
    +
    +
    {{ _("This will log out {0} from all other devices".format(app.app_name)) }}
    +
    +
    + + +
    +
    +
    +
    +
    +{% elif apps|length > 0 %} +

    {{ _("Active Sessions") }}

    + {% for app in apps %} +
    +
    +
    + {{ app.app_name }} +
    +
    + + {{ _("logged in") }} {{ frappe.utils.pretty_date(app.creation) }} + +
    + +
    +
    + {% endfor %} +{% else %} +
    + {{ _("No Active Sessions")}} +
    +{% endif %} +
    + + +{% endblock %} diff --git a/frappe/www/third_party_apps.py b/frappe/www/third_party_apps.py new file mode 100644 index 0000000000..b70f795948 --- /dev/null +++ b/frappe/www/third_party_apps.py @@ -0,0 +1,53 @@ +from __future__ import unicode_literals +import frappe +from frappe import _ +import frappe.www.list + +no_cache = 1 +no_sitemap = 1 + +def get_context(context): + if frappe.session.user == 'Guest': + frappe.throw(_("You need to be logged in to access this page"), frappe.PermissionError) + + active_tokens = frappe.get_all("OAuth Bearer Token", + filters=[["user", "=", frappe.session.user]], + fields=["client"], distinct=True, order_by="creation") + + client_apps = [] + + for token in active_tokens: + creation = get_first_login(token.client) + app = { + "name": token.get("client"), + "app_name": frappe.db.get_value("OAuth Client", token.get("client"), "app_name"), + "creation": creation + } + client_apps.append(app) + + app = None + if (frappe.form_dict.has_key("app")): + app = frappe.get_doc("OAuth Client", frappe.form_dict.app) + app = app.__dict__ + app["client_secret"] = None + + if app: + context.app = app + + context.apps = client_apps + context.show_sidebar = True + +def get_first_login(client): + login_date = frappe.get_all("OAuth Bearer Token", + filters=[["user", "=", frappe.session.user], ["client", "=", client]], + fields=["creation"], order_by="creation", limit=1) + + login_date = login_date[0].get("creation") if login_date and len(login_date) > 0 else None + + return login_date + +@frappe.whitelist() +def delete_client(client_id): + active_client_id_tokens = frappe.get_all("OAuth Bearer Token", filters=[["user", "=", frappe.session.user], ["client","=", client_id]]) + for token in active_client_id_tokens: + frappe.delete_doc("OAuth Bearer Token", token.get("name"), ignore_permissions=True) \ No newline at end of file diff --git a/frappe/www/update_password.py b/frappe/www/update_password.py index 5ed7ad913d..7dc23cd494 100644 --- a/frappe/www/update_password.py +++ b/frappe/www/update_password.py @@ -8,4 +8,5 @@ no_sitemap = 1 no_cache = 1 def get_context(context): + context.no_breadcrumbs = True context.parents = [{"name":"me", "title":_("My Account")}] diff --git a/frappe/www/website_script.js b/frappe/www/website_script.js index 3b84bdd084..fd361f8986 100644 --- a/frappe/www/website_script.js +++ b/frappe/www/website_script.js @@ -1,7 +1,8 @@ +// website_script.js {% if javascript -%}{{ javascript }}{%- endif %} {% if google_analytics_id -%} - +// Google Analytics (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) @@ -9,5 +10,5 @@ m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) ga('create', '{{ google_analytics_id }}', 'auto'); ga('send', 'pageview'); - +// End Google Analytics {%- endif %} diff --git a/frappe/www/website_script.py b/frappe/www/website_script.py index 2f5ee0d654..0db00bc3d8 100644 --- a/frappe/www/website_script.py +++ b/frappe/www/website_script.py @@ -10,7 +10,8 @@ no_sitemap = 1 base_template_path = "templates/www/website_script.js" def get_context(context): - context.javascript = frappe.db.get_single_value('Website Script', 'javascript') or "" + context.javascript = frappe.db.get_single_value('Website Script', + 'javascript') or "" theme = get_active_theme() js = strip(theme and theme.js or "") diff --git a/hooks.md b/hooks.md index 5ff70471c9..3e394f68a1 100644 --- a/hooks.md +++ b/hooks.md @@ -3,7 +3,7 @@ #### Application Name and Details 1. `app_name` - slugified name e.g. "frappe" -1. `app_title` - full title name e.g. "Frappe" +1. `app_title` - full title name e.g. "Frappé" 1. `app_publisher` 1. `app_description` 1. `app_version`
    Sr