diff --git a/.gitignore b/.gitignore index 27b2369dab..a45a03de15 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,3 @@ locale *.egg-info dist/ build/ -docs/ diff --git a/.travis.yml b/.travis.yml index 464256b1dd..1c4e8acbc0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,8 +14,6 @@ install: - sudo apt-get purge -y mysql-common - wget https://raw.githubusercontent.com/frappe/bench/master/install_scripts/setup_frappe.sh - sudo bash setup_frappe.sh --skip-setup-bench --mysql-root-password travis - - sudo pip install --upgrade pip - - sudo service redis-server start - rm $TRAVIS_BUILD_DIR/.git/shallow - cd ~/ && bench init frappe-bench --frappe-path $TRAVIS_BUILD_DIR - cp -r $TRAVIS_BUILD_DIR/test_sites/test_site ~/frappe-bench/sites/ @@ -23,10 +21,13 @@ install: script: - cd ~/frappe-bench - bench use test_site + - bench setup redis-cache + - bench setup redis-async-broker + - bench setup procfile --with-celery-broker - bench reinstall - bench build - bench build-website - - bench serve & + - bench start & - sleep 10 - bench --verbose run-tests --driver Firefox diff --git a/README.md b/README.md index d2010dd1bd..1de665c5c6 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ -## frappe [![Build Status](https://travis-ci.org/frappe/frappe.png)](https://travis-ci.org/frappe/frappe) +## Frappe Framework + +[![Build Status](https://travis-ci.org/frappe/frappe.png)](https://travis-ci.org/frappe/frappe) Full-stack web application framework that uses Python and MariaDB on the server side and a tightly integrated client side library. [Built for ERPNext](https://erpnext.com) diff --git a/frappe/__init__.py b/frappe/__init__.py index b2f8a1b614..30e4e1d3b3 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -7,6 +7,7 @@ globals attached to frappe module from __future__ import unicode_literals from werkzeug.local import Local, release_local +from functools import wraps import os, importlib, inspect, logging, json # public @@ -14,6 +15,7 @@ from frappe.__version__ import __version__ from .exceptions import * from .utils.jinja import get_jenv, get_template, render_template + local = Local() class _dict(dict): @@ -36,13 +38,16 @@ class _dict(dict): def copy(self): return _dict(dict(self).copy()) -def _(msg): +def _(msg, lang=None): """Returns translated string in current lang, if exists.""" - if local.lang == "en": + if not lang: + lang = local.lang + + if lang == "en": return msg from frappe.translate import get_full_dict - return get_full_dict(local.lang).get(msg, msg) + return get_full_dict(local.lang).get(msg) or msg def get_lang_dict(fortype, name=None): """Returns the translated language dict for the given type and name. @@ -64,7 +69,6 @@ db = local("db") conf = local("conf") form = form_dict = local("form_dict") request = local("request") -request_method = local("request_method") response = local("response") session = local("session") user = local("user") @@ -87,6 +91,7 @@ def init(site, sites_path=None): local.error_log = [] local.message_log = [] local.debug_log = [] + local.realtime_log = [] local.flags = _dict({ "ran_schedulers": [], "redirect_location": "", @@ -106,9 +111,9 @@ def init(site, sites_path=None): local.sites_path = sites_path local.site_path = os.path.join(sites_path, site) - local.request_method = request.method if request else None local.request_ip = None local.response = _dict({"docs":[]}) + local.task_id = None local.conf = _dict(get_site_config()) local.lang = local.conf.lang or "en" @@ -239,7 +244,7 @@ def msgprint(msg, small=0, raise_exception=0, as_table=False): msg = '' + ''.join([''+''.join(['' % c for c in r])+'' for r in msg]) + '
%s
' if flags.print_messages: - print "Message: " + repr(msg) + print "Message: " + repr(msg).encode("utf-8") message_log.append((small and '__small:' or '')+cstr(msg or '')) _raise_exception() @@ -305,7 +310,8 @@ def sendmail(recipients=(), sender="", subject="No Subject", message="No Message as_markdown=False, bulk=False, reference_doctype=None, reference_name=None, unsubscribe_method=None, unsubscribe_params=None, unsubscribe_message=None, attachments=None, content=None, doctype=None, name=None, reply_to=None, - cc=(), message_id=None, as_bulk=False, send_after=None): + cc=(), show_as_cc=(), message_id=None, as_bulk=False, send_after=None, expose_recipients=False, + bulk_priority=1): """Send email using user's default **Email Account** or global default **Email Account**. @@ -315,6 +321,7 @@ def sendmail(recipients=(), sender="", subject="No Subject", message="No Message :param message: (or `content`) Email Content. :param as_markdown: Convert content markdown to HTML. :param bulk: Send via scheduled email sender **Bulk Email**. Don't send immediately. + :param bulk_priority: Priority for bulk email, default 1. :param reference_doctype: (or `doctype`) Append as communication to this DocType. :param reference_name: (or `name`) Append as communication to this document name. :param unsubscribe_method: Unsubscribe url with options email, doctype, name. e.g. `/api/method/unsubscribe` @@ -323,6 +330,7 @@ def sendmail(recipients=(), sender="", subject="No Subject", message="No Message :param reply_to: Reply-To email id. :param message_id: Used for threading. If a reply is received to this email, Message-Id is sent back as In-Reply-To in received email. :param send_after: Send after the given datetime. + :param expose_recipients: Display all recipients in the footer message - "This email was sent to" """ if bulk or as_bulk: @@ -331,7 +339,8 @@ def sendmail(recipients=(), sender="", subject="No Subject", message="No Message subject=subject, message=content or message, reference_doctype = doctype or reference_doctype, reference_name = name or reference_name, unsubscribe_method=unsubscribe_method, unsubscribe_params=unsubscribe_params, unsubscribe_message=unsubscribe_message, - attachments=attachments, reply_to=reply_to, cc=cc, message_id=message_id, send_after=send_after) + attachments=attachments, reply_to=reply_to, cc=cc, show_as_cc=show_as_cc, message_id=message_id, send_after=send_after, + expose_recipients=expose_recipients, bulk_priority=bulk_priority) else: import frappe.email if as_markdown: @@ -346,7 +355,8 @@ def sendmail(recipients=(), sender="", subject="No Subject", message="No Message logger = None whitelisted = [] guest_methods = [] -def whitelist(allow_guest=False): +xss_safe_methods = [] +def whitelist(allow_guest=False, xss_safe=False): """ Decorator for whitelisting a function and making it accessible via HTTP. Standard request will be `/api/method/[path.to.method]` @@ -360,12 +370,15 @@ def whitelist(allow_guest=False): pass """ def innerfn(fn): - global whitelisted, guest_methods + global whitelisted, guest_methods, xss_safe_methods whitelisted.append(fn) if allow_guest: guest_methods.append(fn) + if xss_safe: + xss_safe_methods.append(fn) + return fn return innerfn @@ -405,7 +418,7 @@ def clear_cache(user=None, doctype=None): frappe.local.role_permissions = {} -def has_permission(doctype, ptype="read", doc=None, user=None, verbose=False): +def has_permission(doctype, ptype="read", doc=None, user=None, verbose=False, throw=False): """Raises `frappe.PermissionError` if not permitted. :param doctype: DocType for which permission is to be check. @@ -413,7 +426,14 @@ def has_permission(doctype, ptype="read", doc=None, user=None, verbose=False): :param doc: [optional] Checks User permissions for given doc. :param user: [optional] Check for given user. Default: current user.""" import frappe.permissions - return frappe.permissions.has_permission(doctype, ptype, doc=doc, verbose=verbose, user=user) + out = frappe.permissions.has_permission(doctype, ptype, doc=doc, verbose=verbose, user=user) + if throw and not out: + if doc: + frappe.throw(_("No permission for {0}").format(doc.doctype + " " + doc.name)) + else: + frappe.throw(_("No permission for {0}").format(doctype)) + + return out def has_website_permission(doctype, ptype="read", doc=None, user=None, verbose=False): """Raises `frappe.PermissionError` if not permitted. @@ -428,6 +448,9 @@ def has_website_permission(doctype, ptype="read", doc=None, user=None, verbose=F hooks = (get_hooks("has_website_permission") or {}).get(doctype, []) if hooks: + if isinstance(doc, basestring): + doc = get_doc(doctype, doc) + for method in hooks: result = call(get_attr(method), doc=doc, ptype=ptype, user=user, verbose=verbose) # if even a single permission check is Falsy @@ -443,7 +466,7 @@ def has_website_permission(doctype, ptype="read", doc=None, user=None, verbose=F def is_table(doctype): """Returns True if `istable` property (indicating child Table) is set for given DocType.""" def get_tables(): - return db.sql_list("select name from tabDocType where ifnull(istable,0)=1") + return db.sql_list("select name from tabDocType where istable=1") tables = cache().get_value("is_table", get_tables) return doctype in tables @@ -453,11 +476,14 @@ def get_precision(doctype, fieldname, currency=None, doc=None): from frappe.model.meta import get_field_precision return get_field_precision(get_meta(doctype).get_field(fieldname), doc, currency) -def generate_hash(txt=None): +def generate_hash(txt=None, length=None): """Generates random hash for given text + current timestamp + random string.""" import hashlib, time from .utils import random_string - return hashlib.sha224((txt or "") + repr(time.time()) + repr(random_string(8))).hexdigest() + digest = hashlib.sha224((txt or "") + repr(time.time()) + repr(random_string(8))).hexdigest() + if length: + digest = digest[:length] + return digest def reset_metadata_version(): """Reset `metadata_version` (Client (Javascript) build ID) hash.""" @@ -538,9 +564,9 @@ def delete_doc_if_exists(doctype, name): if db.exists(doctype, name): delete_doc(doctype, name) -def reload_doctype(doctype): +def reload_doctype(doctype, force=False): """Reload DocType from model (`[module]/[doctype]/[name]/[name].json`) files.""" - reload_doc(scrub(db.get_value("DocType", doctype, "module")), "doctype", scrub(doctype)) + reload_doc(scrub(db.get_value("DocType", doctype, "module")), "doctype", scrub(doctype), force=force) def reload_doc(module, dt=None, dn=None, force=False): """Reload Document from model (`[module]/[doctype]/[name]/[name].json`) files. @@ -597,7 +623,8 @@ def get_pymodule_path(modulename, *joins): :param modulename: Python module name. :param *joins: Join additional path elements using `os.path.join`.""" - joins = [scrub(part) for part in joins] + if not "public" in joins: + joins = [scrub(part) for part in joins] return os.path.join(os.path.dirname(get_module(scrub(modulename)).__file__), *joins) def get_module_list(app_name): @@ -623,6 +650,9 @@ def get_installed_apps(sort=False): if getattr(flags, "in_install_db", True): return [] + if not db: + connect() + installed = json.loads(db.get_global("installed_apps") or "[]") if sort: @@ -719,6 +749,9 @@ def get_file_json(path): def read_file(path, raise_not_found=False): """Open a file and return its content as Unicode.""" from frappe.utils import cstr + if isinstance(path, unicode): + path = path.encode("utf-8") + if os.path.exists(path): with open(path, "r") as f: return cstr(f.read()) @@ -729,6 +762,10 @@ def read_file(path, raise_not_found=False): def get_attr(method_string): """Get python method object from its name.""" + app_name = method_string.split(".")[0] + if not local.flags.in_install and app_name not in get_installed_apps(): + throw(_("App {0} is not installed").format(app_name), AppNotInstalledError) + modulename = '.'.join(method_string.split('.')[:-1]) methodname = method_string.split('.')[-1] return getattr(get_module(modulename), methodname) @@ -898,6 +935,20 @@ def get_all(doctype, *args, **kwargs): kwargs["limit_page_length"] = 0 return get_list(doctype, *args, **kwargs) +def get_value(*args, **kwargs): + """Returns a document property or list of properties. + + Alias for `frappe.db.get_value` + + :param doctype: DocType name. + :param filters: Filters like `{"x":"y"}` or name of the document. `None` if Single DocType. + :param fieldname: Column name. + :param ignore: Don't raise exception if table, column is missing. + :param as_dict: Return values as dict. + :param debug: Print query in error log. + """ + return db.get_value(*args, **kwargs) + def add_version(doc): """Insert a new **Version** of the given document. A **Version** is a JSON dump of the current document state.""" @@ -912,6 +963,9 @@ def as_json(obj, indent=1): from frappe.utils.response import json_handler return json.dumps(obj, indent=indent, sort_keys=True, default=json_handler) +def are_emails_muted(): + return flags.mute_emails or conf.get("mute_emails") or False + def get_test_records(doctype): """Returns list of objects from `test_records.json` in the given doctype's folder.""" from frappe.modules import get_doctype_module, get_module_path @@ -989,3 +1043,43 @@ def get_logger(module=None): logging_setup_complete = True return logging.getLogger(module or "frappe") + +def publish_realtime(*args, **kwargs): + """Publish real-time updates + + :param event: Event name, like `task_progress` etc. + :param message: JSON message object. For async must contain `task_id` + :param room: Room in which to publish update (default entire site) + :param user: Transmit to user + :param doctype: Transmit to doctype, docname + :param docname: Transmit to doctype, docname""" + import frappe.async + + return frappe.async.publish_realtime(*args, **kwargs) + +def local_cache(namespace, key, generator, regenerate_if_none=False): + """A key value store for caching within a request + + :param namespace: frappe.local.cache[namespace] + :param key: frappe.local.cache[namespace][key] used to retrieve value + :param generator: method to generate a value if not found in store + + """ + if namespace not in local.cache: + local.cache[namespace] = {} + + if key not in local.cache[namespace]: + local.cache[namespace][key] = generator() + + elif local.cache[namespace][key]==None and regenerate_if_none: + # if key exists but the previous result was None + local.cache[namespace][key] = generator() + + return local.cache[namespace][key] + +def get_doctype_app(doctype): + def _get_doctype_app(): + doctype_module = local.db.get_value("DocType", doctype, "module") + return local.module_app[scrub(doctype_module)] + + return local_cache("doctype_app", doctype, generator=_get_doctype_app) diff --git a/frappe/__version__.py b/frappe/__version__.py index 902d2412b9..08963105ce 100644 --- a/frappe/__version__.py +++ b/frappe/__version__.py @@ -1,2 +1,2 @@ from __future__ import unicode_literals -__version__ = "5.0.29" +__version__ = "6.16.4" diff --git a/frappe/api.py b/frappe/api.py index 425d48725c..5ec69d5f6d 100644 --- a/frappe/api.py +++ b/frappe/api.py @@ -58,13 +58,13 @@ def handle(): if frappe.local.request.method=="GET": if not doc.has_permission("read"): frappe.throw(_("Not permitted"), frappe.PermissionError) - doc.run_method(method, **frappe.local.form_dict) + frappe.local.response.update({"data": doc.run_method(method, **frappe.local.form_dict)}) if frappe.local.request.method=="POST": if not doc.has_permission("write"): frappe.throw(_("Not permitted"), frappe.PermissionError) - doc.run_method(method, **frappe.local.form_dict) + frappe.local.response.update({"data": doc.run_method(method, **frappe.local.form_dict)}) frappe.db.commit() else: diff --git a/frappe/app.py b/frappe/app.py index 4a645fa746..49c1991c4c 100644 --- a/frappe/app.py +++ b/frappe/app.py @@ -1,28 +1,31 @@ +# -*- coding: utf-8 -*- # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # MIT License. See license.txt from __future__ import unicode_literals -import sys, os -import json -import logging +import os import MySQLdb -from werkzeug.wrappers import Request, Response +from werkzeug.wrappers import Request from werkzeug.local import LocalManager from werkzeug.exceptions import HTTPException, NotFound from werkzeug.contrib.profiler import ProfilerMiddleware from werkzeug.wsgi import SharedDataMiddleware +from werkzeug.serving import run_with_reloader + -import mimetypes import frappe import frappe.handler import frappe.auth import frappe.api +import frappe.async import frappe.utils.response import frappe.website.render -from frappe.utils import get_site_name +from frappe.utils import get_site_name, get_site_path from frappe.middlewares import StaticDataMiddleware +from frappe.utils.error import make_error_snapshot + local_manager = LocalManager([frappe.local]) _site = None @@ -30,6 +33,21 @@ _sites_path = os.environ.get("SITES_PATH", ".") logger = frappe.get_logger() +class RequestContext(object): + + def __init__(self, environ): + self.request = Request(environ) + + def __enter__(self): + frappe.local.request = self.request + init_site(self.request) + make_form_dict(self.request) + frappe.local.http_request = frappe.auth.HTTPRequest() + + def __exit__(self, type, value, traceback): + frappe.destroy() + + @Request.application def application(request): frappe.local.request = request @@ -56,6 +74,9 @@ def application(request): elif frappe.request.path.startswith('/backups'): response = frappe.utils.response.download_backup(request.path) + elif frappe.request.path.startswith('/private/files/'): + response = frappe.utils.response.download_private_file(request.path) + elif frappe.local.request.method in ('GET', 'HEAD'): response = frappe.website.render.render(request.path) @@ -82,8 +103,12 @@ def application(request): if frappe.local.is_ajax or 'application/json' in request.headers.get('Accept', ''): response = frappe.utils.response.report_error(http_status_code) else: + traceback = "
"+frappe.get_traceback()+"
" + if frappe.local.flags.disable_traceback: + traceback = "" + frappe.respond_as_web_page("Server Error", - "
"+frappe.get_traceback()+"
", + traceback, http_status_code=http_status_code) response = frappe.website.render.render("message", http_status_code=http_status_code) @@ -94,10 +119,13 @@ def application(request): if http_status_code==500: logger.error('Request Error') + make_error_snapshot(e) + else: - if frappe.local.request.method in ("POST", "PUT") and frappe.db: - frappe.db.commit() - rollback = False + if (frappe.local.request.method in ("POST", "PUT") or frappe.local.flags.commit) and frappe.db: + if frappe.db.transaction_writes: + frappe.db.commit() + rollback = False # update session if getattr(frappe.local, "session_obj", None): @@ -105,6 +133,10 @@ def application(request): if updated_in_db: frappe.db.commit() + # publish realtime + for args in frappe.local.realtime_log: + frappe.async.emit_via_redis(*args) + finally: if frappe.local.request.method in ("POST", "PUT") and frappe.db and rollback: frappe.db.rollback() @@ -134,6 +166,8 @@ def make_form_dict(request): frappe.local.form_dict.pop("_") application = local_manager.make_middleware(application) +application.debug = True + def serve(port=8000, profile=False, site=None, sites_path='.'): global application, _site, _sites_path @@ -154,5 +188,10 @@ def serve(port=8000, profile=False, site=None, sites_path='.'): b'/files': os.path.abspath(sites_path).encode("utf-8") }) + application.debug = True + application.config = { + 'SERVER_NAME': 'localhost:8000' + } + run_simple('0.0.0.0', int(port), application, use_reloader=True, use_debugger=True, use_evalex=True, threaded=True) diff --git a/frappe/async.py b/frappe/async.py new file mode 100644 index 0000000000..a27bb7ee0e --- /dev/null +++ b/frappe/async.py @@ -0,0 +1,228 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals + + +import frappe +import os +import time +import redis +from functools import wraps +from frappe.utils import get_site_path +from frappe import conf + +END_LINE = '' +TASK_LOG_MAX_AGE = 86400 # 1 day in seconds +redis_server = None + +def handler(f): + cmd = f.__module__ + '.' + f.__name__ + + def run(args, set_in_response=True, hijack_std=False): + from frappe.tasks import run_async_task + from frappe.handler import execute_cmd + if frappe.conf.disable_async: + return execute_cmd(cmd, from_async=True) + args = frappe._dict(args) + task = run_async_task.delay(site=frappe.local.site, + user=(frappe.session and frappe.session.user) or 'Administrator', cmd=cmd, + form_dict=args, hijack_std=hijack_std) + if set_in_response: + frappe.local.response['task_id'] = task.id + return task.id + + @wraps(f) + def queue(*args, **kwargs): + task_id = run(frappe.local.form_dict, set_in_response=True) + return { + "status": "queued", + "task_id": task_id + } + + queue.async = True + queue.queue = f + queue.run = run + frappe.whitelisted.append(f) + frappe.whitelisted.append(queue) + return queue + + +@frappe.whitelist() +def get_pending_tasks_for_doc(doctype, docname): + return frappe.db.sql_list("select name from `tabAsync Task` where status in ('Queued', 'Running') and reference_doctype=%s and reference_name=%s", (doctype, docname)) + + +@handler +def ping(): + from time import sleep + sleep(1) + return "pong" + +@frappe.whitelist() +def get_task_status(task_id): + from frappe.celery_app import get_celery + c = get_celery() + a = c.AsyncResult(task_id) + frappe.local.response['response'] = a.result + return { + "state": a.state, + "progress": 0 + } + +def set_task_status(task_id, status, response=None): + if not response: + response = {} + response.update({ + "status": status, + "task_id": task_id + }) + emit_via_redis("task_status_change", response, room="task:" + task_id) + + +def remove_old_task_logs(): + logs_path = get_site_path('task-logs') + + def full_path(_file): + return os.path.join(logs_path, _file) + + files_to_remove = [full_path(_file) for _file in os.listdir(logs_path)] + files_to_remove = [_file for _file in files_to_remove if is_file_old(_file) and os.path.isfile(_file)] + for _file in files_to_remove: + os.remove(_file) + + +def is_file_old(file_path): + return ((time.time() - os.stat(file_path).st_mtime) > TASK_LOG_MAX_AGE) + + +def publish_realtime(event=None, message=None, room=None, user=None, doctype=None, docname=None, now=False): + """Publish real-time updates + + :param event: Event name, like `task_progress` etc. that will be handled by the client (default is `task_progress` if within task or `global`) + :param message: JSON message object. For async must contain `task_id` + :param room: Room in which to publish update (default entire site) + :param user: Transmit to user + :param doctype: Transmit to doctype, docname + :param docname: Transmit to doctype, docname""" + if message is None: + message = {} + + if event is None: + if frappe.local.task_id: + event = "task_progress" + else: + event = "global" + + if not room: + if frappe.local.task_id: + room = get_task_progress_room() + if not "task_id" in message: + message["task_id"] = frappe.local.task_id + + now = True + elif user: + room = get_user_room(user) + elif doctype and docname: + room = get_doc_room(doctype, docname) + else: + room = get_site_room() + + if now: + emit_via_redis(event, message, room) + else: + frappe.local.realtime_log.append([event, message, room]) + +def emit_via_redis(event, message, room): + """Publish real-time updates via redis + + :param event: Event name, like `task_progress` etc. + :param message: JSON message object. For async must contain `task_id` + :param room: name of the room""" + r = get_redis_server() + + try: + r.publish('events', frappe.as_json({'event': event, 'message': message, 'room': room})) + except redis.exceptions.ConnectionError: + # print frappe.get_traceback() + pass + +def put_log(line_no, line, task_id=None): + r = get_redis_server() + if not task_id: + task_id = frappe.local.task_id + task_progress_room = get_task_progress_room() + task_log_key = "task_log:" + task_id + publish_realtime('task_progress', { + "message": { + "lines": {line_no: line} + }, + "task_id": task_id + }, room=task_progress_room, now=True) + r.hset(task_log_key, line_no, line) + r.expire(task_log_key, 3600) + + +def get_redis_server(): + """Returns memcache connection.""" + global redis_server + if not redis_server: + from redis import Redis + redis_server = Redis.from_url(conf.get("async_redis_server") or "redis://localhost:12311") + return redis_server + + +class FileAndRedisStream(file): + def __init__(self, *args, **kwargs): + ret = super(FileAndRedisStream, self).__init__(*args, **kwargs) + self.count = 0 + return ret + + def write(self, data): + ret = super(FileAndRedisStream, self).write(data) + if frappe.local.task_id: + put_log(self.count, data, task_id=frappe.local.task_id) + self.count += 1 + return ret + + +def get_std_streams(task_id): + stdout = FileAndRedisStream(get_task_log_file_path(task_id, 'stdout'), 'w') + # stderr = FileAndRedisStream(get_task_log_file_path(task_id, 'stderr'), 'w') + return stdout, stdout + + +def get_task_log_file_path(task_id, stream_type): + logs_dir = frappe.utils.get_site_path('task-logs') + return os.path.join(logs_dir, task_id + '.' + stream_type) + + +@frappe.whitelist(allow_guest=True) +def can_subscribe_doc(doctype, docname, sid): + from frappe.sessions import Session + from frappe.exceptions import PermissionError + session = Session(None, resume=True).get_session_data() + if not frappe.has_permission(user=session.user, doctype=doctype, doc=docname, ptype='read'): + raise PermissionError() + return True + +@frappe.whitelist(allow_guest=True) +def get_user_info(sid): + from frappe.sessions import Session + session = Session(None, resume=True).get_session_data() + return { + 'user': session.user, + } + +def get_doc_room(doctype, docname): + return ''.join([frappe.local.site, ':doc:', doctype, '/', docname]) + +def get_user_room(user): + return ''.join([frappe.local.site, ':user:', user]) + +def get_site_room(): + return ''.join([frappe.local.site, ':all']) + +def get_task_progress_room(): + return "task_progress:" + frappe.local.task_id diff --git a/frappe/auth.py b/frappe/auth.py index 37caeb4781..8a644b3c1d 100644 --- a/frappe/auth.py +++ b/frappe/auth.py @@ -23,7 +23,7 @@ class HTTPRequest: self.domain = self.domain[4:] if frappe.get_request_header('X-Forwarded-For'): - frappe.local.request_ip = frappe.get_request_header('X-Forwarded-For') + frappe.local.request_ip = (frappe.get_request_header('X-Forwarded-For').split(",")[0]).strip() elif frappe.get_request_header('REMOTE_ADDR'): frappe.local.request_ip = frappe.get_request_header('REMOTE_ADDR') @@ -32,22 +32,19 @@ class HTTPRequest: frappe.local.request_ip = '127.0.0.1' # language - self.set_lang(frappe.request.accept_languages.values()) + self.set_lang() # load cookies frappe.local.cookie_manager = CookieManager() - # override request method. All request to be of type POST, but if _type == "POST" then commit - if frappe.form_dict.get("_type"): - frappe.local.request_method = frappe.form_dict.get("_type") - del frappe.form_dict["_type"] - # set db self.connect() # login frappe.local.login_manager = LoginManager() + self.validate_csrf_token() + # write out latest cookies frappe.local.cookie_manager.init_cookies() @@ -57,19 +54,25 @@ class HTTPRequest: # run login triggers if frappe.form_dict.get('cmd')=='login': frappe.local.login_manager.run_trigger('on_session_creation') - self.clear_active_sessions() - def clear_active_sessions(self): - if not frappe.conf.get("deny_multiple_sessions"): - return + def validate_csrf_token(self): + if frappe.local.request and frappe.local.request.method=="POST": + if not frappe.local.session.data.csrf_token or frappe.local.session.data.device=="mobile": + # not via boot + return - if frappe.session.user != "Guest": - clear_sessions(frappe.session.user, keep_current=True) + csrf_token = frappe.get_request_header("X-Frappe-CSRF-Token") + if not csrf_token and "csrf_token" in frappe.local.form_dict: + csrf_token = frappe.local.form_dict.csrf_token + del frappe.local.form_dict["csrf_token"] + if frappe.local.session.data.csrf_token != csrf_token: + frappe.local.flags.disable_traceback = True + frappe.throw(_("Invalid Request"), frappe.CSRFTokenError) - def set_lang(self, lang_codes): + def set_lang(self): from frappe.translate import guess_language - frappe.local.lang = guess_language(lang_codes) + frappe.local.lang = guess_language() def get_db_name(self): """get database name from conf""" @@ -89,8 +92,16 @@ class LoginManager: if frappe.local.form_dict.get('cmd')=='login' or frappe.local.request.path=="/api/method/login": self.login() + self.resume = False else: - self.make_session(resume=True) + try: + self.resume = True + self.make_session(resume=True) + self.set_user_info(resume=True) + except AttributeError: + self.user = "Guest" + self.make_session() + self.set_user_info() def login(self): # clear cache @@ -99,29 +110,34 @@ class LoginManager: self.post_login() def post_login(self): - self.info = frappe.db.get_value("User", self.user, - ["user_type", "first_name", "last_name", "user_image"], as_dict=1) - self.full_name = " ".join(filter(None, [self.info.first_name, self.info.last_name])) - self.user_type = self.info.user_type - self.run_trigger('on_login') self.validate_ip_address() self.validate_hour() self.make_session() self.set_user_info() - def set_user_info(self): + def set_user_info(self, resume=False): # set sid again frappe.local.cookie_manager.init_cookies() + self.info = frappe.db.get_value("User", self.user, + ["user_type", "first_name", "last_name", "user_image"], as_dict=1) + self.full_name = " ".join(filter(None, [self.info.first_name, + self.info.last_name])) + self.user_type = self.info.user_type + if self.info.user_type=="Website User": frappe.local.cookie_manager.set_cookie("system_user", "no") - frappe.local.response["message"] = "No App" + if not resume: + frappe.local.response["message"] = "No App" else: frappe.local.cookie_manager.set_cookie("system_user", "yes") - frappe.local.response['message'] = 'Logged In' + if not resume: + frappe.local.response['message'] = 'Logged In' + + if not resume: + frappe.response["full_name"] = self.full_name - frappe.response["full_name"] = self.full_name frappe.local.cookie_manager.set_cookie("full_name", self.full_name) frappe.local.cookie_manager.set_cookie("user_id", self.user) frappe.local.cookie_manager.set_cookie("user_image", self.info.user_image or "") @@ -134,6 +150,14 @@ class LoginManager: # reset user if changed to Guest self.user = frappe.local.session_obj.user frappe.local.session = frappe.local.session_obj.data + self.clear_active_sessions() + + def clear_active_sessions(self): + if not frappe.conf.get("deny_multiple_sessions"): + return + + if frappe.session.user != "Guest": + clear_sessions(frappe.session.user, keep_current=True) def authenticate(self, user=None, pwd=None): if not (user and pwd): diff --git a/frappe/boot.py b/frappe/boot.py index b63935d4c8..00943c1d1f 100644 --- a/frappe/boot.py +++ b/frappe/boot.py @@ -31,7 +31,6 @@ def get_bootinfo(): bootinfo['user_info'] = get_fullnames() bootinfo['sid'] = frappe.session['sid']; - # home page bootinfo.modules = {} for app in frappe.get_installed_apps(): try: @@ -45,7 +44,7 @@ def get_bootinfo(): bootinfo.hidden_modules = frappe.db.get_global("hidden_modules") bootinfo.doctype_icons = dict(frappe.db.sql("""select name, icon from tabDocType where ifnull(icon,'')!=''""")) - bootinfo.single_types = frappe.db.sql_list("""select name from tabDocType where ifnull(issingle,0)=1""") + bootinfo.single_types = frappe.db.sql_list("""select name from tabDocType where issingle=1""") add_home_page(bootinfo, doclist) bootinfo.page_info = get_allowed_pages() load_translations(bootinfo) @@ -53,6 +52,7 @@ def get_bootinfo(): load_conf_settings(bootinfo) load_print(bootinfo, doclist) doclist.extend(get_meta_bundle("Page")) + bootinfo.home_folder = frappe.db.get_value("File", {"is_home_folder": 1}) # ipinfo if frappe.session['data'].get('ipinfo'): @@ -69,13 +69,13 @@ def get_bootinfo(): bootinfo['versions'] = {k: v['version'] for k, v in get_versions().items()} bootinfo.error_report_email = frappe.get_hooks("error_report_email") - bootinfo.default_background_image = get_url("/assets/frappe/images/ui/into-the-dawn.jpg") + bootinfo.calendars = sorted(frappe.get_hooks("calendars")) return bootinfo def load_conf_settings(bootinfo): from frappe import conf - bootinfo.max_file_size = conf.get('max_file_size') or 5242880 + bootinfo.max_file_size = conf.get('max_file_size') or 10485760 for key in ['developer_mode']: if key in conf: bootinfo[key] = conf.get(key) @@ -104,16 +104,23 @@ def get_allowed_pages(): def load_translations(bootinfo): if frappe.local.lang != 'en': - bootinfo["__messages"] = frappe.get_lang_dict("boot") + messages = frappe.get_lang_dict("boot") + bootinfo["lang"] = frappe.lang + # load translated report names + for name in bootinfo.user.all_reports: + messages[name] = frappe._(name) + + bootinfo["__messages"] = messages + def get_fullnames(): """map of user fullnames""" ret = frappe.db.sql("""select name, concat(ifnull(first_name, ''), if(ifnull(last_name, '')!='', ' ', ''), ifnull(last_name, '')) as fullname, - user_image as image, gender, email - from tabUser where ifnull(enabled, 0)=1 and user_type!="Website User" """, as_dict=1) + user_image as image, gender, email, username + from tabUser where enabled=1 and user_type!="Website User" """, as_dict=1) d = {} for r in ret: @@ -132,10 +139,15 @@ def add_home_page(bootinfo, docs): if frappe.session.user=="Guest": return home_page = frappe.db.get_default("desktop:home_page") + + if home_page == "setup-wizard": + bootinfo.setup_wizard_requires = frappe.get_hooks("setup_wizard_requires") + try: page = frappe.desk.desk_page.get(home_page) except (frappe.DoesNotExistError, frappe.PermissionError): - frappe.message_log.pop() + if frappe.message_log: + frappe.message_log.pop() page = frappe.desk.desk_page.get('desktop') bootinfo['home_page'] = page.name @@ -154,4 +166,4 @@ def load_print(bootinfo, doclist): load_print_css(bootinfo, print_settings) def load_print_css(bootinfo, print_settings): - bootinfo.print_css = frappe.get_attr("frappe.templates.pages.print.get_print_style")(print_settings.print_style or "Modern") + bootinfo.print_css = frappe.get_attr("frappe.templates.pages.print.get_print_style")(print_settings.print_style or "Modern", for_legacy=True) diff --git a/frappe/build.py b/frappe/build.py index 42136f59fa..b91f8e85e4 100644 --- a/frappe/build.py +++ b/frappe/build.py @@ -127,7 +127,8 @@ def pack(target, sources, no_compress, verbose): tmpin, tmpout = StringIO(data.encode('utf-8')), StringIO() jsm.minify(tmpin, tmpout) minified = tmpout.getvalue() - outtxt += unicode(minified or '', 'utf-8').strip('\n') + ';' + if minified: + outtxt += unicode(minified or '', 'utf-8').strip('\n') + ';' if verbose: print "{0}: {1}k".format(f, int(len(minified) / 1024)) @@ -173,6 +174,10 @@ def files_dirty(): return False def compile_less(): + from distutils.spawn import find_executable + if not find_executable("lessc"): + return + for path in app_paths: less_path = os.path.join(path, "public", "less") if os.path.exists(less_path): @@ -188,4 +193,4 @@ def compile_less(): print "compiling {0}".format(fpath) css_path = os.path.join(path, "public", "css", fname.rsplit(".", 1)[0] + ".css") - os.system("which lessc && lessc {0} > {1}".format(fpath, css_path)) + os.system("lessc {0} > {1}".format(fpath, css_path)) diff --git a/frappe/celery_app.py b/frappe/celery_app.py index 715a1b8363..08d29d4d07 100644 --- a/frappe/celery_app.py +++ b/frappe/celery_app.py @@ -10,48 +10,59 @@ task_logger = get_task_logger(__name__) from datetime import timedelta import frappe -import json import os +import threading +import time SITES_PATH = os.environ.get('SITES_PATH', '.') # defaults DEFAULT_CELERY_BROKER = "redis://localhost" -DEFAULT_CELERY_BACKEND = None +DEFAULT_CELERY_BACKEND = "redis://localhost" DEFAULT_SCHEDULER_INTERVAL = 300 LONGJOBS_PREFIX = "longjobs@" +ASYNC_TASKS_PREFIX = "async@" _app = None def get_celery(): global _app if not _app: - conf = frappe.get_site_config(sites_path=SITES_PATH) - - _app = Celery('frappe', - broker=conf.celery_broker or DEFAULT_CELERY_BROKER, - backend=conf.celery_result_backend or DEFAULT_CELERY_BACKEND) - - setup_celery(_app, conf) - + _app = get_celery_app() return _app - -def setup_celery(app, conf): + +def get_celery_app(): + conf = get_site_config() + app = Celery('frappe', + broker=conf.celery_broker or DEFAULT_CELERY_BROKER, + backend=conf.async_redis_server or DEFAULT_CELERY_BACKEND) + app.autodiscover_tasks(frappe.get_all_apps(with_frappe=True, with_internal_apps=False, sites_path=SITES_PATH)) + app.conf.CELERY_TASK_SERIALIZER = 'json' app.conf.CELERY_ACCEPT_CONTENT = ['json'] app.conf.CELERY_TIMEZONE = 'UTC' - - if conf.celery_queue_per_site: - app.conf.CELERY_ROUTES = (SiteRouter(),) - + app.conf.CELERY_RESULT_SERIALIZER = 'json' + app.conf.CELERY_TASK_RESULT_EXPIRES = timedelta(0, 3600) + + if conf.monitory_celery: + app.conf.CELERY_SEND_EVENTS = True + app.conf.CELERY_SEND_TASK_SENT_EVENT = True + + app.conf.CELERY_ROUTES = (SiteRouter(), AsyncTaskRouter()) + app.conf.CELERYBEAT_SCHEDULE = get_beat_schedule(conf) if conf.celery_error_emails: app.conf.CELERY_SEND_TASK_ERROR_EMAILS = True for k, v in conf.celery_error_emails.iteritems(): setattr(app.conf, k, v) - + + return app + +def get_site_config(): + return frappe.get_site_config(sites_path=SITES_PATH) + class SiteRouter(object): def route_for_task(self, task, args=None, kwargs=None): if hasattr(frappe.local, 'site'): @@ -59,12 +70,17 @@ class SiteRouter(object): return get_queue(frappe.local.site, LONGJOBS_PREFIX) else: return get_queue(frappe.local.site) - + return None - + +class AsyncTaskRouter(object): + def route_for_task(self, task, args=None, kwargs=None): + if task == "frappe.tasks.run_async_task" and hasattr(frappe.local, 'site'): + return get_queue(frappe.local.site, ASYNC_TASKS_PREFIX) + def get_queue(site, prefix=None): return {'queue': "{}{}".format(prefix or "", site)} - + def get_beat_schedule(conf): schedule = { 'scheduler': { @@ -72,17 +88,135 @@ def get_beat_schedule(conf): 'schedule': timedelta(seconds=conf.scheduler_interval or DEFAULT_SCHEDULER_INTERVAL) }, } - - if conf.celery_queue_per_site: - schedule['sync_queues'] = { - 'task': 'frappe.tasks.sync_queues', - 'schedule': timedelta(seconds=conf.scheduler_interval or DEFAULT_SCHEDULER_INTERVAL) - } - + + schedule['sync_queues'] = { + 'task': 'frappe.tasks.sync_queues', + 'schedule': timedelta(seconds=conf.scheduler_interval or DEFAULT_SCHEDULER_INTERVAL) + } + return schedule - + def celery_task(*args, **kwargs): return get_celery().task(*args, **kwargs) - + +def make_async_task(args): + task = frappe.new_doc("Async Task") + task.update(args) + task.status = "Queued" + task.set_docstatus_user_and_timestamp() + task.db_insert() + task.notify_update() + +def run_test(): + for i in xrange(30): + test.delay(site=frappe.local.site) + +@celery_task() +def test(site=None): + time.sleep(1) + print "task" + +class MonitorThread(object): + """Thread manager for monitoring celery events""" + def __init__(self, celery_app, interval=1): + self.celery_app = celery_app + self.interval = interval + + self.state = self.celery_app.events.State() + + self.thread = threading.Thread(target=self.run, args=()) + self.thread.daemon = True + self.thread.start() + + def catchall(self, event): + if event['type'] != 'worker-heartbeat': + self.state.event(event) + + if not 'uuid' in event: + return + + task = self.state.tasks.get(event['uuid']) + info = task.info() + + if 'name' in event and 'enqueue_events_for_site' in event['name']: + return + + try: + kwargs = eval(info.get('kwargs')) + + if 'site' in kwargs: + frappe.connect(kwargs['site']) + + if event['type']=='task-sent': + make_async_task({ + 'name': event['uuid'], + 'task_name': kwargs.get("cmd") or event['name'] + }) + + elif event['type']=='task-received': + try: + task = frappe.get_doc("Async Task", event['uuid']) + task.status = 'Started' + task.set_docstatus_user_and_timestamp() + task.db_update() + task.notify_update() + except frappe.DoesNotExistError: + pass + + elif event['type']=='task-succeeded': + try: + task = frappe.get_doc("Async Task", event['uuid']) + task.status = 'Succeeded' + task.result = info.get('result') + task.runtime = info.get('runtime') + task.set_docstatus_user_and_timestamp() + task.db_update() + task.notify_update() + except frappe.DoesNotExistError: + pass + + elif event['type']=='task-failed': + try: + task = frappe.get_doc("Async Task", event['uuid']) + task.status = 'Failed' + task.traceback = event.get('traceback') or event.get('exception') + task.traceback = frappe.as_json(info) + "\n\n" + task.traceback + task.runtime = info.get('runtime') + task.set_docstatus_user_and_timestamp() + task.db_update() + task.notify_update() + except frappe.DoesNotExistError: + pass + + frappe.db.commit() + except Exception: + print frappe.get_traceback() + finally: + frappe.destroy() + + + def run(self): + while True: + try: + with self.celery_app.connection() as connection: + recv = self.celery_app.events.Receiver(connection, handlers={ + '*': self.catchall + }) + recv.capture(limit=None, timeout=None, wakeup=True) + + except (KeyboardInterrupt, SystemExit): + raise + + except Exception: + # unable to capture + print "unable to capture:" + print frappe.get_traceback() + + time.sleep(self.interval) + + if __name__ == '__main__': - get_celery().start() + app = get_celery() + if get_site_config().get("monitor_celery"): + MonitorThread(app) + app.start() diff --git a/frappe/change_log/current/readme.md b/frappe/change_log/current/readme.md new file mode 100644 index 0000000000..e93bb75396 --- /dev/null +++ b/frappe/change_log/current/readme.md @@ -0,0 +1,3 @@ +Leave change log files in this folder for user release notes. + +(this file is just a place holder, don't delete it) diff --git a/frappe/change_log/v5/v5_0_32.md b/frappe/change_log/v5/v5_0_32.md new file mode 100644 index 0000000000..17a7cb3283 --- /dev/null +++ b/frappe/change_log/v5/v5_0_32.md @@ -0,0 +1,5 @@ +- Reports are now searchable from awesome bar +- Show currect label for title in list views +- Datepicker now sets default value as Today +- Map child table as per meta, if not mentioned in table_map via mapper +- Re-enable save button on error \ No newline at end of file diff --git a/frappe/change_log/v5/v5_1_0.md b/frappe/change_log/v5/v5_1_0.md new file mode 100644 index 0000000000..3036acc826 --- /dev/null +++ b/frappe/change_log/v5/v5_1_0.md @@ -0,0 +1,3 @@ +- Change print font from Setup > Print Settings or set it for each Print Format. Font options are "Default", "Arial", "Helvetica", "Verdana", "Monospace". +- Print and full-page print preview in user's language +- Fixed inconsistent visibility of a logged-in user's image in website diff --git a/frappe/change_log/v5/v5_1_1.md b/frappe/change_log/v5/v5_1_1.md new file mode 100644 index 0000000000..befa5be85c --- /dev/null +++ b/frappe/change_log/v5/v5_1_1.md @@ -0,0 +1 @@ +- Ability to **Share with Everyone** (except Guest) using **Share With** diff --git a/frappe/change_log/v5/v5_3_0.md b/frappe/change_log/v5/v5_3_0.md new file mode 100644 index 0000000000..b03ec225d5 --- /dev/null +++ b/frappe/change_log/v5/v5_3_0.md @@ -0,0 +1,40 @@ +- Added Language Support for Following languages + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
bo*ལྷ་སའི་སྐད་
fisuomalainen
kmភាសាខ្មែរ
mkмакедонски
myMelayu
nonorsk
svSvenska
sqshqiptar
+ +* Unable to find translations for Tibetian via Google + +- To contribute to translations, please login to [https://translate.erpnext.com](https://translate.erpnext.com) diff --git a/frappe/change_log/v5/v5_4_0.md b/frappe/change_log/v5/v5_4_0.md new file mode 100644 index 0000000000..5bf04bb82d --- /dev/null +++ b/frappe/change_log/v5/v5_4_0.md @@ -0,0 +1 @@ +- Moved Backup Manager and Social Login Keys to the new **Integrations** module diff --git a/frappe/change_log/v6/v6_0_0.md b/frappe/change_log/v6/v6_0_0.md new file mode 100644 index 0000000000..6f9c8fcd5e --- /dev/null +++ b/frappe/change_log/v6/v6_0_0.md @@ -0,0 +1,3 @@ +- **Realtime updates** for new comments and list view +- Get warned if someone else modified the document that you are working on +- You can now quickly assign a document to yourself by clicking on "Assign to me" diff --git a/frappe/change_log/v6/v6_0_8.md b/frappe/change_log/v6/v6_0_8.md new file mode 100644 index 0000000000..6a2fba57a6 --- /dev/null +++ b/frappe/change_log/v6/v6_0_8.md @@ -0,0 +1 @@ +- Set HTML in Website Settings. This is usually used for website verification and SEO. diff --git a/frappe/change_log/v6/v6_12_0.md b/frappe/change_log/v6/v6_12_0.md new file mode 100644 index 0000000000..abcdd16275 --- /dev/null +++ b/frappe/change_log/v6/v6_12_0.md @@ -0,0 +1 @@ +- Extract emails using IMAP. Contributed by Gangadhar Kadam ([New Indictrans](http://indictranstech.com/)) diff --git a/frappe/change_log/v6/v6_13_0.md b/frappe/change_log/v6/v6_13_0.md new file mode 100644 index 0000000000..661e04f8b4 --- /dev/null +++ b/frappe/change_log/v6/v6_13_0.md @@ -0,0 +1,4 @@ +- Attachments can now be marked as **Private** + - Private files cannot be accessed unless you are logged in + - To access a private file, you need to have read permission on the file or read permission on the document to which the file is attached + - All attachments in a new incoming email are private diff --git a/frappe/change_log/v6/v6_14_1.md b/frappe/change_log/v6/v6_14_1.md new file mode 100644 index 0000000000..4aed877879 --- /dev/null +++ b/frappe/change_log/v6/v6_14_1.md @@ -0,0 +1 @@ +- Added language support for Malayalam: **ml - മലയാളം** diff --git a/frappe/change_log/v6/v6_15_0.md b/frappe/change_log/v6/v6_15_0.md new file mode 100644 index 0000000000..4a1b334c1e --- /dev/null +++ b/frappe/change_log/v6/v6_15_0.md @@ -0,0 +1,4 @@ +- **For Developers:** Automatic logging of request errors and its context in **Error Snapshot** + - Thank you [Maxwell Morais](https://discuss.erpnext.com/users/max_morais_dmm/activity) for this useful feature + - You can access it from *Developer > Logs > Error Snapshot* +- Added language support for [Gujarati](https://translate.erpnext.com/view?lang=gu): **gu - ગુજરાતી** diff --git a/frappe/change_log/v6/v6_16_1.md b/frappe/change_log/v6/v6_16_1.md new file mode 100644 index 0000000000..b63872485a --- /dev/null +++ b/frappe/change_log/v6/v6_16_1.md @@ -0,0 +1 @@ +- Mention users in comments using `@username`. Mentioned users will receive an email with the comment. \ No newline at end of file diff --git a/frappe/change_log/v6/v6_16_4.md b/frappe/change_log/v6/v6_16_4.md new file mode 100644 index 0000000000..fc901c3248 --- /dev/null +++ b/frappe/change_log/v6/v6_16_4.md @@ -0,0 +1,2 @@ +- Developer Tutorial [Videos](http://frappe.github.io/frappe/user/videos/) +- Increased uploaded file size limit upto 10MB \ No newline at end of file diff --git a/frappe/change_log/v6/v6_1_0.md b/frappe/change_log/v6/v6_1_0.md new file mode 100644 index 0000000000..cc628ba7f5 --- /dev/null +++ b/frappe/change_log/v6/v6_1_0.md @@ -0,0 +1,7 @@ +- Sections can now be set as **Collapsible**. +- Collapsible sections can be shown as collapsed based on certain rules defined in the **Collapsible Depends On** property of the document field (DocField). +- Title is now editable from the form if the `fieldname` of the title field is **title**. +- Document can now be renamed by clicking the page heading. +- Fields can be set as **Bold** so that they can be easily identified in long forms. +- Fixed mobile views +- See Data Import progress in realtime diff --git a/frappe/change_log/v6/v6_2_0.md b/frappe/change_log/v6/v6_2_0.md new file mode 100644 index 0000000000..05253243e3 --- /dev/null +++ b/frappe/change_log/v6/v6_2_0.md @@ -0,0 +1,3 @@ +- **Permissions:** + - If User Permissions are missing for a DocType, don't show non-matching records. + - If **Ignore User Permissions If Missing** is checked in System Settings, show records even if User Permissions are not defined. diff --git a/frappe/change_log/v6/v6_3_0.md b/frappe/change_log/v6/v6_3_0.md new file mode 100644 index 0000000000..f605c4607a --- /dev/null +++ b/frappe/change_log/v6/v6_3_0.md @@ -0,0 +1,2 @@ +- You can now add **CC** in Email +- Show checkboxes in Print diff --git a/frappe/change_log/v6/v6_4_0.md b/frappe/change_log/v6/v6_4_0.md new file mode 100644 index 0000000000..ae56802e6f --- /dev/null +++ b/frappe/change_log/v6/v6_4_0.md @@ -0,0 +1 @@ +- **File Manager:** A Document Management System for your organisation. Add files, organize them in folders and share it with a few users or everyone in the company. diff --git a/frappe/change_log/v6/v6_4_8.md b/frappe/change_log/v6/v6_4_8.md new file mode 100644 index 0000000000..afc5e45f22 --- /dev/null +++ b/frappe/change_log/v6/v6_4_8.md @@ -0,0 +1,20 @@ +- Added Language Support for Following languages + + + + + + + + + + + + + + + + + + +
bnবাংলা
da-DKdansk (Danmark)
es-PEEspañol (Perú)
sislovenščina
diff --git a/frappe/change_log/v6/v6_5_0.md b/frappe/change_log/v6/v6_5_0.md new file mode 100644 index 0000000000..62afbd34c3 --- /dev/null +++ b/frappe/change_log/v6/v6_5_0.md @@ -0,0 +1,2 @@ +- **Linked With** will now show links from Dynamic Links +- **Data** field-type size truncated to 140 characters from 255 (by default). Can be changed by setting the **length** property from **Customize Form View** diff --git a/frappe/change_log/v6/v6_6_0.md b/frappe/change_log/v6/v6_6_0.md new file mode 100644 index 0000000000..6655897117 --- /dev/null +++ b/frappe/change_log/v6/v6_6_0.md @@ -0,0 +1 @@ +- Added language support for Ukranian: **uk - українська** diff --git a/frappe/change_log/v6/v6_7_0.md b/frappe/change_log/v6/v6_7_0.md new file mode 100644 index 0000000000..ffb546d13d --- /dev/null +++ b/frappe/change_log/v6/v6_7_0.md @@ -0,0 +1,3 @@ +- See who is currently viewing the document +- Sounds for various actions +- Added language support for Slovene: **sl - slovenščina (Slovene)** diff --git a/frappe/change_log/v6/v6_8_0.md b/frappe/change_log/v6/v6_8_0.md new file mode 100644 index 0000000000..012b406f51 --- /dev/null +++ b/frappe/change_log/v6/v6_8_0.md @@ -0,0 +1,2 @@ +- Pre-configured Email Account for Yandex.Mail +- Fixed inline images in received emails diff --git a/frappe/client.py b/frappe/client.py index 7b1f3569ac..745523df51 100644 --- a/frappe/client.py +++ b/frappe/client.py @@ -156,9 +156,12 @@ def has_permission(doctype, docname, perm_type="read"): @frappe.whitelist() def get_js(src): - if src[0]=="/": - src = src[1:] - contentpath = os.path.join(frappe.local.sites_path, src) + src = src.strip("/").split("/") + + if ".." in src: + frappe.throw(_("Invalid file path: {0}").format("/".join(src))) + + contentpath = os.path.join(frappe.local.sites_path, *src) with open(contentpath, "r") as srcfile: code = frappe.utils.cstr(srcfile.read()) diff --git a/frappe/commands.py b/frappe/commands.py index 46023ee508..5537118355 100644 --- a/frappe/commands.py +++ b/frappe/commands.py @@ -4,7 +4,6 @@ from __future__ import unicode_literals, absolute_import import sys import os -import subprocess import json import click import hashlib @@ -17,6 +16,8 @@ from frappe.utils import cint from distutils.spawn import find_executable from functools import wraps +click.disable_unicode_literals_warning = True + def pass_context(f): @wraps(f) def _func(ctx, *args, **kwargs): @@ -104,7 +105,7 @@ def _is_scheduler_enabled(): enable_scheduler = False try: frappe.connect() - enable_scheduler = cint(frappe.db.get_default("enable_scheduler")) + enable_scheduler = cint(frappe.db.get_single_value("System Settings", "enable_scheduler")) and True or False except: pass finally: @@ -125,9 +126,8 @@ def restore(context, sql_file_path, mariadb_root_username=None, mariadb_root_pas site = get_single_site(context) frappe.init(site=site) - if not db_name: - db_name = frappe.conf.db_name - _new_site(db_name, site, mariadb_root_username=mariadb_root_username, mariadb_root_password=mariadb_root_password, admin_password=admin_password, verbose=context.verbose, install_apps=install_app, source_sql=sql_file_path, force=context.force) + db_name = db_name or frappe.conf.db_name or hashlib.sha1(site).hexdigest()[:10] + _new_site(db_name, site, mariadb_root_username=mariadb_root_username, mariadb_root_password=mariadb_root_password, admin_password=admin_password, verbose=context.verbose, install_apps=install_app, source_sql=sql_file_path, force=context.force) @click.command('reinstall') @pass_context @@ -140,7 +140,7 @@ def reinstall(context): frappe.clear_cache() installed = frappe.get_installed_apps() frappe.clear_cache() - except Exception, e: + except Exception: installed = [] finally: if frappe.db: @@ -164,6 +164,16 @@ def install_app(context, app): finally: frappe.destroy() +@click.command('list-apps') +@pass_context +def list_apps(context): + "Reinstall site ie. wipe all data and start over" + site = get_single_site(context) + frappe.init(site=site) + frappe.connect() + print "\n".join(frappe.get_installed_apps()) + frappe.destroy() + @click.command('add-system-manager') @click.argument('email') @click.option('--first-name') @@ -191,8 +201,6 @@ def migrate(context, rebuild_website=False): import frappe.translate from frappe.desk.notifications import clear_notifications - verbose = context.verbose - for site in context.sites: print 'Migrating', site frappe.init(site=site) @@ -312,15 +320,16 @@ def destroy_all_sessions(context): frappe.destroy() @click.command('sync-www') +@click.option('--force', help='Rebuild all pages', is_flag=True, default=False) @pass_context -def sync_www(context): +def sync_www(context, force=False): "Sync files from static pages from www directory to Web Pages" from frappe.website import statics for site in context.sites: try: frappe.init(site=site) frappe.connect() - statics.sync_statics(rebuild=context.force) + statics.sync_statics(rebuild=force) frappe.db.commit() finally: frappe.destroy() @@ -340,35 +349,102 @@ def build_website(context): finally: frappe.destroy() -@click.command('setup-docs') -@click.argument('app') -@click.argument('docs-app') -@click.argument('path') +@click.command('make-docs') @pass_context -def setup_docs(context,app, docs_app, path): +@click.argument('app') +@click.argument('docs_version') +def make_docs(context, app, docs_version): "Setup docs in target folder of target app" from frappe.utils.setup_docs import setup_docs for site in context.sites: try: frappe.init(site=site) frappe.connect() - setup_docs(app, docs_app, path) + make = setup_docs(app) + make.build(docs_version) + finally: + frappe.destroy() + +@click.command('sync-docs') +@pass_context +@click.argument('app') +def sync_docs(context, app): + "Sync docs from /docs folder into the database (Web Page)" + from frappe.utils.setup_docs import setup_docs + for site in context.sites: + try: + frappe.init(site=site) + frappe.connect() + make = setup_docs(app) + make.sync_docs() + finally: + frappe.destroy() + + +@click.command('write-docs') +@pass_context +@click.argument('app') +@click.argument('target') +@click.option('--local', default=False, is_flag=True, help='Run app locally') +def write_docs(context, app, target, local=False): + "Setup docs in target folder of target app" + from frappe.utils.setup_docs import setup_docs + 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') -@click.argument('app') @pass_context -def build_docs(context, app): - "Build docs from /src to /www folder in app" - from frappe.utils.autodoc import build - frappe.destroy() +@click.argument('app') +@click.option('--docs-version', default='current') +@click.option('--target', default=None) +@click.option('--local', default=False, is_flag=True, help='Run app locally') +@click.option('--watch', default=False, is_flag=True, help='Watch for changes and rewrite') +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)) + for site in context.sites: - try: - frappe.init(site=site) - build(app) - finally: - frappe.destroy() + _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("frappe", "..", "..") + start_watch(apps_path, handler=trigger_make) + +def _build_docs_once(site, app, docs_version, target, local, only_content_updated=False): + from frappe.utils.setup_docs import setup_docs + + try: + + frappe.init(site=site) + frappe.connect() + make = setup_docs(app) + + if not only_content_updated: + make.build(docs_version) + make.sync_docs() + + make.make_docs(target, local) + + finally: + frappe.destroy() + @click.command('reset-perms') @pass_context @@ -380,7 +456,7 @@ def reset_perms(context): frappe.init(site=site) frappe.connect() for d in frappe.db.sql_list("""select name from `tabDocType` - where ifnull(istable, 0)=0 and ifnull(custom, 0)=0"""): + where istable=0 and custom=0"""): frappe.clear_cache(doctype=d) reset_perms(d) finally: @@ -388,22 +464,34 @@ def reset_perms(context): @click.command('execute') @click.argument('method') +@click.option('--args') +@click.option('--kwargs') @pass_context -def execute(context, method): +def execute(context, method, args=None, kwargs=None): "execute a function" for site in context.sites: try: frappe.init(site=site) frappe.connect() - print frappe.local.site - ret = frappe.get_attr(method)() + + if args: + args = eval(args) + else: + args = () + + if kwargs: + kwargs = eval(args) + else: + kwargs = {} + + ret = frappe.get_attr(method)(*args, **kwargs) if frappe.db: frappe.db.commit() finally: frappe.destroy() if ret: - print ret + print json.dumps(ret) @click.command('celery') @click.argument('args') @@ -529,6 +617,55 @@ def import_doc(context, path, force=False): finally: frappe.destroy() +@click.command('import-csv') +@click.argument('path') +@click.option('--only-insert', default=False, is_flag=True, help='Do not overwrite existing records') +@click.option('--submit-after-import', default=False, is_flag=True, help='Submit document after importing it') +@click.option('--ignore-encoding-errors', default=False, is_flag=True, help='Ignore encoding errors while coverting to unicode') +@pass_context +def import_csv(context, path, only_insert=False, submit_after_import=False, ignore_encoding_errors=False): + "Import CSV using data import tool" + from frappe.core.page.data_import_tool import importer + from frappe.utils.csvutils import read_csv_content + site = get_single_site(context) + + with open(path, 'r') as csvfile: + content = read_csv_content(csvfile.read()) + + frappe.init(site=site) + frappe.connect() + + try: + importer.upload(content, submit_after_import=submit_after_import, + ignore_encoding_errors=ignore_encoding_errors, overwrite=not only_insert, + via_console=True) + frappe.db.commit() + except Exception: + print frappe.get_traceback() + + frappe.destroy() + +@click.command('bulk-rename') +@click.argument('doctype') +@click.argument('path') +@pass_context +def _bulk_rename(context, doctype, path): + "Rename multiple records via CSV file" + from frappe.model.rename_doc import bulk_rename + from frappe.utils.csvutils import read_csv_content + + site = get_single_site(context) + + with open(path, 'r') as csvfile: + rows = read_csv_content(csvfile.read()) + + frappe.init(site=site) + frappe.connect() + + bulk_rename(doctype, rows, via_console = True) + + frappe.destroy() + # translation @click.command('build-message-files') @pass_context @@ -613,6 +750,7 @@ def console(context): site = get_single_site(context) frappe.init(site=site) frappe.connect() + frappe.local.lang = frappe.db.get_default("lang") import IPython IPython.embed() @@ -632,7 +770,7 @@ def run_tests(context, app=None, module=None, doctype=None, test=(), driver=None site = get_single_site(context) frappe.init(site=site) - if frappe.conf.run_selenium_tests: + if frappe.conf.run_selenium_tests and False: sel.start(context.verbose, driver) try: @@ -686,11 +824,19 @@ def request(context, args): @click.command('doctor') def doctor(): - "Get untranslated strings for lang." + "Get diagnostic info about background workers" from frappe.utils.doctor import doctor as _doctor frappe.init('') return _doctor() +@click.command('celery-doctor') +@click.option('--site', help='site name') +def celery_doctor(site=None): + "Get diagnostic info about background workers" + from frappe.utils.doctor import celery_doctor as _celery_doctor + frappe.init('') + return _celery_doctor(site=site) + @click.command('purge-all-tasks') def purge_all_tasks(): "Purge any pending periodic tasks of 'all' event. Doesn't purge hourly, daily and weekly" @@ -725,21 +871,23 @@ def use(site, sites_path='.'): @click.command('backup') @click.option('--with-files', default=False, is_flag=True, help="Take backup with files") @pass_context -def backup(context, with_files=False, backup_path_db=None, backup_path_files=None, quiet=False): +def backup(context, with_files=False, backup_path_db=None, backup_path_files=None, + backup_path_private_files=None, quiet=False): "Backup" from frappe.utils.backups import scheduled_backup verbose = context.verbose for site in context.sites: frappe.init(site=site) frappe.connect() - odb = scheduled_backup(ignore_files=not with_files, backup_path_db=backup_path_db, backup_path_files=backup_path_files, force=True) + odb = scheduled_backup(ignore_files=not with_files, backup_path_db=backup_path_db, backup_path_files=backup_path_files, backup_path_private_files=backup_path_private_files, force=True) if verbose: from frappe.utils import now print "database backup taken -", odb.backup_path_db, "- on", now() if with_files: print "files backup taken -", odb.backup_path_files, "- on", now() - frappe.destroy() + print "private files backup taken -", odb.backup_path_private_files, "- on", now() + frappe.destroy() @click.command('remove-from-installed-apps') @click.argument('app') @@ -754,6 +902,20 @@ def remove_from_installed_apps(context, app): finally: frappe.destroy() +@click.command('uninstall-app') +@click.argument('app') +@click.option('--dry-run', help='List all doctypes that will be deleted', is_flag=True, default=False) +@pass_context +def uninstall(context, app, dry_run=False): + from frappe.installer import remove_app + for site in context.sites: + try: + frappe.init(site=site) + frappe.connect() + remove_app(app, dry_run) + finally: + frappe.destroy() + def move(dest_dir, site): import os if not os.path.isdir(dest_dir): @@ -775,6 +937,18 @@ def move(dest_dir, site): frappe.destroy() return final_new_path + +@click.command('set-config') +@click.argument('key') +@click.argument('value') +@pass_context +def set_config(context, key, value): + from frappe.installer import update_site_config + for site in context.sites: + frappe.init(site=site) + update_site_config(key, value) + frappe.destroy() + @click.command('drop-site') @click.argument('site') @click.option('--root-login', default='root') @@ -799,6 +973,15 @@ def drop_site(site, root_login='root', root_password=None): os.mkdir(archived_sites_dir) move(archived_sites_dir, site) +@click.command('version') +@pass_context +def get_version(context): + frappe.init(site=context.sites[0]) + for m in sorted(frappe.local.app_modules.keys()): + module = frappe.get_module(m) + if hasattr(module, "__version__"): + print "{0} {1}".format(m, module.__version__) + # commands = [ # new_site, # restore, @@ -813,6 +996,7 @@ commands = [ restore, reinstall, install_app, + list_apps, add_system_manager, migrate, run_patch, @@ -824,7 +1008,9 @@ commands = [ destroy_all_sessions, sync_www, build_website, - setup_docs, + make_docs, + sync_docs, + write_docs, build_docs, reset_perms, execute, @@ -837,6 +1023,8 @@ commands = [ export_csv, export_fixtures, import_doc, + import_csv, + _bulk_rename, build_message_files, get_untranslated, update_translations, @@ -846,6 +1034,7 @@ commands = [ serve, request, doctor, + celery_doctor, purge_all_tasks, dump_queue_status, console, @@ -853,5 +1042,8 @@ commands = [ _use, backup, remove_from_installed_apps, + uninstall, drop_site, + set_config, + get_version, ] diff --git a/frappe/config/core.py b/frappe/config/core.py new file mode 100644 index 0000000000..6bb81b2e2d --- /dev/null +++ b/frappe/config/core.py @@ -0,0 +1,61 @@ +from __future__ import unicode_literals +from frappe import _ + +def get_data(): + return [ + { + "label": _("Documents"), + "items": [ + { + "type": "doctype", + "name": "DocType", + "description": _("Models (building blocks) of the Application"), + }, + { + "type": "doctype", + "name": "Module Def", + "description": _("Groups of DocTypes"), + }, + { + "type": "doctype", + "name": "Page", + "description": _("Pages in Desk (place holders)"), + }, + { + "type": "doctype", + "name": "Report", + "description": _("Script or Query reports"), + }, + { + "type": "doctype", + "name": "Print Format", + "description": _("Customized Formats for Printing, Email"), + }, + { + "type": "doctype", + "name": "Custom Script", + "description": _("Client side script extensions in Javascript"), + } + ] + }, + { + "label": _("Logs"), + "items": [ + { + "type": "doctype", + "name": "Scheduler Log", + "description": _("Errors in Background Events"), + }, + { + "type": "doctype", + "name": "Bulk Email", + "description": _("Background Email Queue"), + }, + { + "type": "doctype", + "name": "Error Snapshot", + "description": _("A log of request errors"), + }, + ] + } + ] diff --git a/frappe/config/desk.py b/frappe/config/desk.py new file mode 100644 index 0000000000..a903d74da9 --- /dev/null +++ b/frappe/config/desk.py @@ -0,0 +1,43 @@ +from __future__ import unicode_literals +from frappe import _ + +def get_data(): + return [ + { + "label": _("Tools"), + "icon": "octicon octicon-briefcase", + "items": [ + { + "type": "doctype", + "name": "ToDo", + "label": _("To Do"), + "description": _("Documents assigned to you and by you."), + }, + { + "type": "doctype", + "name": "Event", + "label": _("Calendar"), + "view": "Calendar", + "description": _("Event and other calendars."), + }, + { + "type": "page", + "label": _("Messages"), + "name": "messages", + "description": _("Chat messages and other notifications."), + "data_doctype": "Comment" + }, + { + "type": "doctype", + "name": "Note", + "description": _("Private and public Notes."), + }, + { + "type": "page", + "label": _("Activity"), + "name": "activity", + "description": _("Activity log of all users."), + }, + ] + } + ] diff --git a/frappe/config/desktop.py b/frappe/config/desktop.py index 70b73ffb7e..993acf2587 100644 --- a/frappe/config/desktop.py +++ b/frappe/config/desktop.py @@ -3,73 +3,38 @@ from frappe import _ def get_data(): return { - "Activity": { - "color": "#e67e22", - "icon": "icon-play", - "icon": "octicon octicon-pulse", - "label": _("Activity"), - "link": "activity", - "type": "page" - }, - "Calendar": { - "color": "#2980b9", - "icon": "icon-calendar", - "icon": "octicon octicon-calendar", - "label": _("Calendar"), - "link": "Calendar/Event", - "type": "view" - }, - "Messages": { - "color": "#9b59b6", - "icon": "icon-comments", - "icon": "octicon octicon-comment-discussion", - "label": _("Messages"), - "link": "messages", - "type": "page" - }, - "To Do": { - "color": "#f1c40f", - "icon": "icon-check", - "icon": "octicon octicon-check", - "label": _("To Do"), - "link": "List/ToDo", - "doctype": "ToDo", - "type": "list" - }, - "Notes": { - "color": "#95a5a6", - "doctype": "Note", - "icon": "icon-file-alt", - "icon": "octicon octicon-file-text", - "label": _("Notes"), - "link": "List/Note", + "File Manager": { + "color": "#AA784D", + "doctype": "File", + "icon": "octicon octicon-file-directory", + "label": _("File Manager"), + "link": "List/File", "type": "list" }, "Website": { "color": "#16a085", - "icon": "icon-globe", "icon": "octicon octicon-globe", "type": "module" }, - "Installer": { - "color": "#5ac8fb", - "icon": "icon-download", - "icon": "octicon octicon-cloud-download", - "link": "applications", - "type": "page", - "label": _("Installer") - }, "Setup": { "color": "#bdc3c7", - "icon": "icon-wrench", + "reverse": 1, "icon": "octicon octicon-settings", "type": "module" }, "Core": { + "label": _("Developer"), "color": "#589494", "icon": "icon-cog", - "icon": "octicon octicon-file-binary", + "icon": "octicon octicon-circuit-board", "type": "module", "system_manager": 1 }, + "Desk": { + "label": _("Tools"), + "color": "#FFF5A7", + "reverse": 1, + "icon": "octicon octicon-calendar", + "type": "module" + } } diff --git a/frappe/config/docs.py b/frappe/config/docs.py new file mode 100644 index 0000000000..2fc1f5acc7 --- /dev/null +++ b/frappe/config/docs.py @@ -0,0 +1,31 @@ + +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/tutorial) +""" +docs_version = "6.x.x" + +def get_context(context): + context.top_bar_items = [ + {"label": "Developer Tutorials", "url": context.docs_base_url + "/user", "right": 1}, + {"label": "Documentation", "url": context.docs_base_url + "/current", "right": 1} + ] diff --git a/frappe/config/setup.py b/frappe/config/setup.py index 0e5fe02046..61a9df0a32 100644 --- a/frappe/config/setup.py +++ b/frappe/config/setup.py @@ -38,6 +38,13 @@ def get_data(): "icon": "icon-shield", "description": _("Set Permissions per User") }, + { + "type": "page", + "name": "modules_setup", + "label": _("Show / Hide Modules"), + "icon": "icon-upload", + "description": _("Show or hide modules globally.") + }, { "type": "report", "is_query_report": True, @@ -67,17 +74,14 @@ def get_data(): "hide_count": True }, { - "type": "page", - "name": "modules_setup", - "label": _("Show / Hide Modules"), - "icon": "icon-upload", - "description": _("Show or hide modules globally.") + "type": "doctype", + "name": "Scheduler Log", + "description": _("Log of error on automated events (scheduler).") }, { "type": "doctype", - "name": "Naming Series", - "description": _("Set numbering series for transactions."), - "hide_count": True + "name": "Error Snapshot", + "description": _("Log of error during requests.") }, ] }, @@ -94,15 +98,24 @@ def get_data(): }, { "type": "doctype", - "name": "Rename Tool", - "description": _("Rename many items by uploading a .csv file."), + "name": "Naming Series", + "description": _("Set numbering series for transactions."), "hide_count": True }, { "type": "doctype", - "name": "File Data", - "description": _("Manage uploaded files.") - } + "name": "Rename Tool", + "label": _("Bulk Rename"), + "description": _("Rename many items by uploading a .csv file."), + "hide_count": True + }, + { + "type": "page", + "name": "backups", + "label": _("Download Backups"), + "description": _("List of backups available for download"), + "icon": "icon-download" + }, ] }, { @@ -169,6 +182,30 @@ def get_data(): }, ] }, + { + "label": _("Integrations"), + "icon": "icon-star", + "items": [ + { + "type": "page", + "name": "applications", + "label": _("Application Installer"), + "description": _("Install Applications."), + "icon": "icon-download" + }, + { + "type": "doctype", + "name": "Social Login Keys", + "description": _("Enter keys to enable login via Facebook, Google, GitHub."), + }, + { + "type": "doctype", + "name": "Dropbox Backup", + "description": _("Manage cloud backups on Dropbox"), + "hide_count": True + } + ] + }, { "label": _("Customize"), "icon": "icon-glass", @@ -197,30 +234,6 @@ def get_data(): ] }, - { - "label": _("System"), - "icon": "icon-cog", - "items": [ - { - "type": "page", - "name": "applications", - "label": _("Application Installer"), - "description": _("Install Applications."), - "icon": "icon-download" - }, - { - "type": "doctype", - "name": "Backup Manager", - "description": _("Manage cloud backups on Dropbox"), - "hide_count": True - }, - { - "type": "doctype", - "name": "Scheduler Log", - "description": _("Log of error on automated events (scheduler).") - }, - ] - } ] add_setup_section(data, "frappe", "website", _("Website"), "icon-globe") return data diff --git a/frappe/config/website.py b/frappe/config/website.py index f118e0f589..8d5f44f6ac 100644 --- a/frappe/config/website.py +++ b/frappe/config/website.py @@ -77,11 +77,6 @@ def get_data(): "type": "doctype", "name": "Website Theme", "description": _("List of themes for Website."), - }, - { - "type": "doctype", - "name": "Social Login Keys", - "description": _("Enter keys to enable login via Facebook, Google, GitHub."), } ] }, diff --git a/frappe/website/doctype/social_login_keys/__init__.py b/frappe/core/doctype/async_task/__init__.py similarity index 100% rename from frappe/website/doctype/social_login_keys/__init__.py rename to frappe/core/doctype/async_task/__init__.py diff --git a/frappe/core/doctype/async_task/async_task.json b/frappe/core/doctype/async_task/async_task.json new file mode 100644 index 0000000000..d62cc8ce12 --- /dev/null +++ b/frappe/core/doctype/async_task/async_task.json @@ -0,0 +1,240 @@ +{ + "allow_copy": 0, + "allow_import": 0, + "allow_rename": 0, + "autoname": "", + "creation": "2015-07-03 11:28:03.496346", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "Document", + "fields": [ + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "status", + "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Status", + "length": 0, + "no_copy": 0, + "options": "\nQueued\nRunning\nSucceeded\nFailed\n", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "task_name", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Task Name", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "runtime", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 1, + "label": "Runtime", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "result", + "fieldtype": "Code", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Result", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "traceback", + "fieldtype": "Code", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Traceback", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "section_break_6", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "reference_doctype", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Reference DocType", + "length": 0, + "no_copy": 0, + "options": "DocType", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "reference_name", + "fieldtype": "Dynamic Link", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Reference Doc", + "length": 0, + "no_copy": 0, + "options": "reference_doctype", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + } + ], + "hide_heading": 0, + "hide_toolbar": 0, + "in_create": 0, + "in_dialog": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 0, + "max_attachments": 0, + "modified": "2015-11-16 06:29:42.038458", + "modified_by": "Administrator", + "module": "Core", + "name": "Async Task", + "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 + } + ], + "read_only": 0, + "read_only_onload": 0, + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "task_name" +} \ No newline at end of file diff --git a/frappe/core/doctype/async_task/async_task.py b/frappe/core/doctype/async_task/async_task.py new file mode 100644 index 0000000000..85d043524b --- /dev/null +++ b/frappe/core/doctype/async_task/async_task.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe.model.document import Document + +class AsyncTask(Document): + pass diff --git a/frappe/core/doctype/async_task/async_task_list.js b/frappe/core/doctype/async_task/async_task_list.js new file mode 100644 index 0000000000..699da13084 --- /dev/null +++ b/frappe/core/doctype/async_task/async_task_list.js @@ -0,0 +1,10 @@ +frappe.listview_settings['Async Task'] = { + add_fields: ["status"], + get_indicator: function(doc) { + if(doc.status==="Succeeded") { + return [__("Succeeded"), "green", "status,=,Succeeded"]; + } else if(doc.status==="Failed") { + return [__("Failed"), "red", "status,=,Failed"]; + } + } +}; diff --git a/frappe/core/doctype/async_task/test_async_task.py b/frappe/core/doctype/async_task/test_async_task.py new file mode 100644 index 0000000000..1e885f27f9 --- /dev/null +++ b/frappe/core/doctype/async_task/test_async_task.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +import frappe +import unittest + +# test_records = frappe.get_test_records('Async Task') + +class TestAsyncTask(unittest.TestCase): + pass diff --git a/frappe/core/doctype/block_module/block_module.json b/frappe/core/doctype/block_module/block_module.json index 9b5c79e680..2a5d00bce1 100644 --- a/frappe/core/doctype/block_module/block_module.json +++ b/frappe/core/doctype/block_module/block_module.json @@ -10,6 +10,8 @@ "fields": [ { "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "module", "fieldtype": "Data", "hidden": 0, @@ -17,6 +19,7 @@ "in_filter": 0, "in_list_view": 0, "label": "Module", + "length": 0, "no_copy": 0, "options": "", "permlevel": 0, @@ -26,7 +29,8 @@ "report_hide": 0, "reqd": 1, "search_index": 0, - "set_only_once": 0 + "set_only_once": 0, + "unique": 0 } ], "hide_heading": 0, @@ -36,7 +40,8 @@ "is_submittable": 0, "issingle": 0, "istable": 1, - "modified": "2015-03-24 16:03:52.359042", + "max_attachments": 0, + "modified": "2015-11-16 06:29:42.426522", "modified_by": "Administrator", "module": "Core", "name": "Block Module", diff --git a/frappe/core/doctype/comment/comment.json b/frappe/core/doctype/comment/comment.json index ed68eca7e0..fd8d0e45f0 100644 --- a/frappe/core/doctype/comment/comment.json +++ b/frappe/core/doctype/comment/comment.json @@ -1,154 +1,338 @@ { + "allow_copy": 0, "allow_import": 1, + "allow_rename": 0, "autoname": "hash", "creation": "2012-08-08 10:40:11", + "custom": 0, "docstatus": 0, "doctype": "DocType", "fields": [ { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "comment", "fieldtype": "Text", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, "in_list_view": 1, "label": "Comment", + "length": 0, "no_copy": 0, "oldfieldname": "comment", "oldfieldtype": "Text", "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, "reqd": 1, - "search_index": 0 + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "comment_type", "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Comment Type", + "length": 0, + "no_copy": 0, "options": "Email\nChat\nPhone\nSMS\nCreated\nSubmitted\nCancelled\nAssigned\nAssignment Completed\nComment\nWorkflow\nLabel\nAttachment\nAttachment Removed", - "permlevel": 0 + "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "comment_by", "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, "in_list_view": 1, "label": "Comment By", + "length": 0, "no_copy": 0, "oldfieldname": "comment_by", "oldfieldtype": "Data", "permlevel": 0, - "search_index": 0 + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "comment_by_fullname", "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, "in_list_view": 1, "label": "Comment By Fullname", + "length": 0, "no_copy": 0, "oldfieldname": "comment_by_fullname", "oldfieldtype": "Data", "permlevel": 0, - "search_index": 0 + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "comment_date", "fieldtype": "Date", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, "in_list_view": 1, "label": "Comment Date", + "length": 0, "no_copy": 0, "oldfieldname": "comment_date", "oldfieldtype": "Date", "permlevel": 0, - "search_index": 0 + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "comment_time", "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, "in_list_view": 1, "label": "Comment Time", + "length": 0, "no_copy": 0, "oldfieldname": "comment_time", "oldfieldtype": "Data", "permlevel": 0, - "search_index": 0 + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "comment_doctype", "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Comment Doctype", + "length": 0, "no_copy": 0, "oldfieldname": "comment_doctype", "oldfieldtype": "Data", "permlevel": 0, - "search_index": 0 + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "comment_docname", "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Comment Docname", + "length": 0, "no_copy": 0, "oldfieldname": "comment_docname", "oldfieldtype": "Data", "permlevel": 0, - "search_index": 0 + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "post_topic", "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Post Topic", + "length": 0, "no_copy": 0, "oldfieldname": "post_topic", "oldfieldtype": "Data", "permlevel": 0, - "search_index": 0 + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "unsubscribed", "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Unsubscribed", - "permlevel": 0 + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "reference_doctype", "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Reference DocType", + "length": 0, + "no_copy": 0, "options": "DocType", "permlevel": 0, "precision": "", - "read_only": 1 + "print_hide": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "description": "Reference DocType and Reference Name are used to render a comment as a link (href) to a Doc.", "fieldname": "reference_name", "fieldtype": "Dynamic Link", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Reference Name", + "length": 0, + "no_copy": 0, "options": "reference_doctype", "permlevel": 0, "precision": "", - "read_only": 1 + "print_hide": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 } ], + "hide_heading": 0, + "hide_toolbar": 0, "icon": "icon-comments", "idx": 1, + "in_create": 0, + "in_dialog": 0, + "is_submittable": 0, "issingle": 0, - "modified": "2015-06-08 12:31:15.122312", + "istable": 0, + "max_attachments": 0, + "modified": "2015-11-16 06:29:43.314568", "modified_by": "Administrator", "module": "Core", "name": "Comment", "owner": "Administrator", "permissions": [ { + "amend": 0, + "apply_user_permissions": 0, "cancel": 0, "create": 1, "delete": 1, "email": 1, "export": 1, + "if_owner": 0, "import": 1, "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "System Manager", + "set_user_permissions": 0, "share": 1, "submit": 0, "write": 1 } ], + "read_only": 0, + "read_only_onload": 0, "title_field": "comment" } \ No newline at end of file diff --git a/frappe/core/doctype/comment/comment.py b/frappe/core/doctype/comment/comment.py index fa2c8cb37e..70d71b8c0e 100644 --- a/frappe/core/doctype/comment/comment.py +++ b/frappe/core/doctype/comment/comment.py @@ -7,11 +7,13 @@ from frappe import _ from frappe.website.render import clear_cache from frappe.model.document import Document from frappe.model.db_schema import add_column -from frappe.utils import get_fullname +from frappe.utils import get_fullname, get_link_to_form +from frappe.core.doctype.user.user import extract_mentions + +exclude_from_linked_with = True class Comment(Document): """Comments are added to Documents via forms or views like blogs etc.""" - __doclink__ = "https://frappe.io/docs/models/core/comment" no_feed_on_delete = True def get_feed(self): @@ -33,6 +35,25 @@ class Comment(Document): "feed_type": comment_type } + def after_insert(self): + """Send realtime updates""" + if not self.comment_doctype: + return + + if self.comment_doctype == 'Message': + if self.comment_docname == frappe.session.user: + message = self.as_dict() + message['broadcast'] = True + frappe.publish_realtime('new_message', message) + else: + # comment_docname contains the user who is addressed in the messages' page comment + frappe.publish_realtime('new_message', self.as_dict(), user=self.comment_docname) + else: + frappe.publish_realtime('new_comment', self.as_dict(), doctype= self.comment_doctype, + docname = self.comment_docname) + + self.notify_mentions() + def validate(self): """Raise exception for more than 50 comments.""" if frappe.db.sql("""select count(*) from tabComment where comment_doctype=%s @@ -127,6 +148,33 @@ class Comment(Document): self.update_comments_in_parent(_comments) + def notify_mentions(self): + if self.comment_doctype and self.comment_docname and self.comment and self.comment_type=="Comment": + mentions = extract_mentions(self.comment) + + if not mentions: + return + + sender_fullname = get_fullname(frappe.session.user) + parent_doc_label = "{0} {1}".format(_(self.comment_doctype), self.comment_docname) + 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": self, + "link": get_link_to_form(self.comment_doctype, self.comment_docname, label=parent_doc_label) + }) + + recipients = [frappe.db.get_value("User", {"enabled": 1, "username": username, "user_type": "System User"}) + for username in mentions] + + frappe.sendmail( + recipients=recipients, + sender=frappe.session.user, + subject=subject, + message=message, + bulk=True + ) + def on_doctype_update(): """Add index to `tabComment` `(comment_doctype, comment_name)`""" if not frappe.db.sql("""show index from `tabComment` @@ -134,4 +182,3 @@ def on_doctype_update(): frappe.db.commit() frappe.db.sql("""alter table `tabComment` add index comment_doctype_docname_index(comment_doctype, comment_docname)""") - diff --git a/frappe/core/doctype/communication/communication.js b/frappe/core/doctype/communication/communication.js index e4a93c6656..3b8501cda0 100644 --- a/frappe/core/doctype/communication/communication.js +++ b/frappe/core/doctype/communication/communication.js @@ -1,29 +1,31 @@ -frappe.ui.form.on("Communication", "setup", function(frm) { - frappe.call({ - method:"frappe.core.doctype.doctype.communication.get_convert_to", - callback: function(r) { - frappe.communication_convert_to = r.message; - frm.convert_to_click = []; - $.each(r.message, function(i, v) { - frm.convert_to_click.append({label:__(v), value:v, action:function() { - frm.convert_to($(this).attr("data-value")); - }}); - }); - frm.set_convert_button(); - } - }); - - frm.set_convert_button = function() { - frm.add_custom_button(__("Add To"), frm.convert_to_click); - }; - - frm.convert_to = function(doctype) { - - }; -}); - frappe.ui.form.on("Communication", "refresh", function(frm) { frm.convert_to_click && frm.set_convert_button(); + frm.subject_field = "subject"; + + if(frm.doc.reference_doctype && frm.doc.reference_name) { + frm.add_custom_button(__(frm.doc.reference_name), function() { + frappe.set_route("Form", frm.doc.reference_doctype, frm.doc.reference_name); + }, frappe.boot.doctype_icons[frm.doc.reference_doctype]); + } else { + // if an unlinked communication, set email field + if (frm.doc.sent_or_received==="Received") { + frm.email_field = "sender"; + } else { + frm.email_field = "recipients"; + } + } + + if(frm.doc.status==="Open") { + frm.add_custom_button("Close", function() { + frm.set_value("status", "Closed"); + frm.save(); + }); + } else if (frm.doc.status !== "Linked") { + frm.add_custom_button("Reopen", function() { + frm.set_value("status", "Open"); + frm.save(); + }); + } }); frappe.ui.form.on("Communication", "onload", function(frm) { diff --git a/frappe/core/doctype/communication/communication.json b/frappe/core/doctype/communication/communication.json index 24283f4728..704bc5be79 100644 --- a/frappe/core/doctype/communication/communication.json +++ b/frappe/core/doctype/communication/communication.json @@ -1,188 +1,605 @@ { + "allow_copy": 0, "allow_import": 1, + "allow_rename": 0, "autoname": "naming_series:", "creation": "2013-01-29 10:47:14", + "custom": 0, "description": "Keep a track of all communications", "docstatus": 0, "doctype": "DocType", - "document_type": "Master", + "document_type": "Setup", "fields": [ { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "default": "COMM-", "fieldname": "naming_series", "fieldtype": "Select", "hidden": 1, - "label": "Series", - "options": "COMM-", - "permlevel": 0 - }, - { - "fieldname": "sent_or_received", - "fieldtype": "Select", - "in_list_view": 1, - "label": "Sent or Received", - "options": "Sent\nReceived", - "permlevel": 0, - "reqd": 1 - }, - { - "fieldname": "status", - "fieldtype": "Select", - "label": "Status", - "options": "Open\nReplied\nArchived", - "permlevel": 0, - "precision": "" - }, - { - "fieldname": "subject", - "fieldtype": "Data", + "ignore_user_permissions": 0, + "in_filter": 0, "in_list_view": 0, - "label": "Subject", + "label": "Series", + "length": 0, + "no_copy": 0, + "options": "COMM-", "permlevel": 0, - "reqd": 1 + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "depends_on": "", + "fieldname": "communication_medium", + "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Communication Medium", + "length": 0, + "no_copy": 0, + "options": "\nChat\nPhone\nEmail\nSMS\nVisit\nOther", + "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "recipients", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Recipients", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "depends_on": "eval:doc.communication_medium===\"Email\"", + "fieldname": "cc", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "CC", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "depends_on": "eval:doc.communication_medium!==\"Email\"", + "fieldname": "phone_no", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Phone No.", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "column_break_5", "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "length": 0, + "no_copy": 0, "permlevel": 0, - "precision": "" + "precision": "", + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { - "fieldname": "reference_doctype", - "fieldtype": "Link", - "label": "Reference DocType", - "options": "DocType", + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "status", + "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 1, + "label": "Status", + "length": 0, + "no_copy": 0, + "options": "Open\nReplied\nClosed\nLinked", "permlevel": 0, - "precision": "" + "precision": "", + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { - "fieldname": "reference_name", - "fieldtype": "Dynamic Link", - "label": "Reference Name", - "options": "reference_doctype", + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "sent_or_received", + "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 1, + "label": "Sent or Received", + "length": 0, + "no_copy": 0, + "options": "Sent\nReceived", "permlevel": 0, - "precision": "" + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "description": "Integrations can use this field to set email delivery status", + "fieldname": "delivery_status", + "fieldtype": "Select", + "hidden": 1, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Delivery Status", + "length": 0, + "no_copy": 0, + "options": "\nSent\nBounced\nOpened\nMarked As Spam\nRejected\nDelayed\nSoft-Bounced\nClicked\nRecipient Unsubscribed", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "section_break_10", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "subject", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Subject", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "section_break_8", "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "length": 0, + "no_copy": 0, "permlevel": 0, - "precision": "" + "precision": "", + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "content", "fieldtype": "Text Editor", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Content", + "length": 0, + "no_copy": 0, "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0, "width": "400" }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 1, "fieldname": "additional_info", "fieldtype": "Section Break", - "label": "Additional Info", - "permlevel": 0 - }, - { - "fieldname": "recipients", - "fieldtype": "Data", - "label": "Recipients", - "permlevel": 0 + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "More Information", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "sender", "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Sender", - "permlevel": 0 + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "sender_full_name", "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Sender Full Name", + "length": 0, + "no_copy": 0, "permlevel": 0, - "precision": "" - }, - { - "fieldname": "communication_medium", - "fieldtype": "Select", - "in_list_view": 1, - "label": "Communication Medium", - "options": "\nChat\nPhone\nEmail\nSMS\nVisit\nOther", - "permlevel": 0 - }, - { - "fieldname": "phone_no", - "fieldtype": "Data", - "label": "Phone No.", - "permlevel": 0 - }, - { - "fieldname": "section_break2", - "fieldtype": "Section Break", - "options": "simple", - "permlevel": 0 - }, - { - "fieldname": "column_break4", - "fieldtype": "Column Break", - "label": "By", - "permlevel": 0 - }, - { - "fieldname": "email_account", - "fieldtype": "Link", - "label": "Email Account", - "options": "Email Account", - "permlevel": 0, - "precision": "" - }, - { - "default": "__user", - "fieldname": "user", - "fieldtype": "Link", - "ignore_user_permissions": 1, - "label": "User", - "options": "User", - "permlevel": 0, - "read_only": 1 - }, - { - "fieldname": "column_break5", - "fieldtype": "Column Break", - "label": "On", - "permlevel": 0 + "precision": "", + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "default": "Today", "fieldname": "communication_date", "fieldtype": "Datetime", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Date", - "permlevel": 0 - }, - { - "fieldname": "_user_tags", - "fieldtype": "Data", - "hidden": 1, - "label": "User Tags", - "no_copy": 1, + "length": 0, + "no_copy": 0, "permlevel": 0, - "print_hide": 1 + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "column_break_14", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "reference_doctype", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Reference DocType", + "length": 0, + "no_copy": 0, + "options": "DocType", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "reference_name", + "fieldtype": "Dynamic Link", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Reference Name", + "length": 0, + "no_copy": 0, + "options": "reference_doctype", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "in_reply_to", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "In Reply To", + "length": 0, + "no_copy": 0, + "options": "Communication", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "email_account", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Email Account", + "length": 0, + "no_copy": 0, + "options": "Email Account", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "default": "__user", + "fieldname": "user", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 1, + "in_filter": 0, + "in_list_view": 0, + "label": "User", + "length": 0, + "no_copy": 0, + "options": "User", + "permlevel": 0, + "print_hide": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "default": "0", "fieldname": "unread_notification_sent", "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Unread Notification Sent", + "length": 0, + "no_copy": 0, "permlevel": 0, "precision": "", - "read_only": 1 + "print_hide": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "_user_tags", + "fieldtype": "Data", + "hidden": 1, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "User Tags", + "length": 0, + "no_copy": 1, + "permlevel": 0, + "print_hide": 1, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 } ], + "hide_heading": 0, + "hide_toolbar": 0, "icon": "icon-comment", "idx": 1, + "in_create": 0, "in_dialog": 0, + "is_submittable": 0, "issingle": 0, - "modified": "2015-03-23 02:33:55.289739", + "istable": 0, + "max_attachments": 0, + "modified": "2015-11-16 06:29:43.407180", "modified_by": "Administrator", "module": "Core", "name": "Communication", @@ -190,62 +607,27 @@ "permissions": [ { "amend": 0, - "apply_user_permissions": 1, - "create": 1, - "delete": 1, - "email": 1, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Support Team", - "share": 1, - "submit": 0, - "write": 1 - }, - { - "amend": 0, - "create": 1, - "delete": 1, - "email": 1, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Sales Manager", - "share": 1, - "submit": 0, - "write": 1 - }, - { - "amend": 0, - "apply_user_permissions": 1, - "create": 1, - "delete": 1, - "email": 1, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Sales User", - "share": 1, - "submit": 0, - "write": 1 - }, - { + "apply_user_permissions": 0, + "cancel": 0, "create": 1, "delete": 1, "email": 1, + "export": 0, + "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 } ], + "read_only": 0, + "read_only_onload": 0, "search_fields": "subject", "title_field": "subject" } \ No newline at end of file diff --git a/frappe/core/doctype/communication/communication.py b/frappe/core/doctype/communication/communication.py index 5fbf556568..a8053e672c 100644 --- a/frappe/core/doctype/communication/communication.py +++ b/frappe/core/doctype/communication/communication.py @@ -5,8 +5,9 @@ from __future__ import unicode_literals, absolute_import import frappe import json from email.utils import formataddr, parseaddr -from frappe.utils import get_url, get_formatted_email, cstr, cint +from frappe.utils import get_url, get_formatted_email, cint, validate_email_add, split_emails from frappe.utils.file_manager import get_file +from frappe.email.bulk import check_bulk_limit import frappe.email.smtp from frappe import _ @@ -16,6 +17,7 @@ class Communication(Document): no_feed_on_delete = True """Communication represents an external communication like Email.""" + def get_parent_doc(self): """Returns document of `reference_doctype`, `reference_doctype`""" if not hasattr(self, "parent_doc"): @@ -25,6 +27,31 @@ class Communication(Document): self.parent_doc = None return self.parent_doc + def validate(self): + if self.get("__islocal"): + if self.reference_doctype and self.reference_name: + self.status = "Linked" + else: + self.status = "Open" + + # validate recipients + for email in split_emails(self.recipients): + validate_email_add(email, throw=True) + + # validate CC + for email in split_emails(self.cc): + validate_email_add(email, throw=True) + + def after_insert(self): + # send new comment to listening clients + comment = self.as_dict() + comment["comment"] = comment["content"] + comment["comment_by"] = comment["sender"] + comment["comment_type"] = comment["communication_medium"] + + frappe.publish_realtime('new_comment', comment, doctype = self.reference_doctype, + docname = self.reference_name) + def on_update(self): """Update parent status as `Open` or `Replied`.""" self.update_parent() @@ -41,7 +68,9 @@ class Communication(Document): to_status = "Open" if self.sent_or_received=="Received" else "Replied" if to_status in status_field.options.splitlines(): - frappe.db.set_value(parent.doctype, parent.name, "status", to_status) + parent.db_set("status", to_status) + + parent.notify_update() def send(self, print_html=None, print_format=None, attachments=None, send_me_a_copy=False, recipients=None): @@ -53,31 +82,44 @@ class Communication(Document): self.send_me_a_copy = send_me_a_copy self.notify(print_html, print_format, attachments, recipients) - def set_incoming_outgoing_accounts(self): - self.incoming_email_account = self.outgoing_email_account = None + def notify(self, print_html=None, print_format=None, attachments=None, + recipients=None, cc=None, fetched_from_email_account=False): + """Calls a delayed celery task 'sendmail' that enqueus email in Bulk Email queue - if self.reference_doctype: - self.incoming_email_account = frappe.db.get_value("Email Account", - {"append_to": self.reference_doctype, "enable_incoming": 1}, "email_id") + :param print_html: Send given value as HTML attachment + :param print_format: Attach print format of parent document + :param attachments: A list of filenames that should be attached when sending this email + :param recipients: Email recipients + :param cc: Send email as CC to + :param fetched_from_email_account: True when pulling email, the notification shouldn't go to the main recipient - self.outgoing_email_account = frappe.db.get_value("Email Account", - {"append_to": self.reference_doctype, "enable_outgoing": 1}, - ["email_id", "always_use_account_email_id_as_sender"], as_dict=True) + """ + recipients, cc = self.get_recipients_and_cc(recipients, cc, + fetched_from_email_account=fetched_from_email_account) - if not self.incoming_email_account: - self.incoming_email_account = frappe.db.get_value("Email Account", {"default_incoming": 1}, "email_id") + self.emails_not_sent_to = set(self.all_email_addresses) - set(self.sent_email_addresses) - if not self.outgoing_email_account: - self.outgoing_email_account = frappe.db.get_value("Email Account", {"default_outgoing": 1}, - ["email_id", "always_use_account_email_id_as_sender"], as_dict=True) or frappe._dict() + if frappe.flags.in_test: + # for test cases, run synchronously + self._notify(print_html=print_html, print_format=print_format, attachments=attachments, + recipients=recipients, cc=cc) + else: + check_bulk_limit(list(set(self.sent_email_addresses))) + + from frappe.tasks import sendmail + sendmail.delay(frappe.local.site, self.name, + print_html=print_html, print_format=print_format, attachments=attachments, + recipients=recipients, cc=cc, lang=frappe.local.lang, session=frappe.local.session) + + def _notify(self, print_html=None, print_format=None, attachments=None, + recipients=None, cc=None): - def notify(self, print_html=None, print_format=None, attachments=None, recipients=None, except_recipient=False): self.prepare_to_notify(print_html, print_format, attachments) - if not recipients: - recipients = self.get_recipients(except_recipient=except_recipient) frappe.sendmail( - recipients=recipients, + recipients=(recipients or []) + (cc or []), + show_as_cc=(cc or []), + expose_recipients=True, sender=self.sender, reply_to=self.incoming_email_account, subject=self.subject, @@ -90,6 +132,33 @@ class Communication(Document): bulk=True ) + def get_recipients_and_cc(self, recipients, cc, fetched_from_email_account=False): + self.all_email_addresses = [] + self.sent_email_addresses = [] + self.previous_email_sender = None + + if not recipients: + recipients = self.get_recipients(fetched_from_email_account=fetched_from_email_account) + + if not cc: + cc = self.get_cc(recipients, fetched_from_email_account=fetched_from_email_account) + + if fetched_from_email_account: + # email was already sent to the original recipient by the sender's email service + original_recipients, recipients = recipients, [] + + # send email to the sender of the previous email in the thread which this email is a reply to + if self.previous_email_sender: + recipients.append(self.previous_email_sender) + + # cc that was received in the email + original_cc = split_emails(self.cc) + + # don't cc to people who already received the mail from sender's email service + cc = list(set(cc) - set(original_cc) - set(original_recipients)) + + return recipients, cc + def prepare_to_notify(self, print_html=None, print_format=None, attachments=None): """Prepare to make multipart MIME Email @@ -125,70 +194,134 @@ class Communication(Document): else: self.attachments.append(a) - def get_recipients(self, except_recipient=False): - """Build a list of users to which this email should go to""" + def set_incoming_outgoing_accounts(self): + self.incoming_email_account = self.outgoing_email_account = None + + if self.reference_doctype: + self.incoming_email_account = frappe.db.get_value("Email Account", + {"append_to": self.reference_doctype, "enable_incoming": 1}, "email_id") + + self.outgoing_email_account = frappe.db.get_value("Email Account", + {"append_to": self.reference_doctype, "enable_outgoing": 1}, + ["email_id", "always_use_account_email_id_as_sender"], as_dict=True) + + if not self.incoming_email_account: + self.incoming_email_account = frappe.db.get_value("Email Account", + {"default_incoming": 1, "enable_incoming": 1}, "email_id") + + if not self.outgoing_email_account: + self.outgoing_email_account = frappe.db.get_value("Email Account", + {"default_outgoing": 1, "enable_outgoing": 1}, + ["email_id", "always_use_account_email_id_as_sender"], as_dict=True) or frappe._dict() + + def get_recipients(self, fetched_from_email_account=False): + """Build a list of email addresses for To""" # [EDGE CASE] self.recipients can be None when an email is sent as BCC - original_recipients = [s.strip() for s in cstr(self.recipients).split(",")] - recipients = original_recipients[:] + recipients = split_emails(self.recipients) + + if fetched_from_email_account and self.in_reply_to: + # add sender of previous reply + self.previous_email_sender = frappe.db.get_value("Communication", self.in_reply_to, "sender") + recipients.append(self.previous_email_sender) + + if recipients: + # exclude email accounts + exclude = [d[0] for d in + frappe.db.get_all("Email Account", ["email_id"], {"enable_incoming": 1}, as_list=True)] + exclude += [d[0] for d in + frappe.db.get_all("Email Account", ["login_id"], {"enable_incoming": 1}, as_list=True) + if d[0]] + + recipients = self.filter_email_list(recipients, exclude) + + return recipients + + def get_cc(self, recipients=None, fetched_from_email_account=False): + """Build a list of email addresses for CC""" + # get a copy of CC list + cc = split_emails(self.cc) if self.reference_doctype and self.reference_name: - recipients += self.get_earlier_participants() - recipients += self.get_commentors() - recipients += self.get_assignees() - recipients += self.get_starrers() + if fetched_from_email_account: + # if it is a fetched email, add follows to CC + cc.append(self.get_owner_email()) + cc += self.get_assignees() + cc += self.get_starrers() - # remove unsubscribed recipients - unsubscribed = [d[0] for d in frappe.db.get_all("User", ["name"], {"thread_notify": 0}, as_list=True)] - email_accounts = [d[0] for d in frappe.db.get_all("Email Account", ["email_id"], {"enable_incoming": 1}, as_list=True)] - sender = parseaddr(self.sender)[1] + if cc: + # exclude email accounts, unfollows, recipients and unsubscribes + exclude = [d[0] for d in + frappe.db.get_all("Email Account", ["email_id"], {"enable_incoming": 1}, as_list=True)] + exclude += [d[0] for d in + frappe.db.get_all("Email Account", ["login_id"], {"enable_incoming": 1}, as_list=True) + if d[0]] + exclude += [d[0] for d in frappe.db.get_all("User", ["name"], {"thread_notify": 0}, as_list=True)] + exclude += [(parseaddr(email)[1] or "").lower() for email in recipients] + if fetched_from_email_account: + # exclude sender when pulling email + exclude += [parseaddr(self.sender)[1]] + + if self.reference_doctype and self.reference_name: + exclude += [d[0] for d in frappe.db.get_all("Email Unsubscribe", ["email"], + {"reference_doctype": self.reference_doctype, "reference_name": self.reference_name}, as_list=True)] + + cc = self.filter_email_list(cc, exclude, is_cc=True) + + if getattr(self, "send_me_a_copy", False) and self.sender not in cc: + self.all_email_addresses.append((parseaddr(self.sender)[1] or "").lower()) + cc.append(self.sender) + + return cc + + def filter_email_list(self, email_list, exclude, is_cc=False): + # temp variables filtered = [] - for e in list(set(recipients)): - if (e=="Administrator") or ((e==self.sender) and (e not in original_recipients)) or \ - (e in unsubscribed) or (e in email_accounts): + email_address_list = [] + + for email in list(set(email_list)): + email_address = (parseaddr(email)[1] or "").lower() + if not email_address: continue - email_id = parseaddr(e)[1] - if email_id==sender or email_id in unsubscribed or email_id in email_accounts: + # this will be used to eventually find email addresses that aren't sent to + self.all_email_addresses.append(email_address) + + if (email in exclude) or (email_address in exclude): continue - if except_recipient and (e==self.recipients or email_id==self.recipients): - # while pulling email, don't send email to current recipient - continue + if is_cc: + is_user_enabled = frappe.db.get_value("User", email_address, "enabled") + if is_user_enabled==0: + # don't send to disabled users + continue - if e not in filtered and email_id not in filtered: - filtered.append(e) + # make sure of case-insensitive uniqueness of email address + if email_address not in email_address_list: + # append the full email i.e. "Human " + filtered.append(email) + email_address_list.append(email_address) - if getattr(self, "send_me_a_copy", False): - filtered.append(self.sender) + self.sent_email_addresses.extend(email_address_list) return filtered def get_starrers(self): """Return list of users who have starred this document.""" - if self.reference_doctype and self.reference_name: - return self.get_parent_doc().get_starred_by() - else: - return [] + return [( get_formatted_email(user) or user ) for user in self.get_parent_doc().get_starred_by()] - def get_earlier_participants(self): - return frappe.db.sql_list(""" - select distinct sender - from tabCommunication where - reference_doctype=%s and reference_name=%s""", - (self.reference_doctype, self.reference_name)) - - def get_commentors(self): - return frappe.db.sql_list(""" - select distinct comment_by - from tabComment where - comment_doctype=%s and comment_docname=%s and - ifnull(unsubscribed, 0)=0 and comment_by!='Administrator'""", - (self.reference_doctype, self.reference_name)) + def get_owner_email(self): + owner = self.get_parent_doc().owner + return get_formatted_email(owner) or owner def get_assignees(self): - return [d.owner for d in frappe.db.get_all("ToDo", filters={"reference_type": self.reference_doctype, - "reference_name": self.reference_name, "status": "Open"}, fields=["owner"])] + return [( get_formatted_email(d.owner) or d.owner ) for d in + frappe.db.get_all("ToDo", filters={ + "reference_type": self.reference_doctype, + "reference_name": self.reference_name, + "status": "Open" + }, fields=["owner"]) + ] def get_attach_link(self, print_format): """Returns public link for the attachment via `templates/emails/print_link.html`.""" @@ -208,7 +341,7 @@ def on_doctype_update(): def make(doctype=None, name=None, content=None, subject=None, sent_or_received = "Sent", sender=None, recipients=None, communication_medium="Email", send_email=False, print_html=None, print_format=None, attachments='[]', ignore_doctype_permissions=False, - send_me_a_copy=False): + send_me_a_copy=False, cc=None): """Make a new communication. :param doctype: Reference DocType. @@ -232,7 +365,7 @@ def make(doctype=None, name=None, content=None, subject=None, sent_or_received = raise frappe.PermissionError("You are not allowed to send emails related to: {doctype} {name}".format( doctype=doctype, name=name)) - if not sender and frappe.session.user != "Administrator": + if not sender: sender = get_formatted_email(frappe.session.user) comm = frappe.get_doc({ @@ -241,24 +374,23 @@ def make(doctype=None, name=None, content=None, subject=None, sent_or_received = "content": content, "sender": sender, "recipients": recipients, - "communication_medium": "Email", + "cc": cc or None, + "communication_medium": communication_medium, "sent_or_received": sent_or_received, "reference_doctype": doctype, "reference_name": name }) comm.insert(ignore_permissions=True) - - recipients = None + + # needed for communication.notify which uses celery delay + # if not committed, delayed task doesn't find the communication + frappe.db.commit() + if send_email: comm.send_me_a_copy = send_me_a_copy - recipients = comm.get_recipients() - comm.send(print_html, print_format, attachments, send_me_a_copy=send_me_a_copy, recipients=recipients) + comm.send(print_html, print_format, attachments, send_me_a_copy=send_me_a_copy) return { "name": comm.name, - "recipients": ", ".join(recipients) if recipients else None + "emails_not_sent_to": ", ".join(comm.emails_not_sent_to) if hasattr(comm, "emails_not_sent_to") else None } - -@frappe.whitelist() -def get_convert_to(): - return frappe.get_hooks("communication_convert_to") diff --git a/frappe/core/doctype/defaultvalue/defaultvalue.json b/frappe/core/doctype/defaultvalue/defaultvalue.json index 0fa7b618df..f9d8b4f3d9 100644 --- a/frappe/core/doctype/defaultvalue/defaultvalue.json +++ b/frappe/core/doctype/defaultvalue/defaultvalue.json @@ -1,49 +1,81 @@ { "allow_copy": 0, + "allow_import": 0, + "allow_rename": 0, "autoname": "hash", "creation": "2013-02-22 01:27:32", + "custom": 0, "docstatus": 0, "doctype": "DocType", "fields": [ { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "defkey", "fieldtype": "Data", "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, "in_list_view": 1, "label": "Key", + "length": 0, + "no_copy": 0, "oldfieldname": "defkey", "oldfieldtype": "Data", "permlevel": 0, + "print_hide": 0, "print_width": "200px", + "read_only": 0, + "report_hide": 0, "reqd": 1, "search_index": 0, + "set_only_once": 0, + "unique": 0, "width": "200px" }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "defvalue", "fieldtype": "Text", "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, "in_list_view": 1, "label": "Value", + "length": 0, + "no_copy": 0, "oldfieldname": "defvalue", "oldfieldtype": "Text", "permlevel": 0, + "print_hide": 0, "print_width": "200px", + "read_only": 0, + "report_hide": 0, "reqd": 0, "search_index": 0, + "set_only_once": 0, + "unique": 0, "width": "200px" } ], "hide_heading": 0, "hide_toolbar": 0, "idx": 1, + "in_create": 0, + "in_dialog": 0, + "is_submittable": 0, "issingle": 0, "istable": 1, - "modified": "2015-02-19 01:06:59.622792", + "max_attachments": 0, + "modified": "2015-11-16 06:29:44.604889", "modified_by": "Administrator", "module": "Core", "name": "DefaultValue", "owner": "Administrator", "permissions": [], - "read_only": 0 + "read_only": 0, + "read_only_onload": 0 } \ No newline at end of file diff --git a/frappe/core/doctype/docfield/docfield.json b/frappe/core/doctype/docfield/docfield.json index 0a3a3b9cd3..f53ebdda47 100644 --- a/frappe/core/doctype/docfield/docfield.json +++ b/frappe/core/doctype/docfield/docfield.json @@ -1,331 +1,960 @@ { "allow_copy": 0, + "allow_import": 0, + "allow_rename": 0, "autoname": "hash", "creation": "2013-02-22 01:27:33", + "custom": 0, "docstatus": 0, "doctype": "DocType", "fields": [ { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "label_and_type", "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "", - "permlevel": 0 + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 1, + "collapsible": 0, "fieldname": "label", "fieldtype": "Data", "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, "in_list_view": 1, "label": "Label", + "length": 0, + "no_copy": 0, "oldfieldname": "label", "oldfieldtype": "Data", "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, "print_width": "163", + "read_only": 0, + "report_hide": 0, "reqd": 0, "search_index": 1, + "set_only_once": 0, + "unique": 0, "width": "163" }, { + "allow_on_submit": 0, + "bold": 1, + "collapsible": 0, "default": "Data", "fieldname": "fieldtype", "fieldtype": "Select", "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, "in_list_view": 1, "label": "Type", + "length": 0, + "no_copy": 0, "oldfieldname": "fieldtype", "oldfieldtype": "Select", "options": "Attach\nAttach Image\nButton\nCheck\nCode\nColumn Break\nCurrency\nData\nDate\nDatetime\nDynamic Link\nFloat\nFold\nHeading\nHTML\nImage\nInt\nLink\nLong Text\nPassword\nPercent\nRead Only\nSection Break\nSelect\nSmall Text\nTable\nText\nText Editor\nTime", "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, "reqd": 1, - "search_index": 1 + "search_index": 1, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 1, + "collapsible": 0, "fieldname": "fieldname", "fieldtype": "Data", "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, "in_list_view": 1, "label": "Name", + "length": 0, + "no_copy": 0, "oldfieldname": "fieldname", "oldfieldtype": "Data", "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, "reqd": 0, - "search_index": 1 + "search_index": 1, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "reqd", "fieldtype": "Check", "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, "in_list_view": 1, "label": "Mandatory", + "length": 0, + "no_copy": 0, "oldfieldname": "reqd", "oldfieldtype": "Check", "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, "print_width": "50px", + "read_only": 0, + "report_hide": 0, "reqd": 0, "search_index": 0, + "set_only_once": 0, + "unique": 0, "width": "50px" }, { - "fieldname": "search_index", - "fieldtype": "Check", - "hidden": 0, - "label": "Index", - "oldfieldname": "search_index", - "oldfieldtype": "Check", - "permlevel": 0, - "print_width": "50px", - "reqd": 0, - "search_index": 0, - "width": "50px" - }, - { - "fieldname": "in_list_view", - "fieldtype": "Check", - "label": "In List View", - "permlevel": 0, - "print_width": "70px", - "width": "70px" - }, - { - "fieldname": "column_break_6", - "fieldtype": "Column Break", - "permlevel": 0 - }, - { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "depends_on": "eval:in_list([\"Float\", \"Currency\", \"Percent\"], doc.fieldtype)", "description": "Set non-standard precision for a Float or Currency field", "fieldname": "precision", "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Precision", + "length": 0, + "no_copy": 0, "options": "\n1\n2\n3\n4\n5\n6\n7\n8\n9", "permlevel": 0, - "print_hide": 1 + "print_hide": 1, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "depends_on": "eval:in_list(['Data', 'Link', 'Dynamic Link', 'Password', 'Select', 'Read Only', 'Attach', 'Attach Image'], doc.fieldtype)", + "fieldname": "length", + "fieldtype": "Int", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Length", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "search_index", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Index", + "length": 0, + "no_copy": 0, + "oldfieldname": "search_index", + "oldfieldtype": "Check", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": "50px", + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0, + "width": "50px" + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "in_list_view", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "In List View", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": "70px", + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0, + "width": "70px" + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "bold", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Bold", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "depends_on": "eval:doc.fieldtype===\"Section Break\"", + "fieldname": "collapsible", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Collapsible", + "length": 255, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "depends_on": "eval:doc.fieldtype==\"Section Break\"", + "fieldname": "collapsible_depends_on", + "fieldtype": "Small Text", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Collapsible Depends On", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "column_break_6", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "description": "For Links, enter the DocType as range.\nFor Select, enter list of Options, each on a new line.", "fieldname": "options", "fieldtype": "Text", "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, "in_list_view": 1, "label": "Options", + "length": 0, + "no_copy": 0, "oldfieldname": "options", "oldfieldtype": "Text", "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, "reqd": 0, - "search_index": 0 + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "default", + "fieldtype": "Small Text", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Default", + "length": 0, + "no_copy": 0, + "oldfieldname": "default", + "oldfieldtype": "Text", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "permissions", "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Permissions", - "permlevel": 0 + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "depends_on", - "fieldtype": "Data", + "fieldtype": "Small Text", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Depends On", + "length": 255, + "no_copy": 0, "oldfieldname": "depends_on", "oldfieldtype": "Data", - "permlevel": 0 + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "hidden", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Hidden", + "length": 0, + "no_copy": 0, + "oldfieldname": "hidden", + "oldfieldtype": "Check", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": "50px", + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0, + "width": "50px" + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "read_only", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Read Only", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": "50px", + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0, + "width": "50px" + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "unique", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Unique", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "description": "Do not allow user to change after set the first time", + "fieldname": "set_only_once", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Set Only Once", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "column_break_13", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "default": "0", "fieldname": "permlevel", "fieldtype": "Int", "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Perm Level", + "length": 0, + "no_copy": 0, "oldfieldname": "permlevel", "oldfieldtype": "Int", "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, "print_width": "50px", + "read_only": 0, + "report_hide": 0, "reqd": 0, "search_index": 0, + "set_only_once": 0, + "unique": 0, "width": "50px" }, { - "fieldname": "hidden", - "fieldtype": "Check", - "hidden": 0, - "label": "Hidden", - "oldfieldname": "hidden", - "oldfieldtype": "Check", - "permlevel": 0, - "print_width": "50px", - "reqd": 0, - "search_index": 0, - "width": "50px" - }, - { - "fieldname": "unique", - "fieldtype": "Check", - "label": "Unique", - "permlevel": 0, - "precision": "" - }, - { - "fieldname": "read_only", - "fieldtype": "Check", - "label": "Read Only", - "permlevel": 0, - "print_width": "50px", - "width": "50px" - }, - { - "description": "Do not allow user to change after set the first time", - "fieldname": "set_only_once", - "fieldtype": "Check", - "label": "Set Only Once", - "permlevel": 0 - }, - { - "fieldname": "column_break_13", - "fieldtype": "Column Break", - "permlevel": 0 - }, - { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "description": "User permissions should not apply for this Link", "fieldname": "ignore_user_permissions", "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Ignore User Permissions", - "permlevel": 0 + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "allow_on_submit", "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Allow on Submit", + "length": 0, + "no_copy": 0, "oldfieldname": "allow_on_submit", "oldfieldtype": "Check", "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, "print_width": "50px", + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0, "width": "50px" }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "report_hide", "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Report Hide", + "length": 0, + "no_copy": 0, "oldfieldname": "report_hide", "oldfieldtype": "Check", "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, "print_width": "50px", + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0, "width": "50px" }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "display", "fieldtype": "Section Break", - "label": "Display", - "permlevel": 0 - }, - { - "fieldname": "default", - "fieldtype": "Text", "hidden": 0, - "label": "Default", - "oldfieldname": "default", - "oldfieldtype": "Text", + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Display", + "length": 0, + "no_copy": 0, "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, "reqd": 0, - "search_index": 0 + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "in_filter", "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "In Filter", + "length": 0, + "no_copy": 0, "oldfieldname": "in_filter", "oldfieldtype": "Check", "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, "print_width": "50px", + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0, "width": "50px" }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "no_copy", "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "No Copy", + "length": 0, + "no_copy": 0, "oldfieldname": "no_copy", "oldfieldtype": "Check", "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, "print_width": "50px", + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0, "width": "50px" }, { - "fieldname": "column_break_22", - "fieldtype": "Column Break", - "permlevel": 0 - }, - { - "fieldname": "description", - "fieldtype": "Text", - "in_list_view": 1, - "label": "Description", - "oldfieldname": "description", - "oldfieldtype": "Text", - "permlevel": 0, - "print_width": "300px", - "width": "300px" - }, - { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "print_hide", "fieldtype": "Check", "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Print Hide", + "length": 0, + "no_copy": 0, "oldfieldname": "print_hide", "oldfieldtype": "Check", "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, "print_width": "50px", + "read_only": 0, + "report_hide": 0, "reqd": 0, "search_index": 0, + "set_only_once": 0, + "unique": 0, "width": "50px" }, { - "fieldname": "print_width", - "fieldtype": "Data", - "label": "Print Width", - "permlevel": 0 + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "depends_on": "eval:[\"Int\", \"Float\", \"Currency\", \"Percent\"].indexOf(doc.fieldtype)!==-1", + "fieldname": "print_hide_if_no_value", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Print Hide If No Value", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "print_width", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Print Width", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "width", "fieldtype": "Data", "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Width", + "length": 0, + "no_copy": 0, "oldfieldname": "width", "oldfieldtype": "Data", "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, "print_width": "50px", + "read_only": 0, + "report_hide": 0, "reqd": 0, "search_index": 0, + "set_only_once": 0, + "unique": 0, "width": "50px" }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "column_break_22", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "description", + "fieldtype": "Small Text", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 1, + "label": "Description", + "length": 0, + "no_copy": 0, + "oldfieldname": "description", + "oldfieldtype": "Text", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": "300px", + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0, + "width": "300px" + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "oldfieldname", "fieldtype": "Data", "hidden": 1, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "length": 0, + "no_copy": 0, "oldfieldname": "oldfieldname", "oldfieldtype": "Data", "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, "reqd": 0, - "search_index": 0 + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "oldfieldtype", "fieldtype": "Data", "hidden": 1, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "length": 0, + "no_copy": 0, "oldfieldname": "oldfieldtype", "oldfieldtype": "Data", "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, "reqd": 0, - "search_index": 0 + "search_index": 0, + "set_only_once": 0, + "unique": 0 } ], "hide_heading": 0, "hide_toolbar": 0, "idx": 1, + "in_create": 0, "in_dialog": 1, + "is_submittable": 0, "issingle": 0, "istable": 1, - "modified": "2015-05-15 14:28:16.623971", + "max_attachments": 0, + "modified": "2015-11-24 02:28:08.985496", "modified_by": "Administrator", "module": "Core", "name": "DocField", "owner": "Administrator", "permissions": [], - "read_only": 0 + "read_only": 0, + "read_only_onload": 0 } \ No newline at end of file diff --git a/frappe/core/doctype/docfield/docfield.py b/frappe/core/doctype/docfield/docfield.py index 9dae3aea53..6b53be3288 100644 --- a/frappe/core/doctype/docfield/docfield.py +++ b/frappe/core/doctype/docfield/docfield.py @@ -6,5 +6,4 @@ from __future__ import unicode_literals from frappe.model.document import Document class DocField(Document): - __doclink__ = "https://frappe.io/docs/models/core/docfield" pass diff --git a/frappe/core/doctype/docperm/docperm.json b/frappe/core/doctype/docperm/docperm.json index 04f8b0c19e..c8085aa8fb 100644 --- a/frappe/core/doctype/docperm/docperm.json +++ b/frappe/core/doctype/docperm/docperm.json @@ -1,242 +1,628 @@ { "allow_copy": 0, + "allow_import": 0, + "allow_rename": 0, "autoname": "hash", "creation": "2013-02-22 01:27:33", + "custom": 0, "docstatus": 0, "doctype": "DocType", "fields": [ { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "role_and_level", "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Role and Level", - "permlevel": 0 + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "role", "fieldtype": "Link", "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, "in_list_view": 1, "label": "Role", + "length": 0, + "no_copy": 0, "oldfieldname": "role", "oldfieldtype": "Link", "options": "Role", "permlevel": 0, + "print_hide": 0, "print_width": "150px", + "read_only": 0, + "report_hide": 0, "reqd": 1, "search_index": 0, + "set_only_once": 0, + "unique": 0, "width": "150px" }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "description": "Filter records based on User Permissions defined for a user", "fieldname": "apply_user_permissions", "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Apply User Permissions", - "permlevel": 0 + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "description": "Apply this rule if the User is the Owner", + "fieldname": "if_owner", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "If user is the owner", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "column_break_2", "fieldtype": "Column Break", - "permlevel": 0 + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "default": "0", "fieldname": "permlevel", "fieldtype": "Int", "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, "in_list_view": 1, "label": "Level", + "length": 0, + "no_copy": 0, "oldfieldname": "permlevel", "oldfieldtype": "Int", "permlevel": 0, + "print_hide": 0, "print_width": "40px", + "read_only": 0, + "report_hide": 0, "reqd": 0, "search_index": 0, + "set_only_once": 0, + "unique": 0, "width": "40px" }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "depends_on": "", "description": "JSON list of DocTypes used to apply User Permissions. If empty, all linked DocTypes will be used to apply User Permissions.", "fieldname": "user_permission_doctypes", "fieldtype": "Text", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "User Permission DocTypes", + "length": 0, + "no_copy": 0, "permlevel": 0, - "read_only": 1 + "print_hide": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "section_break_4", "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Permissions", - "permlevel": 0 + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "default": "1", "fieldname": "read", "fieldtype": "Check", "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, "in_list_view": 1, "label": "Read", + "length": 0, + "no_copy": 0, "oldfieldname": "read", "oldfieldtype": "Check", "permlevel": 0, + "print_hide": 0, "print_width": "32px", + "read_only": 0, + "report_hide": 0, "reqd": 0, "search_index": 0, + "set_only_once": 0, + "unique": 0, "width": "32px" }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "default": "1", "fieldname": "write", "fieldtype": "Check", "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, "in_list_view": 1, "label": "Write", + "length": 0, + "no_copy": 0, "oldfieldname": "write", "oldfieldtype": "Check", "permlevel": 0, + "print_hide": 0, "print_width": "32px", + "read_only": 0, + "report_hide": 0, "reqd": 0, "search_index": 0, + "set_only_once": 0, + "unique": 0, "width": "32px" }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "default": "1", "fieldname": "create", "fieldtype": "Check", "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, "in_list_view": 1, "label": "Create", + "length": 0, + "no_copy": 0, "oldfieldname": "create", "oldfieldtype": "Check", "permlevel": 0, + "print_hide": 0, "print_width": "32px", + "read_only": 0, + "report_hide": 0, "reqd": 0, "search_index": 0, + "set_only_once": 0, + "unique": 0, "width": "32px" }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "default": "1", "fieldname": "delete", "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, "in_list_view": 1, "label": "Delete", - "permlevel": 0 + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "column_break_8", "fieldtype": "Column Break", - "permlevel": 0 + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "submit", "fieldtype": "Check", "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, "in_list_view": 1, "label": "Submit", + "length": 0, + "no_copy": 0, "oldfieldname": "submit", "oldfieldtype": "Check", "permlevel": 0, + "print_hide": 0, "print_width": "32px", + "read_only": 0, + "report_hide": 0, "reqd": 0, "search_index": 0, + "set_only_once": 0, + "unique": 0, "width": "32px" }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "cancel", "fieldtype": "Check", "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, "in_list_view": 1, "label": "Cancel", + "length": 0, + "no_copy": 0, "oldfieldname": "cancel", "oldfieldtype": "Check", "permlevel": 0, + "print_hide": 0, "print_width": "32px", + "read_only": 0, + "report_hide": 0, "reqd": 0, "search_index": 0, + "set_only_once": 0, + "unique": 0, "width": "32px" }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "amend", "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Amend", + "length": 0, + "no_copy": 0, "oldfieldname": "amend", "oldfieldtype": "Check", "permlevel": 0, + "print_hide": 0, "print_width": "32px", + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0, "width": "32px" }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "additional_permissions", "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Additional Permissions", - "permlevel": 0 + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "default": "1", "fieldname": "report", "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Report", + "length": 0, + "no_copy": 0, "permlevel": 0, + "print_hide": 0, "print_width": "32px", + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0, "width": "32px" }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "default": "1", "fieldname": "export", "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Export", - "permlevel": 0 + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "import", "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Import", - "permlevel": 0 + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "description": "This role update User Permissions for a user", "fieldname": "set_user_permissions", "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Set User Permissions", - "permlevel": 0 + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "column_break_19", "fieldtype": "Column Break", - "permlevel": 0 + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "default": "1", "fieldname": "share", "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Share", + "length": 0, + "no_copy": 0, "permlevel": 0, - "precision": "" + "precision": "", + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "default": "1", "fieldname": "print", "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Print", - "permlevel": 0 + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "default": "1", "fieldname": "email", "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Email", - "permlevel": 0 + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 } ], "hide_heading": 0, "hide_toolbar": 0, "idx": 1, + "in_create": 0, + "in_dialog": 0, + "is_submittable": 0, "issingle": 0, "istable": 1, - "modified": "2015-03-18 06:09:58.928014", + "max_attachments": 0, + "modified": "2015-11-16 06:29:45.429411", "modified_by": "Administrator", "module": "Core", "name": "DocPerm", "owner": "Administrator", "permissions": [], - "read_only": 0 + "read_only": 0, + "read_only_onload": 0 } \ No newline at end of file diff --git a/frappe/core/doctype/docperm/docperm.py b/frappe/core/doctype/docperm/docperm.py index 919ed1179e..36ed9acbe6 100644 --- a/frappe/core/doctype/docperm/docperm.py +++ b/frappe/core/doctype/docperm/docperm.py @@ -7,5 +7,4 @@ import frappe from frappe.model.document import Document class DocPerm(Document): - __doclink__ = "https://frappe.io/docs/models/v5.x/core/docperm" pass diff --git a/frappe/core/doctype/docshare/docshare.json b/frappe/core/doctype/docshare/docshare.json index 68ebe45c7b..75c2ac1b37 100644 --- a/frappe/core/doctype/docshare/docshare.json +++ b/frappe/core/doctype/docshare/docshare.json @@ -12,6 +12,8 @@ "fields": [ { "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "user", "fieldtype": "Link", "hidden": 0, @@ -19,6 +21,7 @@ "in_filter": 0, "in_list_view": 1, "label": "User", + "length": 0, "no_copy": 0, "options": "User", "permlevel": 0, @@ -26,12 +29,15 @@ "print_hide": 0, "read_only": 0, "report_hide": 0, - "reqd": 1, + "reqd": 0, "search_index": 1, - "set_only_once": 0 + "set_only_once": 0, + "unique": 0 }, { "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "share_doctype", "fieldtype": "Link", "hidden": 0, @@ -39,6 +45,7 @@ "in_filter": 0, "in_list_view": 1, "label": "Document Type", + "length": 0, "no_copy": 0, "options": "DocType", "permlevel": 0, @@ -48,10 +55,13 @@ "report_hide": 0, "reqd": 1, "search_index": 1, - "set_only_once": 0 + "set_only_once": 0, + "unique": 0 }, { "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "share_name", "fieldtype": "Dynamic Link", "hidden": 0, @@ -59,6 +69,7 @@ "in_filter": 0, "in_list_view": 1, "label": "Document Name", + "length": 0, "no_copy": 0, "options": "share_doctype", "permlevel": 0, @@ -68,10 +79,13 @@ "report_hide": 0, "reqd": 1, "search_index": 1, - "set_only_once": 0 + "set_only_once": 0, + "unique": 0 }, { "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "default": "0", "fieldname": "read", "fieldtype": "Check", @@ -80,6 +94,7 @@ "in_filter": 0, "in_list_view": 0, "label": "Read", + "length": 0, "no_copy": 0, "permlevel": 0, "precision": "", @@ -88,10 +103,13 @@ "report_hide": 0, "reqd": 0, "search_index": 0, - "set_only_once": 0 + "set_only_once": 0, + "unique": 0 }, { "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "default": "0", "fieldname": "write", "fieldtype": "Check", @@ -100,6 +118,7 @@ "in_filter": 0, "in_list_view": 0, "label": "Write", + "length": 0, "no_copy": 0, "permlevel": 0, "precision": "", @@ -108,10 +127,13 @@ "report_hide": 0, "reqd": 0, "search_index": 0, - "set_only_once": 0 + "set_only_once": 0, + "unique": 0 }, { "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "default": "0", "fieldname": "share", "fieldtype": "Check", @@ -120,6 +142,7 @@ "in_filter": 0, "in_list_view": 0, "label": "Share", + "length": 0, "no_copy": 0, "permlevel": 0, "precision": "", @@ -128,7 +151,31 @@ "report_hide": 0, "reqd": 0, "search_index": 0, - "set_only_once": 0 + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "everyone", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Everyone", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 } ], "hide_heading": 0, @@ -138,7 +185,8 @@ "is_submittable": 0, "issingle": 0, "istable": 0, - "modified": "2015-02-12 11:30:52.968078", + "max_attachments": 0, + "modified": "2015-11-16 06:29:45.569747", "modified_by": "Administrator", "module": "Core", "name": "DocShare", @@ -153,6 +201,7 @@ "delete": 1, "email": 0, "export": 1, + "if_owner": 0, "import": 1, "permlevel": 0, "print": 0, diff --git a/frappe/core/doctype/docshare/docshare.py b/frappe/core/doctype/docshare/docshare.py index cf79e0531c..bbe31f9e84 100644 --- a/frappe/core/doctype/docshare/docshare.py +++ b/frappe/core/doctype/docshare/docshare.py @@ -7,10 +7,13 @@ from frappe.model.document import Document from frappe import _ from frappe.utils import get_fullname +exclude_from_linked_with = True + class DocShare(Document): no_feed_on_delete = True def validate(self): + self.validate_user() self.check_share_permission() self.cascade_permissions_downwards() self.get_doc().run_method("validate_share", self) @@ -26,6 +29,12 @@ class DocShare(Document): self._doc = frappe.get_doc(self.share_doctype, self.share_name) return self._doc + def validate_user(self): + if self.everyone: + self.user = None + elif not self.user: + frappe.throw(_("User is mandatory for Share"), frappe.MandatoryError) + def check_share_permission(self): if (not self.flags.ignore_share_permission and not frappe.has_permission(self.share_doctype, "share", self.get_doc())): diff --git a/frappe/core/doctype/docshare/test_docshare.py b/frappe/core/doctype/docshare/test_docshare.py index 4fb522e267..e4a41ac0d2 100644 --- a/frappe/core/doctype/docshare/test_docshare.py +++ b/frappe/core/doctype/docshare/test_docshare.py @@ -78,3 +78,15 @@ class TestDocShare(unittest.TestCase): frappe.set_user(self.user) self.assertFalse(self.event.has_permission("share")) + def test_share_with_everyone(self): + self.assertTrue(self.event.name not in frappe.share.get_shared("Event", self.user)) + + frappe.share.set_permission("Event", self.event.name, None, "read", everyone=1) + self.assertTrue(self.event.name in frappe.share.get_shared("Event", self.user)) + self.assertTrue(self.event.name in frappe.share.get_shared("Event", "test1@example.com")) + self.assertTrue(self.event.name not in frappe.share.get_shared("Event", "Guest")) + + frappe.share.set_permission("Event", self.event.name, None, "read", value=0, everyone=1) + self.assertTrue(self.event.name not in frappe.share.get_shared("Event", self.user)) + self.assertTrue(self.event.name not in frappe.share.get_shared("Event", "test1@example.com")) + self.assertTrue(self.event.name not in frappe.share.get_shared("Event", "Guest")) diff --git a/frappe/core/doctype/doctype/doctype.json b/frappe/core/doctype/doctype/doctype.json index e829d3af27..7acda02a74 100644 --- a/frappe/core/doctype/doctype/doctype.json +++ b/frappe/core/doctype/doctype/doctype.json @@ -1,5 +1,6 @@ { "allow_copy": 0, + "allow_import": 0, "allow_rename": 1, "autoname": "Prompt", "creation": "2013-02-18 13:36:19", @@ -7,371 +8,936 @@ "description": "DocType is a Table / Form in the application.", "docstatus": 0, "doctype": "DocType", + "document_type": "Document", "fields": [ { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "sb0", "fieldtype": "Section Break", "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "", + "length": 0, + "no_copy": 0, "oldfieldtype": "Section Break", "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, "reqd": 0, - "search_index": 0 + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "module", "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, "in_list_view": 1, "label": "Module", + "length": 0, + "no_copy": 0, "oldfieldname": "module", "oldfieldtype": "Link", "options": "Module Def", "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, "reqd": 1, - "search_index": 1 + "search_index": 1, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "description": "Child Tables are shown as a Grid in other DocTypes.", "fieldname": "istable", "fieldtype": "Check", "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Is Child Table", + "length": 0, + "no_copy": 0, "oldfieldname": "istable", "oldfieldtype": "Check", "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, "reqd": 0, - "search_index": 0 + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "description": "Single Types have only one record no tables associated. Values are stored in tabSingles", "fieldname": "issingle", "fieldtype": "Check", "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Is Single", + "length": 0, + "no_copy": 0, "oldfieldname": "issingle", "oldfieldtype": "Check", "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, "reqd": 0, - "search_index": 0 + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "cb01", "fieldtype": "Column Break", - "permlevel": 0 + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "document_type", "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Document Type", + "length": 0, + "no_copy": 0, "oldfieldname": "document_type", "oldfieldtype": "Select", - "options": "\nMaster\nTransaction\nSystem\nOther", - "permlevel": 0 + "options": "\nDocument\nSetup\nSystem\nOther", + "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "icon", "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Icon", - "permlevel": 0 + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "custom", "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Custom?", + "length": 0, "no_copy": 0, - "permlevel": 0 + "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { - "fieldname": "plugin", + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "app", "fieldtype": "Data", "hidden": 1, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Plugin", + "length": 0, + "no_copy": 0, "permlevel": 0, - "read_only": 0 + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "fields_section_break", "fieldtype": "Section Break", "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Fields", + "length": 0, + "no_copy": 0, "oldfieldtype": "Section Break", "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, "reqd": 0, - "search_index": 0 + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "fields", "fieldtype": "Table", "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Fields", + "length": 0, + "no_copy": 0, "oldfieldname": "fields", "oldfieldtype": "Table", "options": "DocField", "permlevel": 0, - "reqd": 1, - "search_index": 0 + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "sb1", "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Naming", - "permlevel": 0 + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "description": "\\\n
  • field:[fieldname] - By Field\\\n
  • naming_series: - By Naming Series (field called naming_series must be present\\\n
  • Prompt - Prompt user for a name\\\n
  • [series] - Series by prefix (separated by a dot); for example PRE.#####\\\n')\">Naming Options", "fieldname": "autoname", "fieldtype": "Data", "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Auto Name", + "length": 0, + "no_copy": 0, "oldfieldname": "autoname", "oldfieldtype": "Data", "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, "reqd": 0, - "search_index": 0 + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "name_case", "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Name Case", + "length": 0, + "no_copy": 0, "oldfieldname": "name_case", "oldfieldtype": "Select", "options": "\nTitle Case\nUPPER CASE", - "permlevel": 0 + "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "description", "fieldtype": "Small Text", "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Description", + "length": 0, + "no_copy": 0, "oldfieldname": "description", "oldfieldtype": "Text", "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, "reqd": 0, - "search_index": 0 + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "column_break_15", "fieldtype": "Column Break", - "permlevel": 0 + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "description": "Show this field as title", "fieldname": "title_field", "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Title Field", - "permlevel": 0 + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "search_fields", "fieldtype": "Data", "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Search Fields", + "length": 0, + "no_copy": 0, "oldfieldname": "search_fields", "oldfieldtype": "Data", "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, "reqd": 0, - "search_index": 0 + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "default": "modified", "description": "", "fieldname": "sort_field", "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Sort Field", - "permlevel": 0 + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "default": "DESC", "fieldname": "sort_order", "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Sort Order", + "length": 0, + "no_copy": 0, "options": "ASC\nDESC", - "permlevel": 0 + "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "depends_on": "eval:!doc.istable", "fieldname": "sb2", "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Permission Rules", - "permlevel": 0 + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "depends_on": "", "fieldname": "permissions", "fieldtype": "Table", "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Permissions", + "length": 0, + "no_copy": 0, "oldfieldname": "permissions", "oldfieldtype": "Table", "options": "DocPerm", "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, "reqd": 0, - "search_index": 0 + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "depends_on": "eval:!doc.istable", "fieldname": "sb3", "fieldtype": "Section Break", - "permlevel": 0 + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "cb30", "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Permissions Settings", - "permlevel": 0 + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "in_create", "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "User Cannot Create", + "length": 0, + "no_copy": 0, "oldfieldname": "in_create", "oldfieldtype": "Check", - "permlevel": 0 + "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "read_only", "fieldtype": "Check", "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "User Cannot Search", + "length": 0, + "no_copy": 0, "oldfieldname": "read_only", "oldfieldtype": "Check", "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, "reqd": 0, - "search_index": 0 + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "is_submittable", "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Is Submittable", - "permlevel": 0 + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "description": "Allow Import via Data Import Tool", "fieldname": "allow_import", "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Allow Import", - "permlevel": 0 + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "allow_rename", "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Allow Rename", + "length": 0, + "no_copy": 0, "oldfieldname": "allow_rename", "oldfieldtype": "Check", - "permlevel": 0 + "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "in_dialog", "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "In Dialog", + "length": 0, + "no_copy": 0, "oldfieldname": "in_dialog", "oldfieldtype": "Check", - "permlevel": 0 + "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "read_only_onload", "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Show Print First", + "length": 0, + "no_copy": 0, "oldfieldname": "read_only_onload", "oldfieldtype": "Check", - "permlevel": 0 + "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "max_attachments", "fieldtype": "Int", "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Max Attachments", + "length": 0, + "no_copy": 0, "oldfieldname": "max_attachments", "oldfieldtype": "Int", - "permlevel": 0 + "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "cb31", "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Hide Actions", - "permlevel": 0 + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "hide_heading", "fieldtype": "Check", "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Hide Heading", + "length": 0, + "no_copy": 0, "oldfieldname": "hide_heading", "oldfieldtype": "Check", "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, "reqd": 0, - "search_index": 0 + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "hide_toolbar", "fieldtype": "Check", "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Hide Toolbar", + "length": 0, + "no_copy": 0, "oldfieldname": "hide_toolbar", "oldfieldtype": "Check", "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, "reqd": 0, - "search_index": 0 + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "allow_copy", "fieldtype": "Check", "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Hide Copy", + "length": 0, + "no_copy": 0, "oldfieldname": "allow_copy", "oldfieldtype": "Check", "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, "reqd": 0, - "search_index": 0 + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "default_print_format", "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Default Print Format", - "permlevel": 0 + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 } ], "hide_heading": 0, "hide_toolbar": 0, "icon": "icon-bolt", "idx": 1, + "in_create": 0, + "in_dialog": 0, + "is_submittable": 0, "issingle": 0, "istable": 0, - "modified": "2015-03-03 10:40:45.768116", + "max_attachments": 0, + "modified": "2015-11-16 06:29:45.648699", "modified_by": "Administrator", "module": "Core", "name": "DocType", "owner": "Administrator", "permissions": [ { + "amend": 0, + "apply_user_permissions": 0, "cancel": 0, "create": 1, "delete": 0, "email": 1, + "export": 0, + "if_owner": 0, + "import": 0, "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "System Manager", + "set_user_permissions": 0, + "share": 0, "submit": 0, "write": 1 }, { + "amend": 0, + "apply_user_permissions": 0, "cancel": 0, "create": 1, "delete": 1, "email": 1, + "export": 0, + "if_owner": 0, + "import": 0, "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "Administrator", + "set_user_permissions": 0, "share": 1, "submit": 0, "write": 1 } ], "read_only": 0, + "read_only_onload": 0, "search_fields": "module", "sort_field": "name", "sort_order": "ASC" diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index 74f98aeba7..a3ff7dd261 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals +import re import frappe from frappe import _ @@ -12,13 +13,15 @@ from frappe.model.document import Document from frappe.custom.doctype.property_setter.property_setter import make_property_setter from frappe.desk.notifications import delete_notification_count_for from frappe.modules import make_boilerplate +from frappe.model.db_schema import validate_column_name + +class InvalidFieldNameError(frappe.ValidationError): pass form_grid_templates = { "fields": "templates/form_grid/fields.html" } class DocType(Document): - __doclink__ = "https://frappe.io/docs/models/core/doctype" def get_feed(self): return self.name @@ -30,14 +33,13 @@ class DocType(Document): - Check fieldnames (duplication etc) - Clear permission table for child tables - Add `amended_from` and `ameneded_by` if Amendable""" - if not frappe.conf.get("developer_mode") and not self.custom: - frappe.throw(_("Not in Developer Mode! Set in site_config.json or make 'Custom' DocType.")) + self.check_developer_mode() for c in [".", "/", "#", "&", "=", ":", "'", '"']: if c in self.name: frappe.throw(_("{0} not allowed in name").format(c)) self.validate_series() self.scrub_field_names() - self.validate_title_field() + self.validate_document_type() validate_fields(self) if self.istable: @@ -48,6 +50,20 @@ class DocType(Document): self.make_amendable() + def check_developer_mode(self): + """Throw exception if not developer mode or via patch""" + if frappe.flags.in_patch: + return + + if not frappe.conf.get("developer_mode") and not self.custom: + frappe.throw(_("Not in Developer Mode! Set in site_config.json or make 'Custom' DocType.")) + + def validate_document_type(self): + if self.document_type=="Transaction": + self.document_type = "Document" + if self.document_type=="Master": + self.document_type = "Setup" + def change_modified_of_parent(self): """Change the timestamp of parent DocType if the current one is a child to clear caches.""" if frappe.flags.in_import: @@ -71,12 +87,8 @@ class DocType(Document): else: d.fieldname = d.fieldtype.lower().replace(" ","_") + "_" + str(d.idx) - - def validate_title_field(self): - """Throw exception if `title_field` is not a valid field.""" - if self.title_field and \ - self.title_field not in [d.fieldname for d in self.get("fields")]: - frappe.throw(_("Title field must be a valid fieldname")) + # fieldnames should be lowercase + d.fieldname = d.fieldname.lower() def validate_series(self, autoname=None, name=None): """Validate if `autoname` property is correctly set.""" @@ -88,7 +100,7 @@ class DocType(Document): if autoname and (not autoname.startswith('field:')) \ and (not autoname.startswith('eval:')) \ - and (not autoname in ('Prompt', 'hash')) \ + and (not autoname.lower() in ('prompt', 'hash')) \ and (not autoname.startswith('naming_series:')): prefix = autoname.split('.')[0] @@ -127,6 +139,11 @@ class DocType(Document): def before_rename(self, old, new, merge=False): """Throw exception if merge. DocTypes cannot be merged.""" + if not self.custom and frappe.session.user != "Administrator": + frappe.throw(_("DocType can only be renamed by Administrator")) + + self.check_developer_mode() + if merge: frappe.throw(_("DocType can not be merged")) @@ -201,7 +218,7 @@ class DocType(Document): return max_idx and max_idx[0][0] or 0 def validate_fields_for_doctype(doctype): - validate_fields(frappe.get_meta(doctype)) + validate_fields(frappe.get_meta(doctype, cached=False)) # this is separate because it is also called via custom field def validate_fields(meta): @@ -217,13 +234,13 @@ def validate_fields(meta): 9. Precision is set in numeric fields and is between 1 & 6. 10. Fold is not at the end (if set). 11. `search_fields` are valid. + 12. `title_field` and title field pattern are valid. + 13. `unique` check is only valid for Data, Link and Read Only fieldtypes. + 14. `unique` cannot be checked if there exist non-unique values. :param meta: `frappe.model.meta.Meta` object to check.""" def check_illegal_characters(fieldname): - for c in ['.', ',', ' ', '-', '&', '%', '=', '"', "'", '*', '$', - '(', ')', '[', ']', '/']: - if c in fieldname: - frappe.throw(_("{0} not allowed in fieldname {1}").format(c, fieldname)) + validate_column_name(fieldname) def check_unique_fieldname(fieldname): duplicates = filter(None, map(lambda df: df.fieldname==fieldname and str(df.idx) or None, fields)) @@ -263,8 +280,8 @@ def validate_fields(meta): def check_dynamic_link_options(d): if d.fieldtype=="Dynamic Link": doctype_pointer = filter(lambda df: df.fieldname==d.options, fields) - if not doctype_pointer or (doctype_pointer[0].fieldtype!="Link") \ - or (doctype_pointer[0].options!="DocType"): + if not doctype_pointer or (doctype_pointer[0].fieldtype not in ("Link", "Select")) \ + or (doctype_pointer[0].fieldtype=="Link" and doctype_pointer[0].options!="DocType"): frappe.throw(_("Options 'Dynamic Link' type of field must point to another Link Field with options as 'DocType'")) def check_illegal_default(d): @@ -278,8 +295,17 @@ def validate_fields(meta): frappe.throw(_("Precision should be between 1 and 6")) def check_unique_and_text(d): - if getattr(d, "unique", False) and d.fieldtype in ("Text", "Long Text", "Small Text", "Code", "Text Editor"): - frappe.throw(_("Fieldtype {0} for {1} cannot be unique").format(d.fieldtype, d.label)) + if getattr(d, "unique", False): + if d.fieldtype not in ("Data", "Link", "Read Only"): + frappe.throw(_("Fieldtype {0} for {1} cannot be unique").format(d.fieldtype, d.label)) + + if not d.get("__islocal"): + has_non_unique_values = frappe.db.sql("""select `{fieldname}`, count(*) + from `tab{doctype}` group by `{fieldname}` having count(*) > 1 limit 1""".format( + doctype=d.parent, fieldname=d.fieldname)) + + if has_non_unique_values and has_non_unique_values[0][0]: + frappe.throw(_("Field '{0}' cannot be set as Unique as it has non-unique values").format(d.label)) if d.search_index and d.fieldtype in ("Text", "Long Text", "Small Text", "Code", "Text Editor"): frappe.throw(_("Fieldtype {0} for {1} cannot be indexed").format(d.fieldtype, d.label)) @@ -299,6 +325,7 @@ def validate_fields(meta): frappe.throw(_("Fold can not be at the end of the form")) def check_search_fields(meta): + """Throw exception if `search_fields` don't contain valid fields.""" if not meta.search_fields: return @@ -308,13 +335,40 @@ def validate_fields(meta): if fieldname not in fieldname_list: frappe.throw(_("Search Fields should contain valid fieldnames")) + def check_title_field(meta): + """Throw exception if `title_field` isn't a valid fieldname.""" + if not meta.title_field: + return + fieldname_list = [d.fieldname for d in fields] + + if meta.title_field not in fieldname_list: + frappe.throw(_("Title field must be a valid fieldname"), InvalidFieldNameError) + + def _validate_title_field_pattern(pattern): + if not pattern: + return + + for fieldname in re.findall("{(.*?)}", pattern, re.UNICODE): + if fieldname.startswith("{"): + # edge case when double curlies are used for escape + continue + + if fieldname not in fieldname_list: + frappe.throw(_("{{{0}}} is not a valid fieldname pattern. It should be {{field_name}}.").format(fieldname), + InvalidFieldNameError) + + df = meta.get("fields", filters={"fieldname": meta.title_field})[0] + if df: + _validate_title_field_pattern(df.options) + _validate_title_field_pattern(df.default) fields = meta.get("fields") for d in fields: if not d.permlevel: d.permlevel = 0 if not d.fieldname: frappe.throw(_("Fieldname is required in row {0}").format(d.idx)) + d.fieldname = d.fieldname.lower() check_illegal_characters(d.fieldname) check_unique_fieldname(d.fieldname) check_illegal_mandatory(d) @@ -327,6 +381,7 @@ def validate_fields(meta): check_fold(fields) check_search_fields(meta) + check_title_field(meta) def validate_permissions_for_doctype(doctype, for_remove=False): """Validates if permissions are set correctly.""" @@ -362,14 +417,21 @@ def validate_permissions(doctype, for_remove=False): def check_double(d): has_similar = False + similar_because_of = "" for p in permissions: - if (p.role==d.role and p.permlevel==d.permlevel - and p.apply_user_permissions==d.apply_user_permissions and p!=d): - has_similar = True - break + if p.role==d.role and p.permlevel==d.permlevel and p!=d: + if p.apply_user_permissions==d.apply_user_permissions: + has_similar = True + similar_because_of = _("Apply User Permissions") + break + elif p.if_owner==d.if_owner: + similar_because_of = _("If Owner") + has_similar = True + break if has_similar: - frappe.throw(_("{0}: Only one rule allowed with the same Role, Level and Apply User Permissions").format(get_txt(d))) + frappe.throw(_("{0}: Only one rule allowed with the same Role, Level and {1}")\ + .format(get_txt(d), similar_because_of)) def check_level_zero_is_set(d): if cint(d.permlevel) > 0 and d.role != 'All': @@ -466,4 +528,3 @@ def init_list(doctype): doc = frappe.get_meta(doctype) make_boilerplate("controller_list.js", doc) make_boilerplate("controller_list.html", doc) - diff --git a/frappe/core/doctype/doctype/test_doctype.py b/frappe/core/doctype/doctype/test_doctype.py new file mode 100644 index 0000000000..2419f0c7d1 --- /dev/null +++ b/frappe/core/doctype/doctype/test_doctype.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +import frappe +import unittest + +# test_records = frappe.get_test_records('DocType') + +class TestDocType(unittest.TestCase): + pass diff --git a/frappe/translations/en.csv b/frappe/core/doctype/error_snapshot/__init__.py similarity index 100% rename from frappe/translations/en.csv rename to frappe/core/doctype/error_snapshot/__init__.py diff --git a/frappe/core/doctype/error_snapshot/error_snapshot.js b/frappe/core/doctype/error_snapshot/error_snapshot.js new file mode 100644 index 0000000000..7e9e1313f1 --- /dev/null +++ b/frappe/core/doctype/error_snapshot/error_snapshot.js @@ -0,0 +1,16 @@ +frappe.ui.form.on("Error Snapshot", "load", function(frm){ + frm.set_read_only(true); +}); + +frappe.ui.form.on("Error Snapshot", "refresh", function(frm){ + frm.set_df_property("view", "options", frappe.render_template("error_snapshot", {"doc": frm.doc})); + + if (frm.doc.relapses) { + frm.add_custom_button(__('Show Relapses'), function() { + frappe.route_options = { + parent_error_snapshot: frm.doc.name + }; + frappe.set_route("List", "Error Snapshot"); + }); + } +}); diff --git a/frappe/core/doctype/error_snapshot/error_snapshot.json b/frappe/core/doctype/error_snapshot/error_snapshot.json new file mode 100644 index 0000000000..b9dd81387b --- /dev/null +++ b/frappe/core/doctype/error_snapshot/error_snapshot.json @@ -0,0 +1,344 @@ +{ + "allow_copy": 0, + "allow_import": 0, + "allow_rename": 0, + "creation": "2015-11-28 00:57:39.766888", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "System", + "fields": [ + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "view", + "fieldtype": "HTML", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Snapshot View", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "seen", + "fieldtype": "Check", + "hidden": 1, + "ignore_user_permissions": 0, + "in_filter": 1, + "in_list_view": 0, + "label": "Seen", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "evalue", + "fieldtype": "Small Text", + "hidden": 1, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 1, + "label": "Friendly Title", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "timestamp", + "fieldtype": "Datetime", + "hidden": 1, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Timestamp", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "default": "1", + "fieldname": "relapses", + "fieldtype": "Int", + "hidden": 1, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 1, + "label": "Relapses", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "etype", + "fieldtype": "Data", + "hidden": 1, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Exception Type", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "traceback", + "fieldtype": "Small Text", + "hidden": 1, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Traceback", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "parent_error_snapshot", + "fieldtype": "Data", + "hidden": 1, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Parent Error Snapshot", + "length": 0, + "no_copy": 0, + "options": "", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "pyver", + "fieldtype": "Small Text", + "hidden": 1, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Pyver", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "exception", + "fieldtype": "Small Text", + "hidden": 1, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Exception", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "locals", + "fieldtype": "Text", + "hidden": 1, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Locals", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "frames", + "fieldtype": "Text", + "hidden": 1, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Frames", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + } + ], + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 0, + "in_create": 1, + "in_dialog": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 0, + "max_attachments": 0, + "modified": "2015-12-28 00:44:30.751680", + "modified_by": "Administrator", + "module": "Core", + "name": "Error Snapshot", + "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": "Administrator", + "set_user_permissions": 0, + "share": 1, + "submit": 0, + "write": 1 + } + ], + "read_only": 0, + "read_only_onload": 0, + "sort_field": "timestamp", + "sort_order": "DESC", + "title_field": "evalue" +} \ No newline at end of file diff --git a/frappe/core/doctype/error_snapshot/error_snapshot.py b/frappe/core/doctype/error_snapshot/error_snapshot.py new file mode 100644 index 0000000000..1b9bc072bf --- /dev/null +++ b/frappe/core/doctype/error_snapshot/error_snapshot.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe.model.document import Document + +class ErrorSnapshot(Document): + def onload(self): + if not self.parent_error_snapshot: + self.db_set('seen', True, update_modified=False) + + for relapsed in frappe.get_all("Error Snapshot", filters={"parent_error_snapshot": self.name}): + frappe.db.set_value("Error Snapshot", relapsed.name, "seen", True, update_modified=False) + + frappe.local.flags.commit = True + + def validate(self): + parent = frappe.get_all("Error Snapshot", + filters={"evalue": self.evalue, "parent_error_snapshot": ""}, + fields=["name", "relapses", "seen"], limit_page_length=1) + + if parent: + parent = parent[0] + self.update({"parent_error_snapshot": parent['name']}) + frappe.db.set_value('Error Snapshot', parent['name'], 'relapses', parent["relapses"] + 1) + if parent["seen"]: + frappe.db.set_value("Error Snapshot", parent["name"], "seen", False) diff --git a/frappe/core/doctype/error_snapshot/error_snapshot_list.js b/frappe/core/doctype/error_snapshot/error_snapshot_list.js new file mode 100644 index 0000000000..dfaeb7ab85 --- /dev/null +++ b/frappe/core/doctype/error_snapshot/error_snapshot_list.js @@ -0,0 +1,14 @@ +frappe.listview_settings["Error Snapshot"] = { + add_fields: ["parent_error_snapshot", "relapses", "seen"], + filters:[ + ["parent_error_snapshot","=",null], + ["seen", "=", false] + ], + get_indicator: function(doc){ + if (doc.parent_error_snapshot && doc.parent_error_snapshot.length){ + return [__("Relapsed"), !doc.seen ? "orange" : "blue", "parent_error_snapshot,!=,"]; + } else { + return [__("First Level"), !doc.seen ? "red" : "green", "parent_error_snapshot,=,"]; + } + } +} diff --git a/frappe/core/doctype/error_snapshot/test_error_snapshot.py b/frappe/core/doctype/error_snapshot/test_error_snapshot.py new file mode 100644 index 0000000000..b6438eae1d --- /dev/null +++ b/frappe/core/doctype/error_snapshot/test_error_snapshot.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +import frappe +import unittest + +# test_records = frappe.get_test_records('Error Snapshot') + +class TestErrorSnapshot(unittest.TestCase): + pass diff --git a/frappe/core/doctype/file/__init__.py b/frappe/core/doctype/file/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/core/doctype/file/file.js b/frappe/core/doctype/file/file.js new file mode 100644 index 0000000000..bdcd6cf119 --- /dev/null +++ b/frappe/core/doctype/file/file.js @@ -0,0 +1,25 @@ +frappe.ui.form.on("File", "refresh", function(frm) { + if(!frm.doc.is_folder) { + frm.add_custom_button(__('Download'), function() { + var file_url = frm.doc.file_url; + if (frm.doc.file_name) { + file_url = file_url.replace(/#/g, '%23'); + } + window.open(file_url); + }, "icon-download"); + } + + var wrapper = frm.get_field("preview_html").$wrapper; + var is_viewable = frappe.utils.is_image_file(frm.doc.file_url); + + frm.toggle_display("preview", is_viewable); + frm.toggle_display("preview_html", is_viewable); + + if(is_viewable){ + wrapper.html('
    \ + \ +
    '); + } else { + wrapper.empty(); + } +}); diff --git a/frappe/core/doctype/file/file.json b/frappe/core/doctype/file/file.json new file mode 100644 index 0000000000..7c7b2a0a2a --- /dev/null +++ b/frappe/core/doctype/file/file.json @@ -0,0 +1,575 @@ +{ + "allow_copy": 0, + "allow_import": 1, + "allow_rename": 0, + "autoname": "", + "creation": "2012-12-12 11:19:22", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "fields": [ + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "file_name", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "File Name", + "length": 0, + "no_copy": 0, + "oldfieldname": "file_name", + "oldfieldtype": "Data", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "depends_on": "eval:!doc.is_folder", + "fieldname": "is_private", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Is Private", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 1, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "preview", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Preview", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "preview_html", + "fieldtype": "HTML", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Preview HTML", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "section_break_5", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "depends_on": "", + "fieldname": "is_home_folder", + "fieldtype": "Check", + "hidden": 1, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Is Home Folder", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "is_attachments_folder", + "fieldtype": "Check", + "hidden": 1, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Is Attachments Folder", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "file_size", + "fieldtype": "Int", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 1, + "label": "File Size", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "column_break_5", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "depends_on": "eval:!doc.is_folder", + "fieldname": "file_url", + "fieldtype": "Small Text", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "File URL", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "thumbnail_url", + "fieldtype": "Small Text", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Thumbnail URL", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "folder", + "fieldtype": "Link", + "hidden": 1, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Folder", + "length": 0, + "no_copy": 0, + "options": "File", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "is_folder", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Is Folder", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "depends_on": "eval:!doc.is_folder", + "fieldname": "section_break_8", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "attached_to_doctype", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Attached To DocType", + "length": 0, + "no_copy": 0, + "options": "DocType", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 1, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "column_break_10", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "attached_to_name", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Attached To Name", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 1, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "content_hash", + "fieldtype": "Data", + "hidden": 1, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Content Hash", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 1, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "lft", + "fieldtype": "Int", + "hidden": 1, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "lft", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "rgt", + "fieldtype": "Int", + "hidden": 1, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "rgt", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "old_parent", + "fieldtype": "Data", + "hidden": 1, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "old_parent", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + } + ], + "hide_heading": 0, + "hide_toolbar": 0, + "icon": "icon-file", + "idx": 1, + "in_create": 0, + "in_dialog": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 0, + "max_attachments": 0, + "menu_index": 0, + "modified": "2015-12-08 05:03:48.767257", + "modified_by": "Administrator", + "module": "Core", + "name": "File", + "owner": "Administrator", + "permissions": [ + { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "if_owner": 0, + "import": 1, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "set_user_permissions": 0, + "share": 1, + "submit": 0, + "write": 1 + }, + { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "if_owner": 1, + "import": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 1, + "role": "All", + "set_user_permissions": 0, + "share": 1, + "submit": 0, + "write": 1 + } + ], + "read_only": 0, + "read_only_onload": 0, + "title_field": "file_name" +} \ No newline at end of file diff --git a/frappe/core/doctype/file/file.py b/frappe/core/doctype/file/file.py new file mode 100644 index 0000000000..72bea79686 --- /dev/null +++ b/frappe/core/doctype/file/file.py @@ -0,0 +1,351 @@ +# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# MIT License. See license.txt + +from __future__ import unicode_literals +""" +record of files + +naming for same name files: file.gif, file-1.gif, file-2.gif etc +""" + +import frappe, frappe.utils +from frappe.utils.file_manager import delete_file_data_content, get_content_hash, get_random_filename +from frappe import _ + +from frappe.utils.nestedset import NestedSet +from frappe.utils import strip +import json +import urllib +from PIL import Image, ImageOps +import os +import requests +import requests.exceptions +import StringIO +import mimetypes, imghdr +from frappe.utils import get_files_path + +class FolderNotEmpty(frappe.ValidationError): pass + +exclude_from_linked_with = True + +class File(NestedSet): + nsm_parent_field = 'folder' + no_feed_on_delete = True + + def before_insert(self): + frappe.local.rollback_observers.append(self) + self.set_folder_name() + self.set_name() + + def get_name_based_on_parent_folder(self): + path = get_breadcrumbs(self.folder) + folder_name = frappe.get_value("File", self.folder, "file_name") + return "/".join([d.file_name for d in path] + [folder_name, self.file_name]) + + def set_name(self): + """Set name for folder""" + if self.is_folder: + if self.folder: + self.name = self.get_name_based_on_parent_folder() + else: + # home + self.name = self.file_name + else: + self.name = frappe.generate_hash("", 10) + + def after_insert(self): + self.update_parent_folder_size() + + def after_rename(self, olddn, newdn, merge=False): + for successor in self.get_successor(): + setup_folder_path(successor, self.name) + + def get_successor(self): + return frappe.db.sql_list("select name from tabFile where folder='%s'"%self.name) or [] + + def validate(self): + if self.is_new(): + self.validate_duplicate_entry() + self.validate_folder() + + if not self.flags.ignore_file_validate: + self.validate_file() + self.generate_content_hash() + + self.set_folder_size() + + def set_folder_size(self): + """Set folder size if folder""" + if self.is_folder and not self.is_new(): + self.file_size = self.get_folder_size() + frappe.db.set_value("File", self.name, "file_size", self.file_size) + + for folder in self.get_ancestors(): + frappe.db.set_value("File", folder, "file_size", self.get_folder_size(folder)) + + def get_folder_size(self, folder=None): + """Returns folder size for current folder""" + if not folder: + folder = self.name + file_size = frappe.db.sql("""select sum(ifnull(file_size,0)) + from tabFile where folder=%s """, (folder))[0][0] + + return file_size + + def update_parent_folder_size(self): + """Update size of parent folder""" + if self.folder and not self.is_folder: # it not home + frappe.get_doc("File", self.folder).save(ignore_permissions=True) + + def set_folder_name(self): + """Make parent folders if not exists based on reference doctype and name""" + if self.attached_to_doctype and not self.folder: + self.folder = frappe.db.get_value("File", {"is_attachments_folder": 1}) + + def validate_folder(self): + if not self.is_home_folder and not self.folder and \ + not self.flags.ignore_folder_validate: + frappe.throw(_("Folder is mandatory")) + + def validate_file(self): + """Validates existence of public file + TODO: validate for private file + """ + if (self.file_url or "").startswith("/files/"): + if not self.file_name: + self.file_name = self.file_url.split("/files/")[-1] + + if not os.path.exists(get_files_path(self.file_name.lstrip("/"))): + frappe.throw(_("File {0} does not exist").format(self.file_url), IOError) + + def validate_duplicate_entry(self): + if not self.flags.ignore_duplicate_entry_error and not self.is_folder: + # check duplicate name + + # check duplicate assignement + n_records = frappe.db.sql("""select name from `tabFile` + where content_hash=%s + and name!=%s + and attached_to_doctype=%s + and attached_to_name=%s""", (self.content_hash, self.name, self.attached_to_doctype, + self.attached_to_name)) + if len(n_records) > 0: + self.duplicate_entry = n_records[0][0] + frappe.throw(frappe._("Same file has already been attached to the record"), frappe.DuplicateEntryError) + + def generate_content_hash(self): + if self.content_hash or not self.file_url: + return + + if self.file_url.startswith("/files/"): + try: + with open(get_files_path(self.file_name.lstrip("/")), "r") as f: + self.content_hash = get_content_hash(f.read()) + except IOError: + frappe.msgprint(_("File {0} does not exist").format(self.file_url)) + raise + + def on_trash(self): + if self.is_home_folder or self.is_attachments_folder: + frappe.throw(_("Cannot delete Home and Attachments folders")) + self.check_folder_is_empty() + self.check_reference_doc_permission() + super(File, self).on_trash() + self.delete_file() + + def make_thumbnail(self): + if self.file_url: + if self.file_url.startswith("/files"): + try: + image, filename, extn = get_local_image(self.file_url) + except IOError: + return + + else: + try: + image, filename, extn = get_web_image(self.file_url) + except (requests.exceptions.HTTPError, requests.exceptions.SSLError, IOError): + return + + thumbnail = ImageOps.fit( + image, + (300, 300), + Image.ANTIALIAS + ) + + thumbnail_url = filename + "_small." + extn + + path = os.path.abspath(frappe.get_site_path("public", thumbnail_url.lstrip("/"))) + + try: + thumbnail.save(path) + self.db_set("thumbnail_url", thumbnail_url) + except IOError: + frappe.msgprint("Unable to write file format for {0}".format(path)) + return + + return thumbnail_url + + def after_delete(self): + self.update_parent_folder_size() + + def check_folder_is_empty(self): + """Throw exception if folder is not empty""" + files = frappe.get_all("File", filters={"folder": self.name}, fields=("name", "file_name")) + + if self.is_folder and files: + frappe.throw(_("Folder {0} is not empty").format(self.name), FolderNotEmpty) + + def check_reference_doc_permission(self): + """Check if permission exists for reference document""" + if self.attached_to_name: + # check persmission + try: + if not self.flags.ignore_permissions and \ + not frappe.has_permission(self.attached_to_doctype, + "write", self.attached_to_name): + frappe.throw(frappe._("No permission to write / remove."), + frappe.PermissionError) + except frappe.DoesNotExistError: + pass + + def delete_file(self): + """If file not attached to any other record, delete it""" + if self.file_name and self.content_hash and (not frappe.db.count("File", + {"content_hash": self.content_hash, "name": ["!=", self.name]})): + delete_file_data_content(self) + + elif self.file_url: + delete_file_data_content(self, only_thumbnail=True) + + def on_rollback(self): + self.flags.on_rollback = True + self.on_trash() + +def on_doctype_update(): + frappe.db.add_index("File", ["attached_to_doctype", "attached_to_name"]) + +def make_home_folder(): + home = frappe.get_doc({ + "doctype": "File", + "is_folder": 1, + "is_home_folder": 1, + "file_name": _("Home") + }).insert() + + frappe.get_doc({ + "doctype": "File", + "folder": home.name, + "is_folder": 1, + "is_attachments_folder": 1, + "file_name": _("Attachments") + }).insert() + +@frappe.whitelist() +def get_breadcrumbs(folder): + """returns name, file_name of parent folder""" + lft, rgt = frappe.db.get_value("File", folder, ["lft", "rgt"]) + return frappe.db.sql("""select name, file_name from tabFile + where lft < %s and rgt > %s order by lft asc""", (lft, rgt), as_dict=1) + +@frappe.whitelist() +def create_new_folder(file_name, folder): + """ create new folder under current parent folder """ + file = frappe.new_doc("File") + file.file_name = file_name + file.is_folder = 1 + file.folder = folder + file.insert() + +@frappe.whitelist() +def move_file(file_list, new_parent, old_parent): + if isinstance(file_list, basestring): + file_list = json.loads(file_list) + + for file_obj in file_list: + setup_folder_path(file_obj.get("name"), new_parent) + + # recalculate sizes + frappe.get_doc("File", old_parent).save() + frappe.get_doc("File", new_parent).save() + +def setup_folder_path(filename, new_parent): + file = frappe.get_doc("File", filename) + file.folder = new_parent + file.save() + + if file.is_folder: + frappe.rename_doc("File", file.name, file.get_name_based_on_parent_folder(), ignore_permissions=True) + +def get_extension(filename, extn, content): + mimetype = None + if extn: + mimetype = mimetypes.guess_type(filename + "." + extn)[0] + + if mimetype is None or not mimetype.startswith("image/") and content: + # detect file extension by reading image header properties + extn = imghdr.what(filename + "." + (extn or ""), h=content) + + return extn + +def get_local_image(file_url): + file_path = frappe.get_site_path("public", file_url.lstrip("/")) + + try: + image = Image.open(file_path) + except IOError: + frappe.msgprint("Unable to read file format for {0}".format(file_url)) + raise + + content = None + + try: + filename, extn = file_url.rsplit(".", 1) + except ValueError: + # no extn + with open(file_path, "r") as f: + content = f.read() + + filename = file_url + extn = None + + extn = get_extension(filename, extn, content) + + return image, filename, extn + +def get_web_image(file_url): + # downlaod + file_url = frappe.utils.get_url(file_url) + r = requests.get(file_url, stream=True) + try: + r.raise_for_status() + except requests.exceptions.HTTPError, e: + if "404" in e.args[0]: + frappe.msgprint(_("File '{0}' not found").format(file_url)) + else: + frappe.msgprint("Unable to read file format for {0}".format(file_url)) + raise + + image = Image.open(StringIO.StringIO(r.content)) + + try: + filename, extn = file_url.rsplit("/", 1)[1].rsplit(".", 1) + except ValueError: + # the case when the file url doesn't have filename or extension + # but is fetched due to a query string. example: https://encrypted-tbn3.gstatic.com/images?q=something + filename = get_random_filename() + extn = None + + extn = get_extension(filename, extn, r.content) + filename = "/files/" + strip(urllib.unquote(filename)) + + return image, filename, extn + +def check_file_permission(file_url): + for file in frappe.get_all("File", filters={"file_url": file_url, "is_private": 1}, fields=["name", "attached_to_doctype", "attached_to_name"]): + + if (frappe.has_permission("File", ptype="read", doc=file.name) + or frappe.has_permission(file.attached_to_doctype, ptype="read", doc=file.attached_to_name)): + return True + + raise frappe.PermissionError diff --git a/frappe/core/doctype/file/file_list.js b/frappe/core/doctype/file/file_list.js new file mode 100644 index 0000000000..536a28b9a2 --- /dev/null +++ b/frappe/core/doctype/file/file_list.js @@ -0,0 +1,216 @@ +frappe.provide("frappe.ui"); + +frappe.listview_settings['File'] = { + hide_name_column: true, + use_route: true, + add_fields: ["is_folder", "file_name", "file_url", "folder", "is_private"], + formatters: { + file_size: function(value) { + // formatter for file size + if(value > 1048576) { + value = flt(flt(value) / 1048576, 1) + "M"; + } else if (value > 1024) { + value = flt(flt(value) / 1024, 1) + "K"; + } + return value; + } + }, + prepare_data: function(data) { + // set image icons + var icon = "" + + if(data.is_folder) { + icon += ' '; + } else if(frappe.utils.is_image_file(data.file_name)) { + icon += ' '; + } else { + icon += ' ' + } + + data._title = icon + (data.file_name ? data.file_name : data.file_url) + + if (data.is_private) { + data._title += ' ' + } + }, + onload: function(doclist) { + doclist.filter_area = doclist.wrapper.find(".show_filters"); + + doclist.breadcrumb = $('') + .insertBefore(doclist.filter_area); + + doclist.listview.settings.setup_menu(doclist); + doclist.listview.settings.setup_dragdrop(doclist); + + doclist.$page.on("click", ".list-delete", function(event) { + doclist.listview.settings.add_menu_item_copy(doclist); + }) + }, + list_view_doc:function(doclist){ + $(doclist.wrapper).on("click", 'button[list_view_doc="'+doclist.doctype+'"]', function(){ + dialog = frappe.ui.get_upload_dialog({ + "args": { + "folder": doclist.current_folder, + "from_form": 1 + }, + callback: function() { + doclist.refresh(); + } + }); + }); + }, + setup_menu: function(doclist) { + doclist.page.add_menu_item(__("New Folder"), function() { + var d = frappe.prompt(__("Name"), function(values) { + if((values.value.indexOf("/") > -1)){ + frappe.throw("Folder name should not include / !!!"); + return; + } + var data = { + "file_name": values.value, + "folder": doclist.current_folder + }; + frappe.call({ + method: "frappe.core.doctype.file.file.create_new_folder", + args: data, + callback: function(r) { } + }) + }, __('Enter folder name'), __("Create")); + }); + + doclist.page.add_menu_item(__("Edit Folder"), function() { + frappe.set_route("Form", "File", doclist.current_folder); + }); + }, + setup_dragdrop: function(doclist) { + $(doclist.$page).on('dragenter dragover', false) + .on('drop', function (e) { + var dataTransfer = e.originalEvent.dataTransfer; + if (!(dataTransfer && dataTransfer.files && dataTransfer.files.length > 0)) { + return; + } + e.stopPropagation(); + e.preventDefault(); + frappe.upload.upload_file(dataTransfer.files[0], { + "folder": doclist.current_folder, + "from_form": 1 + }, { + confirm_is_private: 1 + }); + }); + }, + add_menu_item_copy: function(doclist){ + if (!doclist.copy) { + var copy_menu = doclist.page.add_menu_item(__("Copy"), function() { + if(doclist.$page.find(".list-delete:checked").length){ + doclist.selected_files = doclist.get_checked_items(); + doclist.old_parent = doclist.current_folder; + doclist.listview.settings.add_menu_item_paste(doclist); + } + else{ + frappe.throw("Please select file to copy"); + } + }) + doclist.copy = true; + } + }, + add_menu_item_paste:function(doclist){ + var paste_menu = doclist.page.add_menu_item(__("Paste"), function(){ + frappe.call({ + method:"frappe.core.doctype.file.file.move_file", + args: { + "file_list": doclist.selected_files, + "new_parent": doclist.current_folder, + "old_parent": doclist.old_parent + }, + callback:function(r){ + doclist.paste = false; + frappe.msgprint(__(r.message)); + doclist.selected_files = []; + $(paste_menu).remove(); + } + }) + }) + }, + before_run: function(doclist) { + var name_filter = doclist.filter_list.get_filter("file_name"); + if(name_filter) { + doclist.filter_area.removeClass("hide"); + doclist.breadcrumb.addClass("hide"); + } else { + doclist.filter_area.addClass("hide"); + doclist.breadcrumb.removeClass("hide"); + } + }, + refresh: function(doclist) { + // set folder before querying + var name_filter = doclist.filter_list.get_filter("file_name"); + + var folder_filter = doclist.filter_list.get_filter("folder"); + if(folder_filter) { + folder_filter.remove(true); + } + + if(name_filter) return; + + var route = frappe.get_route(); + if(route[2]) { + doclist.current_folder = route.slice(2).join("/"); + doclist.current_folder_name = route.slice(-1)[0]; + } + + if(!doclist.current_folder) { + doclist.current_folder = frappe.boot.home_folder; + doclist.current_folder_name = __("Home"); + } + + doclist.filter_list.add_filter("File", "folder", "=", doclist.current_folder, true); + doclist.dirty = true; + doclist.fresh = false; + + doclist.page.set_title(doclist.current_folder_name); + frappe.utils.set_title(doclist.current_folder_name); + }, + set_primary_action:function(doclist){ + doclist.page.clear_primary_action(); + doclist.page.set_primary_action(__("New"), function() { + dialog = frappe.ui.get_upload_dialog({ + "args": { + "folder": doclist.current_folder, + "from_form": 1 + }, + callback: function() { + doclist.refresh(); + } + }); + }, "octicon octicon-plus"); + }, + post_render_item: function(list, row, data) { + if(data.is_folder) { + $(row).find(".list-id").attr("href", "#List/File/" + data.name); + } + }, + set_file_route: function(name) { + frappe.set_route(["List", "File"].concat(decodeURIComponent(name).split("/"))); + }, + post_render: function(doclist) { + frappe.call({ + method: "frappe.core.doctype.file.file.get_breadcrumbs", + args: { + folder: doclist.current_folder + }, + callback: function(r) { + doclist.breadcrumb.empty(); + if(r.message && r.message.length) { + $.each(r.message, function(i, folder) { + $('
  • ' + + folder.file_name+'
  • ') + .appendTo(doclist.breadcrumb); + }); + } + $('
  • '+ doclist.current_folder_name+'
  • ') + .appendTo(doclist.breadcrumb); + } + }); + } +} diff --git a/frappe/core/doctype/file/test_file.py b/frappe/core/doctype/file/test_file.py new file mode 100644 index 0000000000..60ff647894 --- /dev/null +++ b/frappe/core/doctype/file/test_file.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +import frappe +import unittest +from frappe.utils.file_manager import save_file, get_files_path +from frappe import _ +from frappe.core.doctype.file.file import move_file +# test_records = frappe.get_test_records('File') + +class TestFile(unittest.TestCase): + def setUp(self): + self.delete_test_data() + self.upload_file() + + def tearDown(self): + try: + frappe.get_doc("File", {"file_name": "file_copy.txt"}).delete() + except frappe.DoesNotExistError: + pass + + def delete_test_data(self): + for f in frappe.db.sql('''select name, file_name from tabFile where + is_home_folder = 0 and is_attachments_folder = 0 order by rgt-lft asc'''): + frappe.delete_doc("File", f[0]) + + def upload_file(self): + self.saved_file = save_file('file_copy.txt', "Testing file copy example.",\ + "", "", self.get_folder("Test Folder 1", "Home").name) + self.saved_filename = get_files_path(self.saved_file.file_name) + + def get_folder(self, folder_name, parent_folder="Home"): + return frappe.get_doc({ + "doctype": "File", + "file_name": _(folder_name), + "is_folder": 1, + "folder": _(parent_folder) + }).insert() + + def tests_after_upload(self): + self.assertEqual(self.saved_file.folder, _("Home/Test Folder 1")) + + folder_size = frappe.db.get_value("File", _("Home/Test Folder 1"), "file_size") + saved_file_size = frappe.db.get_value("File", self.saved_file.name, "file_size") + + self.assertEqual(folder_size, saved_file_size) + + def test_file_copy(self): + folder = self.get_folder("Test Folder 2", "Home") + + file = frappe.get_doc("File", {"file_name":"file_copy.txt"}) + move_file([{"name": file.name}], folder.name, file.folder) + file = frappe.get_doc("File", {"file_name":"file_copy.txt"}) + + self.assertEqual(_("Home/Test Folder 2"), file.folder) + self.assertEqual(frappe.db.get_value("File", _("Home/Test Folder 2"), "file_size"), file.file_size) + self.assertEqual(frappe.db.get_value("File", _("Home/Test Folder 1"), "file_size"), 0) + + def test_folder_copy(self): + folder = self.get_folder("Test Folder 2", "Home") + folder = self.get_folder("Test Folder 3", "Home/Test Folder 2") + + self.saved_file = save_file('folder_copy.txt', "Testing folder copy example.", "", "", folder.name) + + move_file([{"name": folder.name}], 'Home/Test Folder 1', folder.folder) + + file = frappe.get_doc("File", {"file_name":"folder_copy.txt"}) + file_copy_txt = frappe.get_value("File", {"file_name":"file_copy.txt"}) + if file_copy_txt: + frappe.get_doc("File", file_copy_txt).delete() + + self.assertEqual(_("Home/Test Folder 1/Test Folder 3"), file.folder) + self.assertEqual(frappe.db.get_value("File", _("Home/Test Folder 1"), "file_size"), file.file_size) + self.assertEqual(frappe.db.get_value("File", _("Home/Test Folder 2"), "file_size"), 0) + + def test_non_parent_folder(self): + d = frappe.get_doc({ + "doctype": "File", + "file_name": _("Test_Folder"), + "is_folder": 1 + }) + + self.assertRaises(frappe.ValidationError, d.save) + + def test_on_delete(self): + file = frappe.get_doc("File", {"file_name":"file_copy.txt"}) + file.delete() + + self.assertEqual(frappe.db.get_value("File", _("Home/Test Folder 1"), "file_size"), 0) + + folder = self.get_folder("Test Folder 3", "Home/Test Folder 1") + self.saved_file = save_file('folder_copy.txt', "Testing folder copy example.", "", "", folder.name) + + folder = frappe.get_doc("File", "Home/Test Folder 1/Test Folder 3") + self.assertRaises(frappe.ValidationError, folder.delete) diff --git a/frappe/core/doctype/file_data/README.md b/frappe/core/doctype/file_data/README.md deleted file mode 100644 index b1c31035ae..0000000000 --- a/frappe/core/doctype/file_data/README.md +++ /dev/null @@ -1,3 +0,0 @@ -File record and its relation to document to which the file is attached. - -This is appened to the form as `docinfo` when a document is loaded. \ No newline at end of file diff --git a/frappe/core/doctype/file_data/__init__.py b/frappe/core/doctype/file_data/__init__.py deleted file mode 100644 index 4dbcd0d163..0000000000 --- a/frappe/core/doctype/file_data/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# MIT License. See license.txt - -from __future__ import unicode_literals diff --git a/frappe/core/doctype/file_data/file_data.json b/frappe/core/doctype/file_data/file_data.json deleted file mode 100644 index e5b36c5b2a..0000000000 --- a/frappe/core/doctype/file_data/file_data.json +++ /dev/null @@ -1,84 +0,0 @@ -{ - "allow_import": 1, - "autoname": "File.######", - "creation": "2012-12-12 11:19:22", - "docstatus": 0, - "doctype": "DocType", - "fields": [ - { - "fieldname": "file_name", - "fieldtype": "Data", - "in_list_view": 1, - "label": "File Name", - "oldfieldname": "file_name", - "oldfieldtype": "Data", - "permlevel": 0, - "read_only": 1 - }, - { - "fieldname": "file_url", - "fieldtype": "Data", - "in_list_view": 1, - "label": "File URL", - "permlevel": 0, - "read_only": 1 - }, - { - "fieldname": "attached_to_doctype", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Attached To DocType", - "options": "DocType", - "permlevel": 0, - "read_only": 1, - "search_index": 1 - }, - { - "fieldname": "attached_to_name", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Attached To Name", - "permlevel": 0, - "read_only": 1, - "search_index": 1 - }, - { - "fieldname": "file_size", - "fieldtype": "Int", - "in_list_view": 1, - "label": "File Size", - "permlevel": 0, - "read_only": 1 - }, - { - "fieldname": "content_hash", - "fieldtype": "Data", - "label": "Content Hash", - "permlevel": 0, - "search_index": 1 - } - ], - "icon": "icon-file", - "idx": 1, - "in_create": 1, - "modified": "2015-02-05 05:11:38.944926", - "modified_by": "Administrator", - "module": "Core", - "name": "File Data", - "owner": "Administrator", - "permissions": [ - { - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "permlevel": 0, - "print": 1, - "read": 1, - "role": "System Manager", - "share": 1, - "write": 1 - } - ], - "read_only": 0 -} \ No newline at end of file diff --git a/frappe/core/doctype/file_data/file_data.py b/frappe/core/doctype/file_data/file_data.py deleted file mode 100644 index 7929386b09..0000000000 --- a/frappe/core/doctype/file_data/file_data.py +++ /dev/null @@ -1,57 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# MIT License. See license.txt - -from __future__ import unicode_literals -""" -record of files - -naming for same name files: file.gif, file-1.gif, file-2.gif etc -""" - -import frappe, frappe.utils, os -from frappe import conf -from frappe.model.document import Document -from frappe.utils.file_manager import delete_file_data_content - -class FileData(Document): - no_feed_on_delete = True - - def before_insert(self): - frappe.local.rollback_observers.append(self) - - def validate(self): - if not self.flags.ignore_duplicate_entry_error: - # check duplicate assignement - n_records = frappe.db.sql("""select name from `tabFile Data` - where content_hash=%s - and name!=%s - and attached_to_doctype=%s - and attached_to_name=%s""", (self.content_hash, self.name, self.attached_to_doctype, - self.attached_to_name)) - if len(n_records) > 0: - self.duplicate_entry = n_records[0][0] - frappe.throw(frappe._("Same file has already been attached to the record"), frappe.DuplicateEntryError) - - def on_trash(self): - if self.attached_to_name: - # check persmission - try: - if not self.flags.ignore_permissions and \ - not frappe.has_permission(self.attached_to_doctype, "write", self.attached_to_name): - - frappe.msgprint(frappe._("No permission to write / remove."), raise_exception=True) - - except frappe.DoesNotExistError: - pass - - # if file not attached to any other record, delete it - if self.file_name and self.content_hash and (not frappe.db.count("File Data", - {"content_hash": self.content_hash, "name": ["!=", self.name]})): - delete_file_data_content(self) - - def on_rollback(self): - self.on_trash() - -def on_doctype_update(): - frappe.db.add_index("File Data", ["attached_to_doctype", "attached_to_name"]) - diff --git a/frappe/core/doctype/module_def/module_def.json b/frappe/core/doctype/module_def/module_def.json index f002d87577..4978b8eebe 100644 --- a/frappe/core/doctype/module_def/module_def.json +++ b/frappe/core/doctype/module_def/module_def.json @@ -1,31 +1,71 @@ { + "allow_copy": 0, + "allow_import": 0, "allow_rename": 1, "autoname": "field:module_name", "creation": "2013-01-10 16:34:03", + "custom": 0, "docstatus": 0, "doctype": "DocType", "fields": [ { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "module_name", "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, "in_list_view": 1, "label": "Module Name", + "length": 0, + "no_copy": 0, "oldfieldname": "module_name", "oldfieldtype": "Data", - "permlevel": 0 + "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "app_name", "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, "in_list_view": 1, "label": "App Name", + "length": 0, + "no_copy": 0, "permlevel": 0, - "reqd": 1 + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 } ], + "hide_heading": 0, + "hide_toolbar": 0, "icon": "icon-sitemap", "idx": 1, - "modified": "2015-02-05 05:11:41.388856", + "in_create": 0, + "in_dialog": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 0, + "max_attachments": 0, + "modified": "2015-11-16 06:29:50.398063", "modified_by": "Administrator", "module": "Core", "name": "Module Def", @@ -33,23 +73,45 @@ "permissions": [ { "amend": 0, + "apply_user_permissions": 0, "cancel": 0, "create": 1, "delete": 1, "email": 1, + "export": 0, + "if_owner": 0, + "import": 0, "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "Administrator", + "set_user_permissions": 0, "share": 1, "submit": 0, "write": 1 }, { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "create": 0, + "delete": 0, + "email": 0, + "export": 0, + "if_owner": 0, + "import": 0, "permlevel": 0, + "print": 0, "read": 1, - "role": "System Manager" + "report": 0, + "role": "System Manager", + "set_user_permissions": 0, + "share": 0, + "submit": 0, + "write": 0 } - ] + ], + "read_only": 0, + "read_only_onload": 0 } \ No newline at end of file diff --git a/frappe/core/doctype/module_def/module_def.py b/frappe/core/doctype/module_def/module_def.py index 75b7e14825..44df59cb52 100644 --- a/frappe/core/doctype/module_def/module_def.py +++ b/frappe/core/doctype/module_def/module_def.py @@ -7,7 +7,6 @@ import frappe, os from frappe.model.document import Document class ModuleDef(Document): - __doclink__ = "https://frappe.io/docs/models/core/module_def" def on_update(self): """If in `developer_mode`, create folder for module and add in `modules.txt` of app if missing.""" @@ -39,7 +38,3 @@ class ModuleDef(Document): frappe.clear_cache() frappe.setup_module_map() - - - - diff --git a/frappe/core/doctype/module_def/test_module_def.py b/frappe/core/doctype/module_def/test_module_def.py new file mode 100644 index 0000000000..1f9bea4768 --- /dev/null +++ b/frappe/core/doctype/module_def/test_module_def.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +import frappe +import unittest + +# test_records = frappe.get_test_records('Module Def') + +class TestModuleDef(unittest.TestCase): + pass diff --git a/frappe/core/doctype/page/page.json b/frappe/core/doctype/page/page.json index 75a2fc003d..62bd473a74 100644 --- a/frappe/core/doctype/page/page.json +++ b/frappe/core/doctype/page/page.json @@ -1,105 +1,254 @@ { "allow_copy": 0, + "allow_import": 0, "allow_rename": 1, "autoname": "field:page_name", "creation": "2012-12-20 17:16:49", + "custom": 0, "docstatus": 0, "doctype": "DocType", "fields": [ { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "page_html", "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Page HTML", + "length": 0, + "no_copy": 0, "oldfieldtype": "Section Break", - "permlevel": 0 + "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "page_name", "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, "in_list_view": 1, "label": "Page Name", + "length": 0, + "no_copy": 0, "oldfieldname": "page_name", "oldfieldtype": "Data", "permlevel": 0, - "reqd": 1 + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "title", "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, "in_list_view": 1, "label": "Title", - "permlevel": 0 + "length": 0, + "no_copy": 1, + "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "icon", "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, "in_list_view": 1, "label": "icon", - "permlevel": 0 + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "column_break0", "fieldtype": "Column Break", - "permlevel": 0 + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "module", "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Module", + "length": 0, + "no_copy": 0, "oldfieldname": "module", "oldfieldtype": "Select", "options": "Module Def", "permlevel": 0, - "reqd": 1 + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "standard", "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Standard", + "length": 0, + "no_copy": 0, "oldfieldname": "standard", "oldfieldtype": "Select", "options": "\nYes\nNo", "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, "reqd": 1, - "search_index": 1 + "search_index": 1, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "section_break0", "fieldtype": "Section Break", - "permlevel": 0 + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "roles", "fieldtype": "Table", "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Roles", + "length": 0, + "no_copy": 0, "oldfieldname": "roles", "oldfieldtype": "Table", "options": "Page Role", "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, "reqd": 0, - "search_index": 0 + "search_index": 0, + "set_only_once": 0, + "unique": 0 } ], "hide_heading": 0, "hide_toolbar": 0, "icon": "icon-file", "idx": 1, + "in_create": 0, + "in_dialog": 0, + "is_submittable": 0, "issingle": 0, "istable": 0, - "modified": "2015-02-05 05:11:41.982758", + "max_attachments": 0, + "modified": "2015-11-16 06:29:51.370746", "modified_by": "Administrator", "module": "Core", "name": "Page", "owner": "Administrator", "permissions": [ { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, "create": 1, + "delete": 0, "email": 1, + "export": 0, + "if_owner": 0, + "import": 0, "permlevel": 0, "print": 1, "read": 1, + "report": 0, "role": "Administrator", + "set_user_permissions": 0, "share": 1, "submit": 0, "write": 1 @@ -112,6 +261,7 @@ "delete": 0, "email": 1, "export": 0, + "if_owner": 0, "import": 0, "permlevel": 0, "print": 1, @@ -122,15 +272,8 @@ "share": 1, "submit": 0, "write": 1 - }, - { - "apply_user_permissions": 1, - "email": 1, - "permlevel": 0, - "print": 1, - "read": 1, - "role": "All" } ], - "read_only": 0 + "read_only": 0, + "read_only_onload": 0 } \ No newline at end of file diff --git a/frappe/core/doctype/page/page.py b/frappe/core/doctype/page/page.py index 1e961dbf06..c95aaf4f2d 100644 --- a/frappe/core/doctype/page/page.py +++ b/frappe/core/doctype/page/page.py @@ -5,6 +5,8 @@ from __future__ import unicode_literals import frappe from frappe.model.document import Document from frappe.build import html_to_js_template +from frappe import conf, _ +from frappe.desk.form.meta import get_code_files_via_hooks, get_js class Page(Document): def autoname(self): @@ -26,13 +28,18 @@ class Page(Document): cnt = 1 self.name += '-' + str(cnt) + def validate(self): + if self.is_new() and not getattr(conf,'developer_mode', 0): + frappe.throw(_("Not in Developer Mode")) + if frappe.session.user!="Administrator": + frappe.throw(_("Only Administrator can edit")) + # export def on_update(self): """ Writes the .txt for this page and if write_content is checked, it will write out a .html file """ - from frappe import conf from frappe.core.doctype.doctype.doctype import make_module_and_roles make_module_and_roles(self, "roles") @@ -62,20 +69,37 @@ class Page(Document): d[key] = self.get(key) return d + def is_permitted(self): + """Returns true if Page Role is not set or the user is allowed.""" + from frappe.utils import has_common + + allowed = [d.role for d in frappe.get_all("Page Role", fields=["role"], + filters={"parent": self.name})] + + if not allowed: + return True + + roles = frappe.get_roles() + + if has_common(roles, allowed): + return True + def load_assets(self): from frappe.modules import get_module_path, scrub import os - path = os.path.join(get_module_path(self.module), 'page', scrub(self.name)) + page_name = scrub(self.name) + + path = os.path.join(get_module_path(self.module), 'page', page_name) # script - fpath = os.path.join(path, scrub(self.name) + '.js') + fpath = os.path.join(path, page_name + '.js') if os.path.exists(fpath): with open(fpath, 'r') as f: self.script = unicode(f.read(), "utf-8") # css - fpath = os.path.join(path, scrub(self.name) + '.css') + 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") @@ -85,8 +109,27 @@ class Page(Document): if fname.endswith(".html"): with open(os.path.join(path, fname), 'r') as f: template = unicode(f.read(), "utf-8") + if "" in template: + context = {} + try: + context = frappe.get_attr("{app}.{module}.page.{page}.{page}.get_context".format( + app = frappe.local.module_app[scrub(self.module)], + module = scrub(self.module), + page = page_name + ))(context) + except (AttributeError, ImportError): + pass + + template = frappe.render_template(template, context) self.script = html_to_js_template(fname, template) + self.script if frappe.lang != 'en': from frappe.translate import get_lang_js self.script += get_lang_js("page", self.name) + + for path in get_code_files_via_hooks("page_js", self.name): + js = get_js(path) + if js: + self.script += "\n\n" + js + + diff --git a/frappe/core/doctype/page_role/page_role.json b/frappe/core/doctype/page_role/page_role.json index d220ca42e2..6f6b718a6c 100644 --- a/frappe/core/doctype/page_role/page_role.json +++ b/frappe/core/doctype/page_role/page_role.json @@ -1,34 +1,54 @@ { "allow_copy": 0, + "allow_import": 0, + "allow_rename": 0, "autoname": "hash", "creation": "2013-02-22 01:27:34", + "custom": 0, "docstatus": 0, "doctype": "DocType", "fields": [ { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "role", "fieldtype": "Link", "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, "in_list_view": 1, "label": "Role", + "length": 0, + "no_copy": 0, "oldfieldname": "role", "oldfieldtype": "Link", "options": "Role", "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, "reqd": 0, - "search_index": 0 + "search_index": 0, + "set_only_once": 0, + "unique": 0 } ], "hide_heading": 0, "hide_toolbar": 0, "idx": 1, + "in_create": 0, + "in_dialog": 0, + "is_submittable": 0, "issingle": 0, "istable": 1, - "modified": "2015-02-19 01:07:00.897854", + "max_attachments": 0, + "modified": "2015-11-16 06:29:51.420589", "modified_by": "Administrator", "module": "Core", "name": "Page Role", "owner": "Administrator", "permissions": [], - "read_only": 0 + "read_only": 0, + "read_only_onload": 0 } \ No newline at end of file diff --git a/frappe/core/doctype/patch_log/patch_log.json b/frappe/core/doctype/patch_log/patch_log.json index f7e6b65297..96a0e4fff8 100644 --- a/frappe/core/doctype/patch_log/patch_log.json +++ b/frappe/core/doctype/patch_log/patch_log.json @@ -1,33 +1,75 @@ { + "allow_copy": 0, + "allow_import": 0, + "allow_rename": 0, "autoname": "PATCHLOG.#####", - "creation": "2013-01-17 11:36:45.000000", + "creation": "2013-01-17 11:36:45", + "custom": 0, "description": "List of patches executed", "docstatus": 0, "doctype": "DocType", "document_type": "System", "fields": [ { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "patch", - "fieldtype": "Data", + "fieldtype": "Small Text", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Patch", - "permlevel": 0 + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 } ], + "hide_heading": 0, + "hide_toolbar": 0, "icon": "icon-cog", "idx": 1, - "modified": "2013-12-20 19:24:15.000000", + "in_create": 0, + "in_dialog": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 0, + "max_attachments": 0, + "modified": "2015-11-16 06:29:51.487656", "modified_by": "Administrator", "module": "Core", "name": "Patch Log", "owner": "Administrator", "permissions": [ { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "create": 0, + "delete": 0, "email": 1, + "export": 0, + "if_owner": 0, + "import": 0, "permlevel": 0, "print": 1, "read": 1, "report": 1, - "role": "Administrator" + "role": "Administrator", + "set_user_permissions": 0, + "share": 0, + "submit": 0, + "write": 0 } - ] + ], + "read_only": 0, + "read_only_onload": 0 } \ No newline at end of file diff --git a/frappe/core/doctype/patch_log/test_patch_log.py b/frappe/core/doctype/patch_log/test_patch_log.py new file mode 100644 index 0000000000..0a7f22a78b --- /dev/null +++ b/frappe/core/doctype/patch_log/test_patch_log.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +import frappe +import unittest + +# test_records = frappe.get_test_records('Patch Log') + +class TestPatchLog(unittest.TestCase): + pass diff --git a/frappe/core/doctype/report/report.json b/frappe/core/doctype/report/report.json index ec828a0f1a..ffe98701f0 100644 --- a/frappe/core/doctype/report/report.json +++ b/frappe/core/doctype/report/report.json @@ -1,173 +1,408 @@ { + "allow_copy": 0, + "allow_import": 0, + "allow_rename": 0, "autoname": "field:report_name", "creation": "2013-03-09 15:45:57", + "custom": 0, "docstatus": 0, "doctype": "DocType", "document_type": "System", "fields": [ { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "report_name", "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Report Name", + "length": 0, + "no_copy": 0, "permlevel": 0, + "print_hide": 0, "read_only": 0, - "reqd": 1 + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "ref_doctype", "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, "in_list_view": 1, "label": "Ref DocType", + "length": 0, + "no_copy": 0, "options": "DocType", "permlevel": 0, + "print_hide": 0, "read_only": 0, - "reqd": 1 + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "is_standard", "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, "in_list_view": 1, "label": "Is Standard", + "length": 0, + "no_copy": 0, "options": "No\nYes", "permlevel": 0, + "print_hide": 0, "read_only": 0, - "reqd": 1 + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "module", "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Module", + "length": 0, + "no_copy": 0, "options": "Module Def", - "permlevel": 0 + "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "add_total_row", "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Add Total Row", - "permlevel": 0 + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "column_break_4", "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "length": 0, + "no_copy": 0, "permlevel": 0, - "read_only": 0 + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "report_type", "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Report Type", + "length": 0, + "no_copy": 0, "options": "Report Builder\nQuery Report\nScript Report", "permlevel": 0, + "print_hide": 0, "read_only": 0, - "reqd": 1 + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "disabled", "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Disabled", + "length": 0, + "no_copy": 0, "permlevel": 0, - "read_only": 0 + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "default": "1", "depends_on": "eval:[\"Query Report\", \"Script Report\"].indexOf(doc.report_type)!==-1", "fieldname": "apply_user_permissions", "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, "in_list_view": 0, "label": "Apply User Permissions", - "permlevel": 0 + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "section_break_6", "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "length": 0, + "no_copy": 0, "permlevel": 0, - "read_only": 0 + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "depends_on": "eval:doc.report_type==\"Query Report\"", "fieldname": "query", "fieldtype": "Code", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Query", + "length": 0, + "no_copy": 0, "permlevel": 0, - "read_only": 0 + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "depends_on": "", "description": "JavaScript Format: frappe.query_reports['REPORTNAME'] = {}", "fieldname": "javascript", "fieldtype": "Code", "hidden": 1, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Javascript", - "permlevel": 0 + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "depends_on": "eval:doc.report_type==\"Report Builder\"", "fieldname": "json", "fieldtype": "Text", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "JSON", + "length": 0, + "no_copy": 0, "permlevel": 0, - "read_only": 1 + "print_hide": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 } ], + "hide_heading": 0, + "hide_toolbar": 0, "icon": "icon-table", "idx": 1, - "modified": "2015-02-05 05:11:44.753200", + "in_create": 0, + "in_dialog": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 0, + "max_attachments": 0, + "modified": "2015-11-16 06:29:55.357855", "modified_by": "Administrator", "module": "Core", "name": "Report", "owner": "Administrator", "permissions": [ { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, "create": 1, "delete": 1, "email": 1, + "export": 0, + "if_owner": 0, + "import": 0, "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "Administrator", + "set_user_permissions": 0, "share": 1, "submit": 0, "write": 1 }, { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, "create": 1, "delete": 1, "email": 1, + "export": 0, + "if_owner": 0, + "import": 0, "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "System Manager", + "set_user_permissions": 0, "share": 1, "submit": 0, "write": 1 }, { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, "create": 1, "delete": 1, "email": 1, + "export": 0, + "if_owner": 0, + "import": 0, "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "Report Manager", + "set_user_permissions": 0, "share": 1, "submit": 0, "write": 1 }, { - "apply_user_permissions": 1, + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "create": 0, "delete": 0, "email": 1, + "export": 0, + "if_owner": 0, + "import": 0, "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "All", - "submit": 0 + "set_user_permissions": 0, + "share": 0, + "submit": 0, + "write": 0 } ], + "read_only": 0, + "read_only_onload": 0, "sort_field": "modified", "sort_order": "DESC" } \ No newline at end of file diff --git a/frappe/core/doctype/role/role.json b/frappe/core/doctype/role/role.json index 6706cb0e3e..18bdb80f59 100644 --- a/frappe/core/doctype/role/role.json +++ b/frappe/core/doctype/role/role.json @@ -1,29 +1,49 @@ { "allow_copy": 0, + "allow_import": 0, "allow_rename": 1, "autoname": "field:role_name", "creation": "2013-01-08 15:50:01", + "custom": 0, "docstatus": 0, "doctype": "DocType", "fields": [ { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "role_name", "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, "in_list_view": 1, "label": "Role Name", + "length": 0, + "no_copy": 0, "oldfieldname": "role_name", "oldfieldtype": "Data", "permlevel": 0, - "reqd": 1 + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 } ], "hide_heading": 0, "hide_toolbar": 0, "icon": "icon-bookmark", "idx": 1, + "in_create": 0, + "in_dialog": 0, + "is_submittable": 0, "issingle": 0, "istable": 0, - "modified": "2015-02-05 05:11:44.831475", + "max_attachments": 0, + "modified": "2015-11-16 06:29:55.424835", "modified_by": "Administrator", "module": "Core", "name": "Role", @@ -31,36 +51,65 @@ "permissions": [ { "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, "create": 1, + "delete": 0, "email": 1, + "export": 0, + "if_owner": 0, + "import": 0, "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "System Manager", + "set_user_permissions": 0, "share": 1, "submit": 0, "write": 1 }, { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, "create": 1, "delete": 1, "email": 1, + "export": 0, + "if_owner": 0, + "import": 0, "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "Administrator", + "set_user_permissions": 0, "share": 1, "submit": 0, "write": 1 }, { - "apply_user_permissions": 1, + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "create": 0, + "delete": 0, + "email": 0, + "export": 0, + "if_owner": 0, + "import": 0, "permlevel": 0, + "print": 0, "read": 1, - "role": "All" + "report": 0, + "role": "All", + "set_user_permissions": 0, + "share": 0, + "submit": 0, + "write": 0 } ], - "read_only": 0 + "read_only": 0, + "read_only_onload": 0 } \ No newline at end of file diff --git a/frappe/core/doctype/role/role.py b/frappe/core/doctype/role/role.py index fa52abf36b..da156d137c 100644 --- a/frappe/core/doctype/role/role.py +++ b/frappe/core/doctype/role/role.py @@ -7,4 +7,9 @@ import frappe from frappe.model.document import Document class Role(Document): - pass \ No newline at end of file + def after_insert(self): + # Add role to Administrator + if frappe.flags.in_install != "frappe": + user = frappe.get_doc("User", "Administrator") + user.flags.ignore_permissions = True + user.add_roles(self.name) diff --git a/frappe/core/doctype/scheduler_log/scheduler_log.json b/frappe/core/doctype/scheduler_log/scheduler_log.json index cd87f27920..7582bb8960 100644 --- a/frappe/core/doctype/scheduler_log/scheduler_log.json +++ b/frappe/core/doctype/scheduler_log/scheduler_log.json @@ -1,55 +1,121 @@ { + "allow_copy": 0, + "allow_import": 0, + "allow_rename": 0, "autoname": "SCHLOG.#####", "creation": "2013-01-16 13:09:40", + "custom": 0, "description": "Log of Scheduler Errors", "docstatus": 0, "doctype": "DocType", "document_type": "System", "fields": [ { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "default": "0", "fieldname": "seen", "fieldtype": "Check", "hidden": 1, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Seen", + "length": 0, + "no_copy": 0, "permlevel": 0, - "precision": "" + "precision": "", + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "method", "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, "in_list_view": 1, "label": "Method", - "permlevel": 0 + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "error", "fieldtype": "Text", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Error", - "permlevel": 0 + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 } ], + "hide_heading": 0, + "hide_toolbar": 0, "icon": "icon-warning-sign", "idx": 1, - "modified": "2015-05-28 02:49:12.819934", + "in_create": 0, + "in_dialog": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 0, + "max_attachments": 0, + "modified": "2015-11-16 06:29:57.440897", "modified_by": "Administrator", "module": "Core", "name": "Scheduler Log", "owner": "Administrator", "permissions": [ { + "amend": 0, + "apply_user_permissions": 0, "cancel": 0, "create": 1, "delete": 1, "email": 1, + "export": 0, + "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 } - ] + ], + "read_only": 0, + "read_only_onload": 0 } \ No newline at end of file diff --git a/frappe/core/doctype/scheduler_log/scheduler_log.py b/frappe/core/doctype/scheduler_log/scheduler_log.py index bb841405bd..4af039c8ee 100644 --- a/frappe/core/doctype/scheduler_log/scheduler_log.py +++ b/frappe/core/doctype/scheduler_log/scheduler_log.py @@ -16,4 +16,4 @@ class SchedulerLog(Document): def set_old_logs_as_seen(): frappe.db.sql("""update `tabScheduler Log` set seen=1 - where ifnull(seen, 0)=0 and datediff(curdate(), creation) > 7""") + where seen=0 and datediff(curdate(), creation) > 7""") diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json index 82ffb370d2..4142edac7c 100644 --- a/frappe/core/doctype/system_settings/system_settings.json +++ b/frappe/core/doctype/system_settings/system_settings.json @@ -1,152 +1,461 @@ { + "allow_copy": 0, + "allow_import": 0, + "allow_rename": 0, "creation": "2014-04-17 16:53:52.640856", + "custom": 0, "docstatus": 0, "doctype": "DocType", "document_type": "System", "fields": [ { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "localization", "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "", - "permlevel": 0 + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "language", "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, "in_list_view": 1, "label": "Language", + "no_copy": 0, "options": "Loading...", "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, "reqd": 1, - "search_index": 0 + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "column_break_3", "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "no_copy": 0, "permlevel": 0, - "precision": "" + "precision": "", + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "time_zone", "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Time Zone", + "no_copy": 0, "permlevel": 0, - "reqd": 1 + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "date_and_number_format", "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "", - "permlevel": 0 + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "date_format", "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Date Format", + "no_copy": 0, "options": "yyyy-mm-dd\ndd-mm-yyyy\ndd/mm/yyyy\ndd.mm.yyyy\nmm/dd/yyyy\nmm-dd-yyyy", "permlevel": 0, - "reqd": 1 + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "column_break_7", "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "no_copy": 0, "permlevel": 0, - "precision": "" + "precision": "", + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "number_format", "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Number Format", + "no_copy": 0, "options": "#,###.##\n#.###,##\n# ###.##\n# ###,##\n#'###.##\n#, ###.##\n#,##,###.##\n#,###.###\n#.###\n#,###", "permlevel": 0, - "reqd": 1 + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "float_precision", "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Float Precision", + "no_copy": 0, "options": "\n2\n3\n4\n5\n6", - "permlevel": 0 + "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "security", "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Security", - "permlevel": 0 + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "default": "06:00", "description": "Session Expiry in Hours e.g. 06:00", "fieldname": "session_expiry", "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Session Expiry", + "no_copy": 0, "options": "", - "permlevel": 0 + "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "default": "720:00", "description": "In Hours", "fieldname": "session_expiry_mobile", "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Session Expiry Mobile", + "no_copy": 0, "permlevel": 0, - "precision": "" + "precision": "", + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "column_break_13", "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "no_copy": 0, "permlevel": 0, - "precision": "" + "precision": "", + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "description": "Run scheduled jobs only if checked", "fieldname": "enable_scheduler", "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, "in_list_view": 0, "label": "Enable Scheduled Jobs", - "permlevel": 0 + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "scheduler_last_event", "fieldtype": "Data", "hidden": 1, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Scheduler Last Event", + "no_copy": 0, "permlevel": 0, "precision": "", - "report_hide": 1 + "print_hide": 0, + "read_only": 0, + "report_hide": 1, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "description": "eg. If Apply User Permissions is checked for Report DocType but no User Permissions are defined for Report for a User, then all Reports are shown to that User", + "fieldname": "ignore_user_permissions_if_missing", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Ignore User Permissions If Missing", + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "email", "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "EMail", + "no_copy": 0, "permlevel": 0, - "precision": "" + "precision": "", + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "description": "Your organization name and address for the email footer.", "fieldname": "email_footer_address", "fieldtype": "Small Text", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Email Footer Address", + "no_copy": 0, "permlevel": 0, - "precision": "" + "precision": "", + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "column_break_18", "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "no_copy": 0, "permlevel": 0, - "precision": "" + "precision": "", + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "disable_standard_email_footer", "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Disable Standard Email Footer", + "no_copy": 0, "permlevel": 0, - "precision": "" + "precision": "", + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 } ], + "hide_heading": 0, + "hide_toolbar": 0, "icon": "icon-cog", + "in_create": 0, + "in_dialog": 0, + "is_submittable": 0, "issingle": 1, - "modified": "2015-05-21 07:15:55.682132", + "istable": 0, + "modified": "2015-09-07 11:36:15.465900", "modified_by": "Administrator", "module": "Core", "name": "System Settings", @@ -154,12 +463,26 @@ "owner": "Administrator", "permissions": [ { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, "create": 1, + "delete": 0, + "email": 0, + "export": 0, + "if_owner": 0, + "import": 0, "permlevel": 0, + "print": 0, "read": 1, + "report": 0, "role": "System Manager", + "set_user_permissions": 0, "share": 1, + "submit": 0, "write": 1 } - ] + ], + "read_only": 0, + "read_only_onload": 0 } \ No newline at end of file diff --git a/frappe/core/doctype/user/user.js b/frappe/core/doctype/user/user.js index 9e6afe4d41..c5ccde182c 100644 --- a/frappe/core/doctype/user/user.js +++ b/frappe/core/doctype/user/user.js @@ -35,10 +35,6 @@ cur_frm.cscript.before_load = function(doc, dt, dn, callback) { } } -cur_frm.cscript.user_image = function(doc) { - refresh_field("user_image_show"); -} - cur_frm.cscript.refresh = function(doc) { if(doc.name===user && !doc.__unsaved && frappe.languages && (doc.language || frappe.boot.user.language) && doc.language !== frappe.boot.user.language) { @@ -76,7 +72,7 @@ cur_frm.cscript.refresh = function(doc) { cur_frm.cscript.enabled = function(doc) { if(!doc.__islocal && has_common(user_roles, ["Administrator", "System Manager"])) { cur_frm.toggle_display(['sb1', 'sb3', 'modules_access'], doc.enabled); - cur_frm.toggle_enable('*', doc.enabled); + // cur_frm.toggle_enable('*', doc.enabled); cur_frm.set_df_property('enabled', 'read_only', 0); } diff --git a/frappe/core/doctype/user/user.json b/frappe/core/doctype/user/user.json index 51fc55df99..9650b623fa 100644 --- a/frappe/core/doctype/user/user.json +++ b/frappe/core/doctype/user/user.json @@ -3,517 +3,1436 @@ "allow_import": 1, "allow_rename": 1, "creation": "2014-03-11 14:55:00", + "custom": 0, "description": "Represents a User in the system.", "docstatus": 0, "doctype": "DocType", - "document_type": "Master", + "document_type": "", "fields": [ { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "sb0_5", "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "", - "permlevel": 0 + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "default": "1", "fieldname": "enabled", "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, "in_list_view": 0, "label": "Enabled", + "length": 0, "no_copy": 0, "oldfieldname": "enabled", "oldfieldtype": "Check", "permlevel": 0, - "read_only": 1 + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "depends_on": "enabled", "fieldname": "section_break_3", "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "length": 0, + "no_copy": 0, "permlevel": 0, - "precision": "" + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "email", "fieldtype": "Data", "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Email", + "length": 0, "no_copy": 1, "oldfieldname": "email", "oldfieldtype": "Data", "options": "Email", "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, "reqd": 1, - "search_index": 0 + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "first_name", "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, "in_list_view": 0, "label": "First Name", + "length": 0, "no_copy": 0, "oldfieldname": "first_name", "oldfieldtype": "Data", "permlevel": 0, - "reqd": 1 + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "middle_name", "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Middle Name (Optional)", + "length": 0, "no_copy": 0, "oldfieldname": "middle_name", "oldfieldtype": "Data", - "permlevel": 0 + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "last_name", "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, "in_list_view": 0, "label": "Last Name", + "length": 0, + "no_copy": 0, "oldfieldname": "last_name", "oldfieldtype": "Data", - "permlevel": 0 + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "username", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Username", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 1 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "default": "1", "depends_on": "eval:doc.__islocal", "fieldname": "send_welcome_email", "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Send Welcome Email", + "length": 0, + "no_copy": 0, "permlevel": 0, - "precision": "" + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "unsubscribed", "fieldtype": "Check", "hidden": 1, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Unsubscribed", + "length": 0, "no_copy": 1, - "permlevel": 0 + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "column_break0", "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "length": 0, + "no_copy": 0, "oldfieldtype": "Column Break", "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, "print_width": "50%", + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0, "width": "50%" }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "description": "", "fieldname": "language", "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Language", + "length": 0, + "no_copy": 0, "options": "Loading...", - "permlevel": 0 + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "description": "", "fieldname": "time_zone", "fieldtype": "Select", - "label": "Timezone", - "permlevel": 0 - }, - { - "depends_on": "eval:!doc.__islocal", - "fieldname": "change_password", - "fieldtype": "Section Break", - "label": "", - "permlevel": 0 - }, - { - "fieldname": "new_password", - "fieldtype": "Password", - "label": "Set New Password", - "no_copy": 1, - "permlevel": 0 - }, - { - "depends_on": "", - "fieldname": "send_password_update_notification", - "fieldtype": "Check", - "label": "Send Password Update Notification", - "permlevel": 0, - "precision": "" - }, - { - "fieldname": "reset_password_key", - "fieldtype": "Data", - "hidden": 1, - "label": "Reset Password Key", - "no_copy": 1, - "permlevel": 0, - "print_hide": 1, - "read_only": 1 - }, - { - "depends_on": "eval:!doc.__islocal", - "fieldname": "display_settings", - "fieldtype": "Section Break", - "label": "", - "permlevel": 0 - }, - { - "description": "Get your globally recognized avatar from Gravatar.com", - "fieldname": "user_image", - "fieldtype": "Attach", "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Timezone", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "description": "Get your globally recognized avatar from Gravatar.com", + "fieldname": "user_image", + "fieldtype": "Attach Image", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "User Image", + "length": 0, "no_copy": 1, - "permlevel": 0 - }, - { - "fieldname": "cb21", - "fieldtype": "Column Break", - "permlevel": 0 - }, - { - "fieldname": "user_image_show", - "fieldtype": "Image", - "label": "user_image_show", - "options": "user_image", - "permlevel": 0 - }, - { - "fieldname": "email_settings", - "fieldtype": "Section Break", - "label": "Email Settings", "permlevel": 0, - "precision": "" - }, - { - "default": "1", - "fieldname": "thread_notify", - "fieldtype": "Check", - "label": "Send Notifications for Transactions I Follow", - "permlevel": 0, - "precision": "" - }, - { - "fieldname": "email_signature", - "fieldtype": "Small Text", - "label": "Email Signature", - "no_copy": 1, - "permlevel": 0 - }, - { - "fieldname": "background", - "fieldtype": "Section Break", - "label": "", - "permlevel": 0, - "precision": "" - }, - { - "fieldname": "background_image", - "fieldtype": "Attach", - "label": "Background Image", - "permlevel": 0, - "precision": "" - }, - { - "fieldname": "background_style", - "fieldtype": "Select", - "label": "Background Style", - "options": "Fill Screen\nTile", - "permlevel": 0, - "precision": "" + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 1, + "depends_on": "enabled", "fieldname": "short_bio", "fieldtype": "Section Break", - "label": "", - "permlevel": 0 + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "More Information", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "gender", "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Gender", + "length": 0, + "no_copy": 0, "oldfieldname": "gender", "oldfieldtype": "Select", "options": "\nMale\nFemale\nOther", "permlevel": 0, - "search_index": 0 + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "birth_date", "fieldtype": "Date", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Birth Date", + "length": 0, "no_copy": 1, "oldfieldname": "birth_date", "oldfieldtype": "Date", - "permlevel": 0 + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "location", "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Location", - "no_copy": 1, - "permlevel": 0 - }, - { - "fieldname": "column_break_22", - "fieldtype": "Column Break", - "permlevel": 0 - }, - { - "fieldname": "bio", - "fieldtype": "Small Text", - "label": "Bio", + "length": 0, "no_copy": 1, "permlevel": 0, - "set_only_once": 0 + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "column_break_22", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "bio", + "fieldtype": "Small Text", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Bio", + "length": 0, + "no_copy": 1, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "mute_sounds", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Mute Sounds", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 1, + "depends_on": "eval:doc.enabled && (!doc.__islocal || !cint(doc.send_welcome_email))", + "fieldname": "change_password", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Change Password", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "new_password", + "fieldtype": "Password", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Set New Password", + "length": 0, + "no_copy": 1, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "depends_on": "eval:!doc.__islocal", + "fieldname": "send_password_update_notification", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Send Password Update Notification", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "reset_password_key", + "fieldtype": "Data", + "hidden": 1, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Reset Password Key", + "length": 0, + "no_copy": 1, + "permlevel": 0, + "print_hide": 1, + "print_hide_if_no_value": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 1, + "depends_on": "enabled", + "fieldname": "email_settings", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Email Settings", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "default": "1", + "fieldname": "thread_notify", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Send Notifications for Transactions I Follow", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "email_signature", + "fieldtype": "Small Text", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Email Signature", + "length": 0, + "no_copy": 1, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 1, + "depends_on": "enabled", + "fieldname": "background", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Desktop Background", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "background_image", + "fieldtype": "Attach", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Background Image", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "background_style", + "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Background Style", + "length": 0, + "no_copy": 0, + "options": "Fill Screen\nTile", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 1, + "depends_on": "enabled", "description": "Check / Uncheck roles assigned to the User. Click on the Role to find out what permissions that Role has.", "fieldname": "sb1", "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Roles", + "length": 0, + "no_copy": 0, "permlevel": 1, - "read_only": 1 + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "roles_html", "fieldtype": "HTML", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Roles HTML", + "length": 0, + "no_copy": 0, "permlevel": 0, - "read_only": 1 + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "user_roles", "fieldtype": "Table", "hidden": 1, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Roles Assigned", + "length": 0, + "no_copy": 0, "options": "UserRole", "permlevel": 1, "print_hide": 1, - "read_only": 1 + "print_hide_if_no_value": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 1, "default": "", "description": "Uncheck modules to hide from user's desktop", "fieldname": "modules_access", "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Modules Access", + "length": 0, + "no_copy": 0, "permlevel": 1, - "precision": "" + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "modules_html", "fieldtype": "HTML", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Modules HTML", + "length": 0, + "no_copy": 0, "permlevel": 1, - "precision": "" + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "block_modules", "fieldtype": "Table", "hidden": 1, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Block Modules", + "length": 0, + "no_copy": 0, "options": "Block Module", "permlevel": 1, - "precision": "" + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "description": "These values will be automatically updated in transactions and also will be useful to restrict permissions for this user on transactions containing these values.", "fieldname": "sb2", "fieldtype": "Section Break", "hidden": 1, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Defaults", + "length": 0, + "no_copy": 0, "oldfieldtype": "Column Break", "permlevel": 1, + "print_hide": 0, + "print_hide_if_no_value": 0, "print_width": "50%", "read_only": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0, "width": "50%" }, { - "description": "Enter default value fields (keys) and values. If you add multiple values for a field, the first one will be picked. These defaults are also used to set \"match\" permission rules. To see list of fields, go to Customize Form.", + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "description": "Enter default value fields (keys) and values. If you add multiple values for a field, the first one will be picked. These defaults are also used to set \"match\" permission rules. To see list of fields, go to \"Customize Form\".", "fieldname": "defaults", "fieldtype": "Table", "hidden": 1, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "User Defaults", + "length": 0, "no_copy": 1, "options": "DefaultValue", - "permlevel": 0 + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 1, + "depends_on": "enabled", "fieldname": "sb3", "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Security Settings", + "length": 0, + "no_copy": 0, "oldfieldtype": "Section Break", "permlevel": 0, - "read_only": 1 + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "default": "System User", "description": "User Type \"System User\" can access Desktop. \"Website User\" can only be logged into the website and portal pages. ", "fieldname": "user_type", "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, "in_list_view": 1, "label": "User Type", + "length": 0, + "no_copy": 0, "oldfieldname": "user_type", "oldfieldtype": "Select", "options": "System User\nWebsite User", - "permlevel": 0, - "read_only": 1, - "reqd": 1 + "permlevel": 1, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "description": "Allow user to login only after this hour (0-24)", "fieldname": "login_after", "fieldtype": "Int", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Login After", - "permlevel": 0, - "read_only": 1 + "length": 0, + "no_copy": 0, + "permlevel": 1, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "description": "Allow user to login only before this hour (0-24)", "fieldname": "login_before", "fieldtype": "Int", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Login Before", - "permlevel": 0, - "read_only": 1 + "length": 0, + "no_copy": 0, + "permlevel": 1, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "description": "Restrict user from this IP address only. Multiple IP addresses can be added by separating with commas. Also accepts partial IP addresses like (111.111.111)", "fieldname": "restrict_ip", "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Restrict IP", - "permlevel": 0, - "read_only": 1 + "length": 0, + "no_copy": 0, + "permlevel": 1, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "column_break1", "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "length": 0, + "no_copy": 0, "oldfieldtype": "Column Break", "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, "print_width": "50%", + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0, "width": "50%" }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "last_login", "fieldtype": "Read Only", "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Last Login", + "length": 0, "no_copy": 1, "oldfieldname": "last_login", "oldfieldtype": "Read Only", "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, "read_only": 1, + "report_hide": 0, "reqd": 0, - "search_index": 0 + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "last_ip", "fieldtype": "Read Only", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Last IP", + "length": 0, "no_copy": 1, "oldfieldname": "last_ip", "oldfieldtype": "Read Only", "permlevel": 0, - "read_only": 1 + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "last_active", + "fieldtype": "Datetime", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Last Active", + "length": 0, + "no_copy": 1, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "description": "Stores the JSON of last known versions of various installed apps. It is used to show release notes.", "fieldname": "last_known_versions", "fieldtype": "Text", "hidden": 1, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Last Known Versions", + "length": 0, + "no_copy": 0, "permlevel": 0, "precision": "", - "read_only": 1 + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 1, + "depends_on": "enabled", "fieldname": "third_party_authentication", "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Third Party Authentication", - "permlevel": 1 + "length": 0, + "no_copy": 0, + "permlevel": 1, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "fb_username", "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Facebook Username", + "length": 0, "no_copy": 1, "permlevel": 0, - "read_only": 1 + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "fb_userid", "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Facebook User ID", + "length": 0, "no_copy": 1, "permlevel": 0, - "read_only": 1 + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "google_userid", "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Google User ID", + "length": 0, "no_copy": 1, "permlevel": 0, - "read_only": 1 + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "column_break_49", "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "length": 0, + "no_copy": 0, "permlevel": 0, - "precision": "" + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "github_userid", "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Github User ID", + "length": 0, "no_copy": 1, "permlevel": 0, - "read_only": 1 + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "github_username", "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, "label": "Github Username", + "length": 0, "no_copy": 1, "permlevel": 0, - "read_only": 1 + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 } ], "hide_heading": 0, "hide_toolbar": 0, "icon": "icon-user", "idx": 1, + "in_create": 0, + "in_dialog": 0, + "is_submittable": 0, "issingle": 0, "istable": 0, "max_attachments": 5, - "modified": "2015-06-01 01:00:32.901851", + "menu_index": 0, + "modified": "2015-12-23 02:45:19.261689", "modified_by": "Administrator", "module": "Core", "name": "User", "owner": "Administrator", "permissions": [ { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, "create": 1, "delete": 1, "email": 1, + "export": 0, + "if_owner": 0, + "import": 0, "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "System Manager", + "set_user_permissions": 0, "share": 1, "submit": 0, "write": 1 }, { "amend": 0, + "apply_user_permissions": 0, "cancel": 0, "create": 0, "delete": 0, + "email": 0, + "export": 0, + "if_owner": 0, + "import": 0, "permlevel": 1, + "print": 0, "read": 1, "report": 1, "role": "System Manager", + "set_user_permissions": 0, + "share": 0, "submit": 0, "write": 1 } ], "read_only": 0, + "read_only_onload": 0, "search_fields": "first_name, last_name" } \ No newline at end of file diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index 415a6c71ac..269d71fb8d 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -10,6 +10,7 @@ from frappe.desk.notifications import clear_notifications from frappe.utils.user import get_system_managers import frappe.permissions import frappe.share +import re STANDARD_USERS = ("Guest", "Administrator") @@ -38,6 +39,8 @@ class User(Document): self.update_gravatar() self.ensure_unique_roles() self.remove_all_roles_for_guest() + self.validate_username() + if self.language == "Loading...": self.language = None @@ -85,7 +88,7 @@ class User(Document): self.share_with_self() clear_notifications(user=self.name) frappe.clear_cache(user=self.name) - self.send_password_notifcation(self.__new_password) + self.send_password_notification(self.__new_password) def share_with_self(self): if self.user_type=="System User": @@ -103,7 +106,7 @@ class User(Document): else: frappe.throw(_("Sorry! Sharing with Website User is prohibited.")) - def send_password_notifcation(self, new_password): + def send_password_notification(self, new_password): try: if self.in_insert: if self.name not in STANDARD_USERS: @@ -141,7 +144,7 @@ class User(Document): return frappe.db.sql("""select distinct user.name from tabUserRole user_role, tabUser user where user_role.role='System Manager' and user.docstatus<2 - and ifnull(user.enabled,0)=1 + and user.enabled=1 and user_role.parent = user.name and user_role.parent not in ('Administrator', %s) limit 1""", (self.name,)) @@ -164,7 +167,7 @@ class User(Document): link = get_url("/update-password?key=" + key) self.send_login_mail(_("Verify Your Account"), "templates/emails/new_user.html", - {"link": link}) + {"link": link, "site_url": get_url()}) def send_login_mail(self, subject, template, add_args): """send mail with login details""" @@ -296,6 +299,48 @@ class User(Document): else: exists.append(d.role) + def validate_username(self): + if not self.username and self.is_new() and self.first_name: + self.username = frappe.scrub(self.first_name) + + if not self.username: + return + + # strip space and @ + self.username = self.username.strip(" @") + + if self.username_exists(): + frappe.msgprint(_("Username {0} already exists").format(self.username)) + self.suggest_username() + self.username = "" + + # should be made up of characters, numbers and underscore only + if self.username and not re.match(r"^[\w]+$", self.username): + frappe.msgprint(_("Username should not contain any special characters other than letters, numbers and underscore")) + self.username = "" + + def suggest_username(self): + def _check_suggestion(suggestion): + if self.username != suggestion and not self.username_exists(suggestion): + return suggestion + + return None + + # @firstname + username = _check_suggestion(frappe.scrub(self.first_name)) + + if not username: + # @firstname_last_name + username = _check_suggestion(frappe.scrub("{0} {1}".format(self.first_name, self.last_name or ""))) + + if username: + frappe.msgprint(_("Suggested Username: {0}").format(username)) + + return username + + def username_exists(self, username=None): + return frappe.db.get_value("User", {"username": username or self.username, "name": ("!=", self.name)}) + @frappe.whitelist() def get_languages(): from frappe.translate import get_lang_dict @@ -397,7 +442,7 @@ def user_query(doctype, txt, searchfield, start, page_len, filters): txt = "%{}%".format(txt) return frappe.db.sql("""select name, concat_ws(' ', first_name, middle_name, last_name) from `tabUser` - where ifnull(enabled, 0)=1 + where enabled=1 and docstatus < 2 and name not in ({standard_users}) and user_type != 'Website User' @@ -437,7 +482,7 @@ def get_active_users(): return frappe.db.sql("""select count(*) from `tabUser` where enabled = 1 and user_type != 'Website User' and name not in ({}) - and hour(timediff(now(), last_login)) < 72""".format(", ".join(["%s"]*len(STANDARD_USERS))), STANDARD_USERS)[0][0] + and hour(timediff(now(), last_active)) < 72""".format(", ".join(["%s"]*len(STANDARD_USERS))), STANDARD_USERS)[0][0] def get_website_users(): """Returns total no. of website users""" @@ -448,7 +493,7 @@ def get_active_website_users(): """Returns No. of website users who logged in, in the last 3 days""" return frappe.db.sql("""select count(*) from `tabUser` where enabled = 1 and user_type = 'Website User' - and hour(timediff(now(), last_login)) < 72""")[0][0] + and hour(timediff(now(), last_active)) < 72""")[0][0] def get_permission_query_conditions(user): if user=="Administrator": @@ -490,3 +535,7 @@ def notifify_admin_access_to_system_manager(login_manager=None): frappe.sendmail(recipients=get_system_managers(), subject=_("Administrator Logged In"), message=message, bulk=True) +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""" + return re.findall(r'(?:[^\w]|^)@([\w]*)', txt) diff --git a/frappe/core/doctype/userrole/userrole.json b/frappe/core/doctype/userrole/userrole.json index 83c87c6b90..235fd25f13 100644 --- a/frappe/core/doctype/userrole/userrole.json +++ b/frappe/core/doctype/userrole/userrole.json @@ -1,36 +1,56 @@ { "allow_copy": 0, + "allow_import": 0, + "allow_rename": 0, "autoname": "hash", "creation": "2013-02-06 11:30:13", + "custom": 0, "docstatus": 0, "doctype": "DocType", "fields": [ { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "role", "fieldtype": "Link", "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, "in_list_view": 1, "label": "Role", + "length": 0, + "no_copy": 0, "oldfieldname": "role", "oldfieldtype": "Link", "options": "Role", "permlevel": 0, + "print_hide": 0, "print_width": "200px", + "read_only": 0, + "report_hide": 0, "reqd": 1, "search_index": 0, + "set_only_once": 0, + "unique": 0, "width": "200px" } ], "hide_heading": 0, "hide_toolbar": 0, "idx": 1, + "in_create": 0, + "in_dialog": 0, + "is_submittable": 0, "issingle": 0, "istable": 1, - "modified": "2015-02-19 01:07:02.561834", + "max_attachments": 0, + "modified": "2015-11-16 06:30:00.004591", "modified_by": "Administrator", "module": "Core", "name": "UserRole", "owner": "Administrator", "permissions": [], - "read_only": 0 + "read_only": 0, + "read_only_onload": 0 } \ No newline at end of file diff --git a/frappe/core/doctype/version/version.json b/frappe/core/doctype/version/version.json index b0f4a8586d..a86578db39 100644 --- a/frappe/core/doctype/version/version.json +++ b/frappe/core/doctype/version/version.json @@ -1,50 +1,119 @@ { + "allow_copy": 0, + "allow_import": 0, + "allow_rename": 0, "autoname": "_VER.######", "creation": "2014-02-20 17:22:37", + "custom": 0, "docstatus": 0, "doctype": "DocType", - "document_type": "Master", + "document_type": "Setup", "fields": [ { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "ref_doctype", "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, "in_list_view": 1, "label": "Ref DocType", + "length": 0, + "no_copy": 0, "options": "DocType", "permlevel": 0, - "reqd": 1 + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "docname", "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, "in_list_view": 1, "label": "Docname", + "length": 0, + "no_copy": 0, "permlevel": 0, - "reqd": 1 + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, "fieldname": "doclist_json", "fieldtype": "Code", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, "in_list_view": 1, "label": "Doclist JSON", + "length": 0, + "no_copy": 0, "permlevel": 0, - "reqd": 1 + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 } ], + "hide_heading": 0, + "hide_toolbar": 0, "icon": "icon-copy", "idx": 1, - "modified": "2014-08-05 01:23:37.541856", + "in_create": 0, + "in_dialog": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 0, + "max_attachments": 0, + "modified": "2015-11-16 06:30:00.036254", "modified_by": "Administrator", "module": "Core", "name": "Version", "owner": "Administrator", "permissions": [ { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "create": 0, + "delete": 0, + "email": 0, "export": 1, + "if_owner": 0, + "import": 0, "permlevel": 0, + "print": 0, "read": 1, "report": 1, - "role": "System Manager" + "role": "System Manager", + "set_user_permissions": 0, + "share": 0, + "submit": 0, + "write": 0 } - ] + ], + "read_only": 0, + "read_only_onload": 0 } \ No newline at end of file diff --git a/frappe/core/notifications.py b/frappe/core/notifications.py index 96ce779f25..266884915e 100644 --- a/frappe/core/notifications.py +++ b/frappe/core/notifications.py @@ -8,17 +8,12 @@ def get_notification_config(): return { "for_doctype": { "Scheduler Log": {"seen": 0}, + "Communication": {"status": "Open"}, + "ToDo": "frappe.core.notifications.get_things_todo", + "Event": "frappe.core.notifications.get_todays_events", + "Comment": "frappe.core.notifications.get_unread_messages", + "Error Snapshot": {"seen": 0, "parent_error_snapshot": None}, }, - "for_module_doctypes": { - "ToDo": "To Do", - "Event": "Calendar", - "Comment": "Messages" - }, - "for_module": { - "To Do": "frappe.core.notifications.get_things_todo", - "Calendar": "frappe.core.notifications.get_todays_events", - "Messages": "frappe.core.notifications.get_unread_messages" - } } def get_things_todo(): @@ -44,5 +39,5 @@ def get_unread_messages(): FROM `tabComment` WHERE comment_doctype IN ('My Company', 'Message') AND comment_docname = %s - AND ifnull(docstatus,0)=0 + AND docstatus=0 """, (frappe.session.user,))[0][0] diff --git a/frappe/core/page/data_import_tool/data_import_tool.js b/frappe/core/page/data_import_tool/data_import_tool.js index 5e349f7d6d..bc3917ee59 100644 --- a/frappe/core/page/data_import_tool/data_import_tool.js +++ b/frappe/core/page/data_import_tool/data_import_tool.js @@ -75,27 +75,61 @@ frappe.DataImportTool = Class.extend({ onerror: function(r) { me.onerror(r); }, + queued: function() { + // async, show queued + msg_dialog.clear(); + msgprint(__("Import Request Queued. This may take a few moments, please be patient.")); + }, + running: function() { + // update async status as running + msg_dialog.clear(); + msgprint(__("Importing...")); + me.write_messages([__("Importing")]); + me.has_progress = false; + }, + progress: function(data) { + // show callback if async + if(data.progress) { + frappe.hide_msgprint(true); + me.has_progress = true; + frappe.show_progress(__("Importing"), data.progress[0], + data.progress[1]); + } + }, callback: function(attachment, r) { if(r.message.error) { me.onerror(r); } else { - // replace links if error has occured + if(me.has_progress) { + frappe.show_progress(__("Importing"), 1, 1); + setTimeout(frappe.hide_progress, 1000); + } + r.messages = ["
    " + __("Import Successful!") + "
    "]. concat(r.message.messages) - me.write_messages(r); + me.write_messages(r.messages); } } }); + frappe.realtime.on("data_import_progress", function(data) { + if(data.progress) { + frappe.hide_msgprint(true); + me.has_progress = true; + frappe.show_progress(__("Importing"), data.progress[0], + data.progress[1]); + } + }) + }, - write_messages: function(r) { + write_messages: function(data) { this.page.main.find(".import-log").removeClass("hide"); var parent = this.page.main.find(".import-log-messages").empty(); // TODO render using template! - for (var i=0, l=r.messages.length; i

    ').html(frappe.markdown(v)).appendTo(parent); if(v.substr(0,5)=='Error') { $p.css('color', 'red'); @@ -127,7 +161,9 @@ frappe.DataImportTool = Class.extend({ r.messages.push("Please correct and import again."); - this.write_messages(r); + frappe.show_progress(__("Importing"), 1, 1); + + this.write_messages(r.messages); } } }); diff --git a/frappe/core/page/data_import_tool/data_import_tool.py b/frappe/core/page/data_import_tool/data_import_tool.py index 8c44c7088a..6b3ec60f72 100644 --- a/frappe/core/page/data_import_tool/data_import_tool.py +++ b/frappe/core/page/data_import_tool/data_import_tool.py @@ -30,12 +30,13 @@ def get_doctype_options(): doctype = frappe.form_dict['doctype'] return [doctype] + [d.options for d in frappe.get_meta(doctype).get_table_fields()] -def import_file_by_path(path, ignore_links=False, overwrite=False, submit=False): +def import_file_by_path(path, ignore_links=False, overwrite=False, submit=False, pre_process=None): from frappe.utils.csvutils import read_csv_content from frappe.core.page.data_import_tool.importer import upload print "Importing " + path with open(path, "r") as infile: - upload(rows = read_csv_content(infile.read()), ignore_links=ignore_links, overwrite=overwrite, submit_after_import=submit) + upload(rows = read_csv_content(infile.read()), ignore_links=ignore_links, overwrite=overwrite, + submit_after_import=submit, pre_process=pre_process) def export_csv(doctype, path): from frappe.core.page.data_import_tool.exporter import get_template @@ -80,7 +81,8 @@ def export_fixture(doctype, app): export_json(doctype, frappe.get_app_path(app, "fixtures", frappe.scrub(doctype) + ".json")) -def import_doc(path, overwrite=False, ignore_links=False, ignore_insert=False, insert=False, submit=False): +def import_doc(path, overwrite=False, ignore_links=False, ignore_insert=False, + insert=False, submit=False, pre_process=None): if os.path.isdir(path): files = [os.path.join(path, f) for f in os.listdir(path)] else: @@ -89,8 +91,8 @@ def import_doc(path, overwrite=False, ignore_links=False, ignore_insert=False, i for f in files: if f.endswith(".json"): frappe.flags.mute_emails = True - frappe.modules.import_file.import_file_by_path(f, data_import=True, force=True) + frappe.modules.import_file.import_file_by_path(f, data_import=True, force=True, pre_process=pre_process) frappe.flags.mute_emails = False elif f.endswith(".csv"): - import_file_by_path(f, ignore_links=ignore_links, overwrite=overwrite, submit=submit) + import_file_by_path(f, ignore_links=ignore_links, overwrite=overwrite, submit=submit, pre_process=pre_process) frappe.db.commit() diff --git a/frappe/core/page/data_import_tool/exporter.py b/frappe/core/page/data_import_tool/exporter.py index 96922e8d8f..510dbd1925 100644 --- a/frappe/core/page/data_import_tool/exporter.py +++ b/frappe/core/page/data_import_tool/exporter.py @@ -75,7 +75,7 @@ def get_template(doctype=None, parent_doctype=None, all_doctypes="No", with_data tablecolumns = filter(None, [(meta.get_field(f[0]) or None) for f in frappe.db.sql('desc `tab%s`' % dt)]) - tablecolumns.sort(lambda a, b: a.idx - b.idx) + tablecolumns.sort(lambda a, b: int(a.idx - b.idx)) if dt==doctype: column_start_end[dt] = frappe._dict({"start": 0}) diff --git a/frappe/core/page/data_import_tool/importer.py b/frappe/core/page/data_import_tool/importer.py index 1ad6256b27..1396174723 100644 --- a/frappe/core/page/data_import_tool/importer.py +++ b/frappe/core/page/data_import_tool/importer.py @@ -5,6 +5,7 @@ from __future__ import unicode_literals import frappe, json import frappe.permissions +import frappe.async from frappe import _ @@ -12,10 +13,12 @@ from frappe.utils.csvutils import getlink from frappe.utils.dateutils import parse_date from frappe.utils import cint, cstr, flt -from frappe.core.page.data_import_tool.data_import_tool import get_data_keys +from frappe.core.page.data_import_tool.data_import_tool import get_data_keys +#@frappe.async.handler @frappe.whitelist() -def upload(rows = None, submit_after_import=None, ignore_encoding_errors=False, overwrite=None, ignore_links=False): +def upload(rows = None, submit_after_import=None, ignore_encoding_errors=False, overwrite=None, + ignore_links=False, pre_process=None, via_console=False): """upload data""" frappe.flags.mute_emails = True # extra input params @@ -38,7 +41,7 @@ def upload(rows = None, submit_after_import=None, ignore_encoding_errors=False, max_rows = 5000 if not data: frappe.throw(_("No data found")) - elif len(data) > max_rows: + elif not via_console and len(data) > max_rows: frappe.throw(_("Only allowed {0} rows in one import").format(max_rows)) def get_start_row(): @@ -190,7 +193,21 @@ def upload(rows = None, submit_after_import=None, ignore_encoding_errors=False, delete_child_rows(data, doctype) ret = [] + + def log(msg): + if via_console: + print msg.encode('utf-8') + else: + ret.append(msg) + + def as_link(doctype, name): + if via_console: + return "{0}: {1}".format(doctype, name) + else: + return getlink(doctype, name) + error = False + total = len(data) for i, row in enumerate(data): # bypass empty rows if main_doc_empty(row): @@ -199,14 +216,20 @@ def upload(rows = None, submit_after_import=None, ignore_encoding_errors=False, row_idx = i + start_row doc = None - doc = get_doc(row_idx) + # publish task_update + frappe.publish_realtime("data_import_progress", {"progress": [i, total]}, + user=frappe.session.user, now=True) + try: - frappe.local.message_log = [] + doc = get_doc(row_idx) + if pre_process: + pre_process(doc) + if parentfield: parent = frappe.get_doc(parenttype, doc["parent"]) doc = parent.append(parentfield, doc) parent.save() - ret.append('Inserted row for %s at #%s' % (getlink(parenttype, + log('Inserted row for %s at #%s' % (as_link(parenttype, doc.parent), unicode(doc.idx))) else: if overwrite and doc["name"] and frappe.db.exists(doctype, doc["name"]): @@ -214,25 +237,27 @@ def upload(rows = None, submit_after_import=None, ignore_encoding_errors=False, original.update(doc) original.flags.ignore_links = ignore_links original.save() - ret.append('Updated row (#%d) %s' % (row_idx + 1, getlink(original.doctype, original.name))) + log('Updated row (#%d) %s' % (row_idx + 1, as_link(original.doctype, original.name))) doc = original else: doc = frappe.get_doc(doc) prepare_for_insert(doc) doc.flags.ignore_links = ignore_links doc.insert() - ret.append('Inserted row (#%d) %s' % (row_idx + 1, getlink(doc.doctype, doc.name))) + log('Inserted row (#%d) %s' % (row_idx + 1, as_link(doc.doctype, doc.name))) if submit_after_import: doc.submit() - ret.append('Submitted row (#%d) %s' % (row_idx + 1, getlink(doc.doctype, doc.name))) + log('Submitted row (#%d) %s' % (row_idx + 1, as_link(doc.doctype, doc.name))) except Exception, e: error = True if doc: frappe.errprint(doc if isinstance(doc, dict) else doc.as_dict()) err_msg = frappe.local.message_log and "\n\n".join(frappe.local.message_log) or cstr(e) - ret.append('Error for row (#%d) %s : %s' % (row_idx + 1, + log('Error for row (#%d) %s : %s' % (row_idx + 1, len(row)>1 and row[1] or "", err_msg)) frappe.errprint(frappe.get_traceback()) + finally: + frappe.local.message_log = [] if error: frappe.db.rollback() diff --git a/frappe/core/page/desktop/all_applications_dialog.html b/frappe/core/page/desktop/all_applications_dialog.html index 285459244f..5799928f2a 100644 --- a/frappe/core/page/desktop/all_applications_dialog.html +++ b/frappe/core/page/desktop/all_applications_dialog.html @@ -1,4 +1,9 @@ - +
    +
    +{% if (frappe.user.has_role("System Manager")) { %} +

    Install new applications +

    +{% } %}

    {%= __("Checked items will be shown on desktop") %}

    diff --git a/frappe/core/page/desktop/desktop.js b/frappe/core/page/desktop/desktop.js index 31837ba7f5..42be800ffc 100644 --- a/frappe/core/page/desktop/desktop.js +++ b/frappe/core/page/desktop/desktop.js @@ -2,10 +2,18 @@ frappe.provide('frappe.desktop'); frappe.pages['desktop'].on_page_load = function(wrapper) { // load desktop - frappe.desktop.set_background(); + if(!frappe.list_desktop) { + frappe.desktop.set_background(); + } frappe.desktop.refresh(wrapper); }; +frappe.pages['desktop'].on_page_show = function(wrapper) { + if(frappe.list_desktop) { + $("body").attr("data-route", "list-desktop"); + } +}; + $.extend(frappe.desktop, { refresh: function(wrapper) { if (wrapper) { @@ -20,7 +28,10 @@ $.extend(frappe.desktop, { var me = this; frappe.utils.set_title("Desktop"); - this.wrapper.html(frappe.render_template("desktop_icon_grid", { + var template = frappe.list_desktop ? "desktop_list_view" : "desktop_icon_grid"; + + + this.wrapper.html(frappe.render_template(template, { // all visible icons desktop_items: this.get_desktop_items(), @@ -28,7 +39,7 @@ $.extend(frappe.desktop, { user_desktop_items: this.get_user_desktop_items(), })); - this.setup_icon_click(); + this.setup_module_click(); // notifications this.show_pending_notifications(); @@ -96,29 +107,40 @@ $.extend(frappe.desktop, { return out; }, - setup_icon_click: function() { - this.wrapper.on("click", ".app-icon", function() { - var parent = $(this).parent(); - var link = parent.attr("data-link"); - if(link) { - if(link.substr(0, 1)==="/" || link.substr(0, 4)==="http") { - window.open(link, "_blank"); - } else { - frappe.set_route(link); - } - return false; + setup_module_click: function() { + var me = this; + + if(frappe.list_desktop) { + this.wrapper.on("click", ".desktop-list-item", function() { + me.open_module($(this)); + }); + } else { + this.wrapper.on("click", ".app-icon", function() { + me.open_module($(this).parent()); + }); + } + }, + + open_module: function(parent) { + var link = parent.attr("data-link"); + if(link) { + if(link.substr(0, 1)==="/" || link.substr(0, 4)==="http") { + window.open(link, "_blank"); } else { - module = frappe.get_module(parent.attr("data-name")); - if (module && module.onclick) { - module.onclick(); - return false; - } + frappe.set_route(link); } - }); + return false; + } else { + module = frappe.get_module(parent.attr("data-name")); + if (module && module.onclick) { + module.onclick(); + return false; + } + } }, make_sortable: function() { - if (frappe.dom.is_touchscreen()) { + if (frappe.dom.is_touchscreen() || frappe.list_desktop) { return; } @@ -215,10 +237,15 @@ $.extend(frappe.desktop, { sum = frappe.boot.notification_info.open_count_module[module]; } if (frappe.modules[module]) { - var notifier = $("#module-count-" + frappe.get_module(module)._id); + var notifier = $(".module-count-" + frappe.get_module(module)._id); if(notifier.length) { notifier.toggle(sum ? true : false); - notifier.find(".circle-text").html(sum || ""); + var circle = notifier.find(".circle-text"); + if(circle.length) { + circle.html(sum || ""); + } else { + notifier.html(sum); + } } } } diff --git a/frappe/core/page/desktop/desktop_list_view.html b/frappe/core/page/desktop/desktop_list_view.html new file mode 100644 index 0000000000..fc5f66f283 --- /dev/null +++ b/frappe/core/page/desktop/desktop_list_view.html @@ -0,0 +1,25 @@ +
    +
    +
    +
    + {% for (var i=0, l=desktop_items.length; i < l; i++) { + var module = frappe.get_module(desktop_items[i]); + if (!module || (user_desktop_items.indexOf(module.name)===-1 && !module.force_show) + || frappe.user.is_module_blocked(module.name)) { continue; } + %} +
    +

    + + {{ module._label }} +

    + +
    + {% } %} +
    +
    +
    +
    diff --git a/frappe/core/page/desktop/desktop_module_icon.html b/frappe/core/page/desktop/desktop_module_icon.html index 49ae8f8e75..f5d685aef8 100644 --- a/frappe/core/page/desktop/desktop_module_icon.html +++ b/frappe/core/page/desktop/desktop_module_icon.html @@ -1,8 +1,8 @@ -
    {%= app_icon %}
    -