@@ -118,6 +118,8 @@ | |||
"getCookie": true, | |||
"getCookies": true, | |||
"get_url_arg": true, | |||
"QUnit": true | |||
"QUnit": true, | |||
"Snap": true, | |||
"mina": true | |||
} | |||
} |
@@ -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 | |||
@@ -6,4 +6,4 @@ | |||
1. | |||
Frappe version: | |||
Frappé version: |
@@ -1,4 +1,4 @@ | |||
## Frappe Framework | |||
## Frappé Framework | |||
[](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 | |||
@@ -1,4 +1,4 @@ | |||
## Frappe framework includes these public works | |||
## Frappé framework includes these public works | |||
### Javascript / CSS | |||
@@ -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 | |||
@@ -14,7 +14,7 @@ import os, sys, importlib, inspect, json | |||
from .exceptions import * | |||
from .utils.jinja import get_jenv, get_template, render_template, get_email_from_template | |||
__version__ = '8.6.8' | |||
__version__ = '8.7.0' | |||
__title__ = "Frappe Framework" | |||
local = Local() | |||
@@ -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): | |||
@@ -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(): | |||
@@ -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 | |||
@@ -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): | |||
@@ -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") | |||
@@ -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]; | |||
@@ -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))) | |||
@@ -1,2 +1 @@ | |||
- Developer Tutorial [Videos](http://frappe.github.io/frappe/user/videos/) | |||
- Increased uploaded file size limit upto 10MB |
@@ -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 | |||
@@ -0,0 +1,2 @@ | |||
### User Permissions | |||
- User Permission is now a DocType, a new UX for the existing Role and User Permission managers to make it easy to enter permissions. For more details please check <a href="https://erpnext.org/docs/user/manual/en/setting-up/users-and-permissions/user-permissions">User Permissions</a> |
@@ -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() | |||
@@ -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() | |||
@@ -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, | |||
] |
@@ -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) | |||
@@ -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 = ('<img class="brand-logo" src="'+context.docs_base_url | |||
+'/assets/img/frappe-bird-white.png"> Frappé Framework</img>') | |||
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" |
@@ -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", | |||
@@ -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): | |||
@@ -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 | |||
@@ -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()) | |||
@@ -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", | |||
@@ -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() | |||
]); | |||
}); |
@@ -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: | |||
@@ -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 | |||
@@ -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)) |
@@ -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 "<!-- jinja -->" in template: | |||
context = frappe._dict({}) | |||
try: | |||
@@ -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() | |||
]); | |||
}); |
@@ -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 | |||
} | |||
} |
@@ -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(): | |||
@@ -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() | |||
]); | |||
}); |
@@ -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); | |||
} | |||
@@ -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 = """<p> | |||
{dear_system_manager} <br><br> | |||
{access_message} <br><br> | |||
{is_it_unauthorized} | |||
</p>""".format( | |||
dear_system_manager=_("Dear System Manager,"), | |||
site = '<a href="{0}" target="_blank">{0}</a>'.format(frappe.local.request.host_url) | |||
date_and_time = '<b>{0}</b>'.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( | |||
"""<a href="{site}" target="_blank">{site}</a>""".format(site=frappe.local.request.host_url), | |||
"""<b>{date_and_time}</b>""".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""" | |||
@@ -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() | |||
]); | |||
}); |
@@ -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 |
@@ -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})); | |||
} | |||
}); |
@@ -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 | |||
} |
@@ -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 |
@@ -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) | |||
@@ -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"]) | |||
@@ -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'); | |||
@@ -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("<p class='text-muted'>" + __("Loading") + "...</div>"); | |||
this.body.html("<p class='text-muted'>" + __("Loading") + "...</p>"); | |||
return; | |||
} | |||
if(!me.get_doctype() && !me.get_role()) { | |||
this.body.html("<p class='text-muted'>"+__("Select Document Type or Role to start.")+"</div>"); | |||
this.body.html("<p class='text-muted'>"+__("Select Document Type or Role to start.")+"</p>"); | |||
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('<ul class="user-permission-help small hidden" style="margin-left: -10px;">\ | |||
<li style="margin-top: 7px;"><a class="show-user-permission-doctypes grey">{%= __("Select Document Types") %}</a></li>\ | |||
<li style="margin-top: 3px;"><a class="show-user-permissions grey">{%= __("Show User Permissions") %}</a></li>\ | |||
</ul>', {}); | |||
d.help = `<ul class="user-permission-help small hidden" | |||
style="margin-left: -10px;"> | |||
<li style="margin-top: 7px;"><a class="show-user-permission-doctypes"> | |||
${__("Select Document Types")}</a></li> | |||
<li style="margin-top: 3px;"><a class="show-user-permissions"> | |||
${__("Show User Permissions")}</a></li> | |||
</ul>`; | |||
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() { | |||
@@ -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): | |||
@@ -1 +0,0 @@ | |||
Interface to set user defaults (DefaultValue). |
@@ -1,3 +0,0 @@ | |||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors | |||
# MIT License. See license.txt | |||
@@ -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"); | |||
$("<div class='user-settings' \ | |||
style='min-height: 200px; padding: 15px;'></div>\ | |||
<p style='padding: 15px; padding-bottom: 0px;'>\ | |||
<a class='view-role-permissions grey'>" + __("Edit Role Permissions") + "</a>\ | |||
</p><hr><div style='padding: 0px 15px;'>\ | |||
<h4>"+__("Help for User Permissions")+":</h4>\ | |||
<ol>\ | |||
<li>" | |||
+ __("Apart from Role based Permission Rules, you can apply User Permissions based on DocTypes.") | |||
+ "</li>" | |||
+ "<li>" | |||
+ __("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.") | |||
+ "</li>" | |||
+ "<li>" | |||
+ __("These will also be set as default values for those links, if only one such permission record is defined.") | |||
+ "</li>" | |||
+ "<li>" | |||
+ __("A user can be permitted to multiple records of the same DocType.") | |||
+ "</li>\ | |||
</ol></div>").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: '<p class="text-muted"><ol>'+ | |||
"<li>"+__("Upload CSV file containing all user permissions in the same format as Download.")+"</li>"+ | |||
"<li><strong>"+__("Any existing permission will be deleted / overwritten.")+"</strong></li>"+ | |||
'</p>' | |||
}, | |||
{ | |||
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()) { | |||
$('<button class="btn btn-default btn-sm" style="margin-left: 10px;">\ | |||
Show Allowed Documents</button>').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) { | |||
$('<p class="text-muted">' + txt + '</p>').appendTo(this.body); | |||
}, | |||
refresh: function() { | |||
var me = this; | |||
if(!me.filters.user) { | |||
this.body.html("<p class='text-muted'>"+__("Loading")+"...</p>"); | |||
return; | |||
} | |||
if(!me.get_user() && !me.get_doctype()) { | |||
this.body.html("<p class='text-muted'>"+__("Select User or DocType to start.")+"</p>"); | |||
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 = $("<table class='table table-bordered'>\ | |||
<thead><tr></tr></thead>\ | |||
<tbody></tbody>\ | |||
</table>").appendTo(this.body); | |||
$('<p class="text-muted small">' | |||
+__("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.") | |||
+'</p>').appendTo(this.body); | |||
$.each([[__("Allow User"), 150], [__("If Document Type"), 150], [__("Is"),150], ["", 50]], | |||
function(i, col) { | |||
$("<th>") | |||
.html(col[0]) | |||
.css("width", col[1]+"px") | |||
.appendTo(me.table.find("thead tr")); | |||
}); | |||
$.each(this.prop_list, function(i, d) { | |||
var row = $("<tr>").appendTo(me.table.find("tbody")); | |||
$("<td>").html('<a class="grey" href="#Form/User/'+encodeURIComponent(d.parent)+'">' | |||
+d.parent+'</a>').appendTo(row); | |||
$("<td>").html(d.defkey).appendTo(row); | |||
$("<td>").html(d.defvalue).appendTo(row); | |||
me.add_delete_button(row, d); | |||
}); | |||
}, | |||
add_delete_button: function(row, d) { | |||
var me = this; | |||
$("<button class='btn btn-sm btn-default'><i class='fa fa-remove'></i></button>") | |||
.appendTo($("<td>").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; | |||
$("<button class='btn btn-default btn-sm'>"+__("Add A User Restriction")+"</button>") | |||
.appendTo($('<p class="btn-area">').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(); | |||
}); | |||
} | |||
}) |
@@ -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" | |||
} |
@@ -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]) |
@@ -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 | |||
@@ -7,6 +7,7 @@ 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), | |||
() => { | |||
@@ -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) | |||
@@ -881,10 +881,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 | |||
@@ -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() | |||
@@ -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 = "<h3>" + frappe._("Events In Today's Calendar") + "</h3>" | |||
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 += "<h4>%(starts_on)s: %(subject)s</h4><p>%(description)s</p>" % e | |||
text += '<p style="color: #888; font-size: 80%; margin-top: 20px; padding-top: 10px; border-top: 1px solid #eee;">'\ | |||
+ frappe._("Daily Event Digest is sent for Calendar Events where reminders are set.")+'</p>' | |||
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): | |||
@@ -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}) | |||
@@ -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) | |||
@@ -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(' ','') |
@@ -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']: | |||
@@ -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 |
@@ -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 | |||
@@ -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 | |||
@@ -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 | |||
@@ -1,10 +0,0 @@ | |||
<!-- title: Table of Contents --> | |||
<h1>Table of Contents</h1> | |||
<br> | |||
{% include "templates/includes/full_index.html" %} | |||
<!-- autodoc --> | |||
<!-- jinja --> | |||
<!-- no-breadcrumbs --> |
@@ -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() |
@@ -1,57 +0,0 @@ | |||
<!-- title: Frappe Framework: Documentation --> | |||
<!-- description: Superhero Web Framework --> | |||
<!-- no-breadcrumbs --> | |||
<style> | |||
</style> | |||
<!-- start-hero --> | |||
<div class="splash"> | |||
<div class="container"> | |||
<div class="col-sm-10 col-sm-offset-1"> | |||
<div class="jumbotron"> | |||
<h1>Superhero Web Framework</h1> | |||
<p>Build extensions to ERPNext or make your own app</p> | |||
</div> | |||
<div class="section" style="padding-top: 0px; margin-top: -30px;"> | |||
<div class="fake-browser-frame"> | |||
<img class="img-responsive browser-image feature-image" | |||
src="assets/img/home.png"> | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
<!-- end-hero --> | |||
<div class="container"> | |||
<div class="col-sm-10 col-sm-offset-1"> | |||
<div class="section"> | |||
<p>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.</p> | |||
<p>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 <strong>Desk</strong> | |||
that handles forms, navigation, lists, menus, permissions, file attachment | |||
and much more out of the box.</p> | |||
<p>Frappe also has a plug-in architecture that can be used to build plugins | |||
to ERPNext.</p> | |||
<p>Frappe Framework was designed to build <a href="https://erpnext.com">ERPNext</a>, open source | |||
ERP for managing small and medium sized businesses.</p> | |||
<p><a href="https://frappe.github.io/frappe/user/">Get started with the Tutorial</a></p> | |||
</div> | |||
</div> | |||
</div> | |||
<!-- autodoc --> | |||
<!-- jinja --> |
@@ -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). |
@@ -1,6 +0,0 @@ | |||
assets | |||
user | |||
contents | |||
current | |||
install | |||
license |
@@ -1,30 +0,0 @@ | |||
<!-- title: Frappe Framework Installation --> | |||
# Installation | |||
Frappe Framework is based on the <a href="https://frappe.io">Frappe Framework</a>, a full stack web framework based on Python, MariaDB, Redis, Node. | |||
To intall Frappe Framework, you will have to install the <a href="https://github.com/frappe/bench">Frappe Bench</a>, 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. | |||
<!-- jinja --> | |||
<!-- autodoc --> |
@@ -1,16 +0,0 @@ | |||
<!-- title: License MIT --> | |||
<h1>MIT</h1> | |||
<p>The MIT License (MIT)</p> | |||
<p>Copyright (c) 2016 Frappe Technologies Pvt. Ltd.</p> | |||
<p>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:</p> | |||
<p>The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.</p> | |||
<p>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.</p> | |||
<!-- autodoc --> |
@@ -1,3 +1,5 @@ | |||
# Diagnosing The Scheduler | |||
<!-- markdown --> | |||
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. | |||
@@ -1 +1,5 @@ | |||
# Guides | |||
{index} |
@@ -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. | |||
@@ -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. | |||
<img class="screenshot" alt="Doctype Saved" src="{{docs_base_url}}/assets/img/usage_info.png"> | |||
<img class="screenshot" alt="Doctype Saved" src="/docs/assets/img/usage_info.png"> |
@@ -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 | |||
@@ -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 | |||
@@ -1 +1,3 @@ | |||
# Bench | |||
{index} |
@@ -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. |
@@ -1,3 +1,5 @@ | |||
# Bench Commands Cheatsheet | |||
### General Usage | |||
* `bench --version` - Show bench version | |||
* `bench src` - Show bench repo directory | |||
@@ -1,3 +1,5 @@ | |||
# Bench Procfile | |||
`bench start` uses [honcho](http://honcho.readthedocs.org) to manage multiple processes in **developer mode**. | |||
### Processes | |||
@@ -1 +1,5 @@ | |||
# Resources | |||
{index} |
@@ -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, | |||
<img class="screenshot" alt="Custom Button" src="{{docs_base_url}}/assets/img/app-development/add_custom_button.png"> | |||
<img class="screenshot" alt="Custom Button" src="/docs/assets/img/app-development/add_custom_button.png"> | |||
<!-- markdown --> |
@@ -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 | |||
@@ -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.<br> | |||
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" | |||
} | |||
} | |||
@@ -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 | |||
<img class="screenshot" src="{{docs_base_url}}/assets/img/app-development/show_alert.png"> | |||
<img class="screenshot" src="/docs/assets/img/app-development/show_alert.png"> | |||
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 | |||
<img class="screenshot" src="{{docs_base_url}}/assets/img/app-development/prompt.png"> | |||
<img class="screenshot" src="/docs/assets/img/app-development/prompt.png"> | |||
Is helpful for ask a value for the user | |||
@@ -42,7 +44,7 @@ This dialog have 4 parameters, they are: | |||
--- | |||
## Confirm Dialog | |||
<img class="screenshot" src="{{docs_base_url}}/assets/img/app-development/confirm-dialog.png"> | |||
<img class="screenshot" src="/docs/assets/img/app-development/confirm-dialog.png"> | |||
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 | |||
<img class="screenshot" src="{{docs_base_url}}/assets/img/app-development/msgprint.png"> | |||
<img class="screenshot" src="/docs/assets/img/app-development/msgprint.png"> | |||
Is helpfull for show a informational dialog for the user; | |||
@@ -91,7 +93,7 @@ This dialog have 2 arguments, they are: | |||
### Custom Dialog | |||
<img class="screenshot" src="{{docs_base_url}}/assets/img/app-development/dialog.png"> | |||
<img class="screenshot" src="/docs/assets/img/app-development/dialog.png"> | |||
Frappé provide too a `Class` that you can extend and build your own custom dialogs | |||
@@ -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 | |||
@@ -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** | |||
<img class="screenshot" src="{{ docs_base_url }}/assets/img/app-development/export-custom-1.png"> | |||
<img class="screenshot" src="/docs/assets/img/app-development/export-custom-1.png"> | |||
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 | |||
<img class="screenshot" src="{{ docs_base_url }}/assets/img/app-development/export-custom-2.png"> | |||
<img class="screenshot" src="/docs/assets/img/app-development/export-custom-2.png"> | |||
When you do `bench update` or `bench migrate` these customizations will be synced to the app. |
@@ -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**. | |||
<img class="screenshot" src="{{ docs_base_url }}/assets/img/add-vat-number-in-supplier.png"> | |||
<img class="screenshot" src="/docs/assets/img/add-vat-number-in-supplier.png"> | |||
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`. | |||
<img class="screenshot" src="{{ docs_base_url }}/assets/img/add-vat-number-in-purchase-order.png"> | |||
<img class="screenshot" src="/docs/assets/img/add-vat-number-in-purchase-order.png"> | |||
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. | |||
<img class="screenshot" src="{{ docs_base_url }}/assets/img/vat-number-fetched.png"> | |||
<img class="screenshot" src="/docs/assets/img/vat-number-fetched.png"> |
@@ -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 %}<a href="{{ docs_base_url }}/user/link/to/page.html">Link Description</a>{% endraw %} | |||
{% raw %}<a href="/docs/user/link/to/page.html">Link Description</a>{% endraw %} | |||
### 4. Adding Images | |||
You can add images in the `/docs/assets` folder. You can add links to the images as follows: | |||
{% raw %}<img src="{{ docs_base_url }}/assets/img/my-img/gif" class="screenshot">{% 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 %}<img src="/docs/assets/img/my-img/gif" class="screenshot">{% 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 |
@@ -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: | |||
@@ -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"` | |||