@@ -4,23 +4,23 @@ include *.json | |||||
include *.md | include *.md | ||||
include *.py | include *.py | ||||
include *.txt | include *.txt | ||||
recursive-include webnotes *.css | |||||
recursive-include webnotes *.dat | |||||
recursive-include webnotes *.eot | |||||
recursive-include webnotes *.gif | |||||
recursive-include webnotes *.html | |||||
recursive-include webnotes *.jpg | |||||
recursive-include webnotes *.js | |||||
recursive-include webnotes *.json | |||||
recursive-include webnotes *.md | |||||
recursive-include webnotes *.otf | |||||
recursive-include webnotes *.png | |||||
recursive-include webnotes *.py | |||||
recursive-include webnotes *.sql | |||||
recursive-include webnotes *.svg | |||||
recursive-include webnotes *.swf | |||||
recursive-include webnotes *.ttf | |||||
recursive-include webnotes *.woff | |||||
recursive-include webnotes *.xml | |||||
recursive-include webnotes *.txt | |||||
recursive-include frappe *.css | |||||
recursive-include frappe *.dat | |||||
recursive-include frappe *.eot | |||||
recursive-include frappe *.gif | |||||
recursive-include frappe *.html | |||||
recursive-include frappe *.jpg | |||||
recursive-include frappe *.js | |||||
recursive-include frappe *.json | |||||
recursive-include frappe *.md | |||||
recursive-include frappe *.otf | |||||
recursive-include frappe *.png | |||||
recursive-include frappe *.py | |||||
recursive-include frappe *.sql | |||||
recursive-include frappe *.svg | |||||
recursive-include frappe *.swf | |||||
recursive-include frappe *.ttf | |||||
recursive-include frappe *.woff | |||||
recursive-include frappe *.xml | |||||
recursive-include frappe *.txt | |||||
recursive-exclude * *.pyc | recursive-exclude * *.pyc |
@@ -0,0 +1,607 @@ | |||||
# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors | |||||
# MIT License. See license.txt | |||||
""" | |||||
globals attached to frappe module | |||||
+ some utility functions that should probably be moved | |||||
""" | |||||
from __future__ import unicode_literals | |||||
from werkzeug.local import Local, release_local | |||||
from werkzeug.exceptions import NotFound | |||||
from MySQLdb import ProgrammingError as SQLError | |||||
import os, sys, importlib, inspect | |||||
import json | |||||
import semantic_version | |||||
from frappe.core.doctype.print_format.print_format import get_html as get_print_html | |||||
local = Local() | |||||
class _dict(dict): | |||||
"""dict like object that exposes keys as attributes""" | |||||
def __getattr__(self, key): | |||||
ret = self.get(key) | |||||
if not ret and key.startswith("__"): | |||||
raise AttributeError() | |||||
return ret | |||||
def __setattr__(self, key, value): | |||||
self[key] = value | |||||
def __getstate__(self): | |||||
return self | |||||
def __setstate__(self, d): | |||||
self.update(d) | |||||
def update(self, d): | |||||
"""update and return self -- the missing dict feature in python""" | |||||
super(_dict, self).update(d) | |||||
return self | |||||
def copy(self): | |||||
return _dict(dict(self).copy()) | |||||
def __getattr__(self, key): | |||||
return local.get("key", None) | |||||
def _(msg): | |||||
"""translate object in current lang, if exists""" | |||||
if local.lang == "en": | |||||
return msg | |||||
from frappe.translate import get_full_dict | |||||
return get_full_dict(local.lang).get(msg, msg) | |||||
def get_lang_dict(fortype, name=None): | |||||
if local.lang=="en": | |||||
return {} | |||||
from frappe.translate import get_dict | |||||
return get_dict(fortype, name) | |||||
def set_user_lang(user, user_language=None): | |||||
from frappe.translate import get_lang_dict | |||||
if not user_language: | |||||
user_language = conn.get_value("Profile", user, "language") | |||||
if user_language: | |||||
lang_dict = get_lang_dict() | |||||
if user_language in lang_dict: | |||||
local.lang = lang_dict[user_language] | |||||
# local-globals | |||||
conn = local("conn") | |||||
conf = local("conf") | |||||
form = form_dict = local("form_dict") | |||||
request = local("request") | |||||
request_method = local("request_method") | |||||
response = local("response") | |||||
_response = local("_response") | |||||
session = local("session") | |||||
user = local("user") | |||||
flags = local("flags") | |||||
restrictions = local("restrictions") | |||||
error_log = local("error_log") | |||||
debug_log = local("debug_log") | |||||
message_log = local("message_log") | |||||
lang = local("lang") | |||||
def init(site, sites_path=None): | |||||
if getattr(local, "initialised", None): | |||||
return | |||||
if not sites_path: | |||||
sites_path = '.' | |||||
local.error_log = [] | |||||
local.site = site | |||||
local.sites_path = sites_path | |||||
local.site_path = os.path.join(sites_path, site) | |||||
local.message_log = [] | |||||
local.debug_log = [] | |||||
local.response = _dict({}) | |||||
local.lang = "en" | |||||
local.request_method = request.method if request else None | |||||
local.conf = _dict(get_site_config()) | |||||
local.initialised = True | |||||
local.flags = _dict({}) | |||||
local.rollback_observers = [] | |||||
local.module_app = None | |||||
local.app_modules = None | |||||
local.user = None | |||||
local.restrictions = None | |||||
local.user_perms = {} | |||||
local.test_objects = {} | |||||
local.jenv = None | |||||
local.jloader =None | |||||
setup_module_map() | |||||
def get_site_config(): | |||||
config = {} | |||||
sites_config_filepath = os.path.join(local.sites_path, "site_config.json") | |||||
site_config_filepath = os.path.join(local.site_path, "site_config.json") | |||||
if os.path.exists(sites_config_filepath): | |||||
config = get_file_json(sites_config_filepath) | |||||
if os.path.exists(site_config_filepath): | |||||
config.update(get_file_json(site_config_filepath)) | |||||
return _dict(config) | |||||
def destroy(): | |||||
"""closes connection and releases werkzeug local""" | |||||
if conn: | |||||
conn.close() | |||||
release_local(local) | |||||
_memc = None | |||||
# memcache | |||||
def cache(): | |||||
global _memc | |||||
if not _memc: | |||||
from frappe.memc import MClient | |||||
_memc = MClient(['localhost:11211']) | |||||
return _memc | |||||
class DuplicateEntryError(Exception): pass | |||||
class ValidationError(Exception): pass | |||||
class AuthenticationError(Exception): pass | |||||
class PermissionError(Exception): pass | |||||
class DataError(Exception): pass | |||||
class UnknownDomainError(Exception): pass | |||||
class SessionStopped(Exception): pass | |||||
class MappingMismatchError(ValidationError): pass | |||||
class InvalidStatusError(ValidationError): pass | |||||
class DoesNotExistError(ValidationError): pass | |||||
class MandatoryError(ValidationError): pass | |||||
class InvalidSignatureError(ValidationError): pass | |||||
class RateLimitExceededError(ValidationError): pass | |||||
class OutgoingEmailError(Exception): pass | |||||
def get_traceback(): | |||||
import utils | |||||
return utils.get_traceback() | |||||
def errprint(msg): | |||||
from utils import cstr | |||||
if not request: | |||||
print cstr(msg) | |||||
error_log.append(cstr(msg)) | |||||
def log(msg): | |||||
if not request: | |||||
if conf.get("logging") or False: | |||||
print repr(msg) | |||||
from utils import cstr | |||||
debug_log.append(cstr(msg)) | |||||
def msgprint(msg, small=0, raise_exception=0, as_table=False): | |||||
def _raise_exception(): | |||||
if raise_exception: | |||||
if flags.rollback_on_exception: | |||||
conn.rollback() | |||||
import inspect | |||||
if inspect.isclass(raise_exception) and issubclass(raise_exception, Exception): | |||||
raise raise_exception, msg | |||||
else: | |||||
raise ValidationError, msg | |||||
if flags.mute_messages: | |||||
_raise_exception() | |||||
return | |||||
from utils import cstr | |||||
if as_table and type(msg) in (list, tuple): | |||||
msg = '<table border="1px" style="border-collapse: collapse" cellpadding="2px">' + ''.join(['<tr>'+''.join(['<td>%s</td>' % c for c in r])+'</tr>' for r in msg]) + '</table>' | |||||
if flags.print_messages: | |||||
print "Message: " + repr(msg) | |||||
message_log.append((small and '__small:' or '')+cstr(msg or '')) | |||||
_raise_exception() | |||||
def throw(msg, exc=ValidationError): | |||||
msgprint(msg, raise_exception=exc) | |||||
def create_folder(path): | |||||
if not os.path.exists(path): os.makedirs(path) | |||||
def connect(site=None, db_name=None): | |||||
from db import Database | |||||
if site: | |||||
init(site) | |||||
local.conn = Database(user=db_name or local.conf.db_name) | |||||
local.response = _dict() | |||||
local.form_dict = _dict() | |||||
local.session = _dict() | |||||
set_user("Administrator") | |||||
def set_user(username): | |||||
import frappe.profile | |||||
local.session["user"] = username | |||||
local.user = frappe.profile.Profile(username) | |||||
local.restrictions = None | |||||
local.user_perms = {} | |||||
def get_request_header(key, default=None): | |||||
return request.headers.get(key, default) | |||||
def sendmail(recipients=[], sender="", subject="No Subject", message="No Message", as_markdown=False): | |||||
import frappe.utils.email_lib | |||||
if as_markdown: | |||||
frappe.utils.email_lib.sendmail_md(recipients, sender=sender, subject=subject, msg=message) | |||||
else: | |||||
frappe.utils.email_lib.sendmail(recipients, sender=sender, subject=subject, msg=message) | |||||
logger = None | |||||
whitelisted = [] | |||||
guest_methods = [] | |||||
def whitelist(allow_guest=False): | |||||
""" | |||||
decorator for whitelisting a function | |||||
Note: if the function is allowed to be accessed by a guest user, | |||||
it must explicitly be marked as allow_guest=True | |||||
for specific roles, set allow_roles = ['Administrator'] etc. | |||||
""" | |||||
def innerfn(fn): | |||||
global whitelisted, guest_methods | |||||
whitelisted.append(fn) | |||||
if allow_guest: | |||||
guest_methods.append(fn) | |||||
return fn | |||||
return innerfn | |||||
def only_for(roles): | |||||
if not isinstance(roles, (tuple, list)): | |||||
roles = (roles,) | |||||
roles = set(roles) | |||||
myroles = set(get_roles()) | |||||
if not roles.intersection(myroles): | |||||
raise PermissionError | |||||
def clear_cache(user=None, doctype=None): | |||||
"""clear cache""" | |||||
import frappe.sessions | |||||
if doctype: | |||||
import frappe.model.doctype | |||||
frappe.model.doctype.clear_cache(doctype) | |||||
reset_metadata_version() | |||||
elif user: | |||||
frappe.sessions.clear_cache(user) | |||||
else: # everything | |||||
import translate | |||||
frappe.sessions.clear_cache() | |||||
translate.clear_cache() | |||||
reset_metadata_version() | |||||
def get_roles(username=None): | |||||
import frappe.profile | |||||
if not local.session: | |||||
return ["Guest"] | |||||
elif not username or username==local.session.user: | |||||
return local.user.get_roles() | |||||
else: | |||||
return frappe.profile.Profile(username).get_roles() | |||||
def has_permission(doctype, ptype="read", refdoc=None): | |||||
import frappe.permissions | |||||
return frappe.permissions.has_permission(doctype, ptype, refdoc) | |||||
def clear_perms(doctype): | |||||
conn.sql("""delete from tabDocPerm where parent=%s""", doctype) | |||||
def reset_perms(doctype): | |||||
clear_perms(doctype) | |||||
reload_doc(conn.get_value("DocType", doctype, "module"), | |||||
"DocType", doctype, force=True) | |||||
def generate_hash(txt=None): | |||||
"""Generates random hash for session id""" | |||||
import hashlib, time | |||||
return hashlib.sha224((txt or "") + str(time.time())).hexdigest() | |||||
def reset_metadata_version(): | |||||
v = generate_hash() | |||||
cache().set_value("metadata_version", v) | |||||
return v | |||||
def get_obj(dt = None, dn = None, doc=None, doclist=None, with_children = True): | |||||
from frappe.model.code import get_obj | |||||
return get_obj(dt, dn, doc, doclist, with_children) | |||||
def doc(doctype=None, name=None, fielddata=None): | |||||
from frappe.model.doc import Document | |||||
return Document(doctype, name, fielddata) | |||||
def new_doc(doctype, parent_doc=None, parentfield=None): | |||||
from frappe.model.create_new import get_new_doc | |||||
return get_new_doc(doctype, parent_doc, parentfield) | |||||
def new_bean(doctype): | |||||
from frappe.model.create_new import get_new_doc | |||||
return bean([get_new_doc(doctype)]) | |||||
def doclist(lst=None): | |||||
from frappe.model.doclist import DocList | |||||
return DocList(lst) | |||||
def bean(doctype=None, name=None, copy=None): | |||||
"""return an instance of the object, wrapped as a Bean (frappe.model.bean)""" | |||||
from frappe.model.bean import Bean | |||||
if copy: | |||||
return Bean(copy_doclist(copy)) | |||||
else: | |||||
return Bean(doctype, name) | |||||
def set_value(doctype, docname, fieldname, value): | |||||
import frappe.client | |||||
return frappe.client.set_value(doctype, docname, fieldname, value) | |||||
def get_doclist(doctype, name=None): | |||||
return bean(doctype, name).doclist | |||||
def get_doctype(doctype, processed=False): | |||||
import frappe.model.doctype | |||||
return frappe.model.doctype.get(doctype, processed) | |||||
def delete_doc(doctype=None, name=None, doclist = None, force=0, ignore_doctypes=None, | |||||
for_reload=False, ignore_permissions=False): | |||||
import frappe.model.delete_doc | |||||
if not ignore_doctypes: | |||||
ignore_doctypes = [] | |||||
if isinstance(name, list): | |||||
for n in name: | |||||
frappe.model.delete_doc.delete_doc(doctype, n, doclist, force, ignore_doctypes, | |||||
for_reload, ignore_permissions) | |||||
else: | |||||
frappe.model.delete_doc.delete_doc(doctype, name, doclist, force, ignore_doctypes, | |||||
for_reload, ignore_permissions) | |||||
def reload_doc(module, dt=None, dn=None, force=False): | |||||
import frappe.modules | |||||
return frappe.modules.reload_doc(module, dt, dn, force=force) | |||||
def rename_doc(doctype, old, new, debug=0, force=False, merge=False, ignore_permissions=False): | |||||
from frappe.model.rename_doc import rename_doc | |||||
return rename_doc(doctype, old, new, force=force, merge=merge, ignore_permissions=ignore_permissions) | |||||
def insert(doclist): | |||||
import frappe.model | |||||
return frappe.model.insert(doclist) | |||||
def get_module(modulename): | |||||
return importlib.import_module(modulename) | |||||
def scrub(txt): | |||||
return txt.replace(' ','_').replace('-', '_').replace('/', '_').lower() | |||||
def get_module_path(module, *joins): | |||||
module = scrub(module) | |||||
return get_pymodule_path(local.module_app[module] + "." + module, *joins) | |||||
def get_app_path(app_name, *joins): | |||||
return get_pymodule_path(app_name, *joins) | |||||
def get_pymodule_path(modulename, *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): | |||||
return get_file_items(os.path.join(os.path.dirname(get_module(app_name).__file__), "modules.txt")) | |||||
def get_all_apps(with_frappe=False): | |||||
apps = get_file_items(os.path.join(local.sites_path, "apps.txt")) \ | |||||
+ get_file_items(os.path.join(local.site_path, "apps.txt")) | |||||
if with_frappe: | |||||
apps.insert(0, 'frappe') | |||||
return apps | |||||
def get_installed_apps(): | |||||
if flags.in_install_db: | |||||
return [] | |||||
installed = json.loads(conn.get_global("installed_apps") or "[]") | |||||
return installed | |||||
def get_hooks(hook=None, app_name=None): | |||||
def load_app_hooks(app_name=None): | |||||
hooks = {} | |||||
for app in [app_name] if app_name else get_installed_apps(): | |||||
for item in get_file_items(get_pymodule_path(app, "hooks.txt")): | |||||
key, value = item.split("=", 1) | |||||
key, value = key.strip(), value.strip() | |||||
hooks.setdefault(key, []) | |||||
hooks[key].append(value) | |||||
return hooks | |||||
if app_name: | |||||
hooks = _dict(load_app_hooks(app_name)) | |||||
else: | |||||
hooks = _dict(cache().get_value("app_hooks", load_app_hooks)) | |||||
if hook: | |||||
return hooks.get(hook) or [] | |||||
else: | |||||
return hooks | |||||
def setup_module_map(): | |||||
_cache = cache() | |||||
if conf.db_name: | |||||
local.app_modules = _cache.get_value("app_modules") | |||||
local.module_app = _cache.get_value("module_app") | |||||
if not local.app_modules: | |||||
local.module_app, local.app_modules = {}, {} | |||||
for app in get_all_apps(True): | |||||
local.app_modules.setdefault(app, []) | |||||
for module in get_module_list(app): | |||||
local.module_app[module] = app | |||||
local.app_modules[app].append(module) | |||||
if conf.db_name: | |||||
_cache.set_value("app_modules", local.app_modules) | |||||
_cache.set_value("module_app", local.module_app) | |||||
def get_file_items(path): | |||||
content = read_file(path) | |||||
if content: | |||||
return [p.strip() for p in content.splitlines() if p.strip() and not p.startswith("#")] | |||||
else: | |||||
return [] | |||||
def get_file_json(path): | |||||
with open(path, 'r') as f: | |||||
return json.load(f) | |||||
def read_file(path): | |||||
if os.path.exists(path): | |||||
with open(path, "r") as f: | |||||
return unicode(f.read(), encoding="utf-8") | |||||
else: | |||||
return None | |||||
def get_attr(method_string): | |||||
modulename = '.'.join(method_string.split('.')[:-1]) | |||||
methodname = method_string.split('.')[-1] | |||||
return getattr(get_module(modulename), methodname) | |||||
def call(fn, *args, **kwargs): | |||||
if hasattr(fn, 'fnargs'): | |||||
fnargs = fn.fnargs | |||||
else: | |||||
fnargs, varargs, varkw, defaults = inspect.getargspec(fn) | |||||
newargs = {} | |||||
for a in fnargs: | |||||
if a in kwargs: | |||||
newargs[a] = kwargs.get(a) | |||||
return fn(*args, **newargs) | |||||
def make_property_setter(args): | |||||
args = _dict(args) | |||||
bean([{ | |||||
'doctype': "Property Setter", | |||||
'doctype_or_field': args.doctype_or_field or "DocField", | |||||
'doc_type': args.doctype, | |||||
'field_name': args.fieldname, | |||||
'property': args.property, | |||||
'value': args.value, | |||||
'property_type': args.property_type or "Data", | |||||
'__islocal': 1 | |||||
}]).save() | |||||
def get_application_home_page(user='Guest'): | |||||
"""get home page for user""" | |||||
hpl = conn.sql("""select home_page | |||||
from `tabDefault Home Page` | |||||
where parent='Control Panel' | |||||
and role in ('%s') order by idx asc limit 1""" % "', '".join(get_roles(user))) | |||||
if hpl: | |||||
return hpl[0][0] | |||||
else: | |||||
return conn.get_value("Control Panel", None, "home_page") | |||||
def copy_doclist(in_doclist): | |||||
new_doclist = [] | |||||
parent_doc = None | |||||
for i, d in enumerate(in_doclist): | |||||
is_dict = False | |||||
if isinstance(d, dict): | |||||
is_dict = True | |||||
values = _dict(d.copy()) | |||||
else: | |||||
values = _dict(d.fields.copy()) | |||||
newd = new_doc(values.doctype, parent_doc=(None if i==0 else parent_doc), parentfield=values.parentfield) | |||||
newd.fields.update(values) | |||||
if i==0: | |||||
parent_doc = newd | |||||
new_doclist.append(newd.fields if is_dict else newd) | |||||
return doclist(new_doclist) | |||||
def compare(val1, condition, val2): | |||||
import frappe.utils | |||||
return frappe.utils.compare(val1, condition, val2) | |||||
def repsond_as_web_page(title, html): | |||||
local.message_title = title | |||||
local.message = "<h3>" + title + "</h3>" + html | |||||
local.response['type'] = 'page' | |||||
local.response['page_name'] = 'message.html' | |||||
return obj | |||||
def build_match_conditions(doctype, fields=None, as_condition=True): | |||||
import frappe.widgets.reportview | |||||
return frappe.widgets.reportview.build_match_conditions(doctype, fields, as_condition) | |||||
def get_list(doctype, filters=None, fields=None, docstatus=None, | |||||
group_by=None, order_by=None, limit_start=0, limit_page_length=None, | |||||
as_list=False, debug=False): | |||||
import frappe.widgets.reportview | |||||
return frappe.widgets.reportview.execute(doctype, filters=filters, fields=fields, docstatus=docstatus, | |||||
group_by=group_by, order_by=order_by, limit_start=limit_start, limit_page_length=limit_page_length, | |||||
as_list=as_list, debug=debug) | |||||
def get_jenv(): | |||||
if not local.jenv: | |||||
from jinja2 import Environment, DebugUndefined | |||||
import frappe.utils | |||||
# frappe will be loaded last, so app templates will get precedence | |||||
jenv = Environment(loader = get_jloader(), undefined=DebugUndefined) | |||||
set_filters(jenv) | |||||
jenv.globals.update({ | |||||
"frappe": sys.modules[__name__], | |||||
"frappe.utils": frappe.utils, | |||||
"_": _ | |||||
}) | |||||
local.jenv = jenv | |||||
return local.jenv | |||||
def get_jloader(): | |||||
if not local.jloader: | |||||
from jinja2 import ChoiceLoader, PackageLoader | |||||
apps = get_installed_apps() | |||||
apps.remove("frappe") | |||||
local.jloader = ChoiceLoader([PackageLoader(app, ".") \ | |||||
for app in apps + ["frappe"]]) | |||||
return local.jloader | |||||
def set_filters(jenv): | |||||
from frappe.utils import global_date_format | |||||
from frappe.website.utils import get_hex_shade | |||||
from markdown2 import markdown | |||||
from json import dumps | |||||
jenv.filters["global_date_format"] = global_date_format | |||||
jenv.filters["markdown"] = markdown | |||||
jenv.filters["json"] = dumps | |||||
jenv.filters["get_hex_shade"] = get_hex_shade | |||||
# load jenv_filters from hooks.txt | |||||
for app in get_all_apps(True): | |||||
for jenv_filter in (get_hooks(app_name=app).jenv_filter or []): | |||||
filter_name, filter_function = jenv_filter.split(":") | |||||
jenv.filters[filter_name] = get_attr(filter_function) | |||||
def get_template(path): | |||||
return get_jenv().get_template(path) |
@@ -0,0 +1,97 @@ | |||||
# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors | |||||
# MIT License. See license.txt | |||||
import frappe | |||||
import frappe.handler | |||||
import frappe.client | |||||
import frappe.widgets.reportview | |||||
from frappe.utils.response import build_response, report_error | |||||
def handle(): | |||||
""" | |||||
/api/method/{methodname} will call a whitelisted method | |||||
/api/resource/{doctype} will query a table | |||||
examples: | |||||
?fields=["name", "owner"] | |||||
?filters=[["Task", "name", "like", "%005"]] | |||||
?limit_start=0 | |||||
?limit_page_length=20 | |||||
/api/resource/{doctype}/{name} will point to a resource | |||||
GET will return doclist | |||||
POST will insert | |||||
PUT will update | |||||
DELETE will delete | |||||
/api/resource/{doctype}/{name}?run_method={method} will run a whitelisted controller method | |||||
""" | |||||
parts = frappe.request.path[1:].split("/") | |||||
call = doctype = name = None | |||||
if len(parts) > 1: | |||||
call = parts[1] | |||||
if len(parts) > 2: | |||||
doctype = parts[2] | |||||
if len(parts) > 3: | |||||
name = parts[3] | |||||
try: | |||||
if call=="method": | |||||
frappe.local.form_dict.cmd = doctype | |||||
frappe.handler.handle() | |||||
return | |||||
elif call=="resource": | |||||
if "run_method" in frappe.local.form_dict: | |||||
bean = frappe.bean(doctype, name) | |||||
if frappe.local.request.method=="GET": | |||||
if not bean.has_permission("read"): | |||||
frappe.throw("No Permission", frappe.PermissionError) | |||||
bean.run_method(frappe.local.form_dict.run_method, **frappe.local.form_dict) | |||||
if frappe.local.request.method=="POST": | |||||
if not bean.has_permission("write"): | |||||
frappe.throw("No Permission", frappe.PermissionError) | |||||
bean.run_method(frappe.local.form_dict.run_method, **frappe.local.form_dict) | |||||
frappe.conn.commit() | |||||
else: | |||||
if name: | |||||
if frappe.local.request.method=="GET": | |||||
frappe.local.response.update({ | |||||
"doclist": frappe.client.get(doctype, | |||||
name)}) | |||||
if frappe.local.request.method=="POST": | |||||
frappe.local.response.update({ | |||||
"doclist": frappe.client.insert(frappe.local.form_dict.doclist)}) | |||||
frappe.conn.commit() | |||||
if frappe.local.request.method=="PUT": | |||||
frappe.local.response.update({ | |||||
"doclist":frappe.client.save(frappe.local.form_dict.doclist)}) | |||||
frappe.conn.commit() | |||||
if frappe.local.request.method=="DELETE": | |||||
frappe.client.delete(doctype, name) | |||||
frappe.local.response.message = "ok" | |||||
elif doctype: | |||||
if frappe.local.request.method=="GET": | |||||
frappe.local.response.update({ | |||||
"data": frappe.call(frappe.widgets.reportview.execute, | |||||
doctype, **frappe.local.form_dict)}) | |||||
else: | |||||
raise frappe.DoesNotExistError | |||||
else: | |||||
raise frappe.DoesNotExistError | |||||
except frappe.DoesNotExistError, e: | |||||
report_error(404) | |||||
except Exception, e: | |||||
report_error(500) | |||||
build_response() |
@@ -0,0 +1,102 @@ | |||||
# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors | |||||
# MIT License. See license.txt | |||||
import sys, os | |||||
import json | |||||
from werkzeug.wrappers import Request, Response | |||||
from werkzeug.local import LocalManager | |||||
from werkzeug.wsgi import SharedDataMiddleware | |||||
from werkzeug.exceptions import HTTPException, NotFound | |||||
from werkzeug.contrib.profiler import ProfilerMiddleware | |||||
import mimetypes | |||||
import frappe | |||||
import frappe.handler | |||||
import frappe.auth | |||||
import frappe.api | |||||
import frappe.website.render | |||||
from frappe.utils import get_site_name | |||||
local_manager = LocalManager([frappe.local]) | |||||
_site = None | |||||
def handle_session_stopped(): | |||||
res = Response("""<html> | |||||
<body style="background-color: #EEE;"> | |||||
<h3 style="width: 900px; background-color: #FFF; border: 2px solid #AAA; padding: 20px; font-family: Arial; margin: 20px auto"> | |||||
Updating. | |||||
We will be back in a few moments... | |||||
</h3> | |||||
</body> | |||||
</html>""") | |||||
res.status_code = 503 | |||||
res.content_type = 'text/html' | |||||
return res | |||||
@Request.application | |||||
def application(request): | |||||
frappe.local.request = request | |||||
try: | |||||
site = _site or get_site_name(request.host) | |||||
frappe.init(site=site) | |||||
if not frappe.local.conf: | |||||
# site does not exist | |||||
raise NotFound | |||||
frappe.local.form_dict = frappe._dict({ k:v[0] if isinstance(v, (list, tuple)) else v \ | |||||
for k, v in (request.form or request.args).iteritems() }) | |||||
frappe.local._response = Response() | |||||
frappe.http_request = frappe.auth.HTTPRequest() | |||||
if frappe.local.form_dict.cmd: | |||||
frappe.handler.handle() | |||||
elif frappe.request.path.startswith("/api/"): | |||||
frappe.api.handle() | |||||
elif frappe.local.request.method in ('GET', 'HEAD'): | |||||
frappe.website.render.render(frappe.request.path[1:]) | |||||
else: | |||||
raise NotFound | |||||
except HTTPException, e: | |||||
return e | |||||
except frappe.AuthenticationError, e: | |||||
frappe._response.status_code=401 | |||||
except frappe.SessionStopped, e: | |||||
frappe.local._response = handle_session_stopped() | |||||
finally: | |||||
if frappe.conn: | |||||
frappe.conn.close() | |||||
return frappe.local._response | |||||
application = local_manager.make_middleware(application) | |||||
def serve(port=8000, profile=False, site=None): | |||||
global application, _site | |||||
_site = site | |||||
from werkzeug.serving import run_simple | |||||
if profile: | |||||
application = ProfilerMiddleware(application) | |||||
if not os.environ.get('NO_STATICS'): | |||||
application = SharedDataMiddleware(application, { | |||||
'/assets': 'assets', | |||||
}) | |||||
if site: | |||||
application = SharedDataMiddleware(application, { | |||||
'/files': os.path.join(site, 'public', 'files') | |||||
}) | |||||
run_simple('0.0.0.0', int(port), application, use_reloader=True, | |||||
use_debugger=True, use_evalex=True) |
@@ -0,0 +1,262 @@ | |||||
# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors | |||||
# MIT License. See license.txt | |||||
from __future__ import unicode_literals | |||||
import frappe | |||||
import frappe.db | |||||
import frappe.utils | |||||
import frappe.profile | |||||
from frappe import conf | |||||
from frappe.sessions import Session | |||||
class HTTPRequest: | |||||
def __init__(self): | |||||
# Get Environment variables | |||||
self.domain = frappe.request.host | |||||
if self.domain and self.domain.startswith('www.'): | |||||
self.domain = self.domain[4:] | |||||
# language | |||||
self.set_lang(frappe.get_request_header('HTTP_ACCEPT_LANGUAGE')) | |||||
# 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() | |||||
# check status | |||||
if frappe.conn.get_global("__session_status")=='stop': | |||||
frappe.msgprint(frappe.conn.get_global("__session_status_message")) | |||||
raise frappe.SessionStopped('Session Stopped') | |||||
# load profile | |||||
self.setup_profile() | |||||
# run login triggers | |||||
if frappe.form_dict.get('cmd')=='login': | |||||
frappe.local.login_manager.run_trigger('on_session_creation') | |||||
def set_lang(self, lang): | |||||
import translate | |||||
lang_list = translate.get_all_languages() or [] | |||||
if not lang: | |||||
return | |||||
if ";" in lang: # not considering weightage | |||||
lang = lang.split(";")[0] | |||||
if "," in lang: | |||||
lang = lang.split(",") | |||||
else: | |||||
lang = [lang] | |||||
for l in lang: | |||||
code = l.strip() | |||||
if code in lang_list: | |||||
frappe.local.lang = code | |||||
return | |||||
# check if parent language (pt) is setup, if variant (pt-BR) | |||||
if "-" in code: | |||||
code = code.split("-")[0] | |||||
if code in lang_list: | |||||
frappe.local.lang = code | |||||
return | |||||
def setup_profile(self): | |||||
frappe.local.user = frappe.profile.Profile() | |||||
def get_db_name(self): | |||||
"""get database name from conf""" | |||||
return conf.db_name | |||||
def connect(self, ac_name = None): | |||||
"""connect to db, from ac_name or db_name""" | |||||
frappe.local.conn = frappe.db.Database(user = self.get_db_name(), \ | |||||
password = getattr(conf,'db_password', '')) | |||||
class LoginManager: | |||||
def __init__(self): | |||||
self.user = None | |||||
if frappe.local.form_dict.get('cmd')=='login' or frappe.local.request.path=="/api/method/login": | |||||
self.login() | |||||
else: | |||||
self.make_session(resume=True) | |||||
def login(self): | |||||
# clear cache | |||||
frappe.clear_cache(user = frappe.form_dict.get('usr')) | |||||
self.authenticate() | |||||
self.post_login() | |||||
def post_login(self): | |||||
self.run_trigger('on_login') | |||||
self.validate_ip_address() | |||||
self.validate_hour() | |||||
self.make_session() | |||||
self.set_user_info() | |||||
def set_user_info(self): | |||||
info = frappe.conn.get_value("Profile", self.user, | |||||
["user_type", "first_name", "last_name", "user_image"], as_dict=1) | |||||
if info.user_type=="Website User": | |||||
frappe.local._response.set_cookie("system_user", "no") | |||||
frappe.local.response["message"] = "No App" | |||||
else: | |||||
frappe.local._response.set_cookie("system_user", "yes") | |||||
frappe.local.response['message'] = 'Logged In' | |||||
full_name = " ".join(filter(None, [info.first_name, info.last_name])) | |||||
frappe.response["full_name"] = full_name | |||||
frappe._response.set_cookie("full_name", full_name) | |||||
frappe._response.set_cookie("user_id", self.user) | |||||
frappe._response.set_cookie("user_image", info.user_image or "") | |||||
def make_session(self, resume=False): | |||||
# start session | |||||
frappe.local.session_obj = Session(user=self.user, resume=resume) | |||||
# reset user if changed to Guest | |||||
self.user = frappe.local.session_obj.user | |||||
frappe.local.session = frappe.local.session_obj.data | |||||
def authenticate(self, user=None, pwd=None): | |||||
if not (user and pwd): | |||||
user, pwd = frappe.form_dict.get('usr'), frappe.form_dict.get('pwd') | |||||
if not (user and pwd): | |||||
self.fail('Incomplete login details') | |||||
self.check_if_enabled(user) | |||||
self.user = self.check_password(user, pwd) | |||||
def check_if_enabled(self, user): | |||||
"""raise exception if user not enabled""" | |||||
from frappe.utils import cint | |||||
if user=='Administrator': return | |||||
if not cint(frappe.conn.get_value('Profile', user, 'enabled')): | |||||
self.fail('User disabled or missing') | |||||
def check_password(self, user, pwd): | |||||
"""check password""" | |||||
user = frappe.conn.sql("""select `user` from __Auth where `user`=%s | |||||
and `password`=password(%s)""", (user, pwd)) | |||||
if not user: | |||||
self.fail('Incorrect password') | |||||
else: | |||||
return user[0][0] # in correct case | |||||
def fail(self, message): | |||||
frappe.local.response['message'] = message | |||||
raise frappe.AuthenticationError | |||||
def run_trigger(self, method='on_login'): | |||||
for method in frappe.get_hooks().get("method", []): | |||||
frappe.get_attr(method)(self) | |||||
def validate_ip_address(self): | |||||
"""check if IP Address is valid""" | |||||
ip_list = frappe.conn.get_value('Profile', self.user, 'restrict_ip', ignore=True) | |||||
if not ip_list: | |||||
return | |||||
ip_list = ip_list.replace(",", "\n").split('\n') | |||||
ip_list = [i.strip() for i in ip_list] | |||||
for ip in ip_list: | |||||
if frappe.get_request_header('REMOTE_ADDR', '').startswith(ip) or frappe.get_request_header('X-Forwarded-For', '').startswith(ip): | |||||
return | |||||
frappe.msgprint('Not allowed from this IP Address') | |||||
raise frappe.AuthenticationError | |||||
def validate_hour(self): | |||||
"""check if user is logging in during restricted hours""" | |||||
login_before = int(frappe.conn.get_value('Profile', self.user, 'login_before', ignore=True) or 0) | |||||
login_after = int(frappe.conn.get_value('Profile', self.user, 'login_after', ignore=True) or 0) | |||||
if not (login_before or login_after): | |||||
return | |||||
from frappe.utils import now_datetime | |||||
current_hour = int(now_datetime().strftime('%H')) | |||||
if login_before and current_hour > login_before: | |||||
frappe.msgprint('Not allowed to login after restricted hour', raise_exception=1) | |||||
if login_after and current_hour < login_after: | |||||
frappe.msgprint('Not allowed to login before restricted hour', raise_exception=1) | |||||
def login_as_guest(self): | |||||
"""login as guest""" | |||||
self.user = 'Guest' | |||||
self.post_login() | |||||
def logout(self, arg='', user=None): | |||||
if not user: user = frappe.session.user | |||||
self.run_trigger('on_logout') | |||||
if user in ['demo@erpnext.com', 'Administrator']: | |||||
frappe.conn.sql('delete from tabSessions where sid=%s', frappe.session.get('sid')) | |||||
frappe.cache().delete_value("session:" + frappe.session.get("sid")) | |||||
else: | |||||
from frappe.sessions import clear_sessions | |||||
clear_sessions(user) | |||||
if user == frappe.session.user: | |||||
frappe.session.sid = "" | |||||
frappe.local._response.delete_cookie("full_name") | |||||
frappe.local._response.delete_cookie("user_id") | |||||
frappe.local._response.delete_cookie("sid") | |||||
frappe.local._response.set_cookie("full_name", "") | |||||
frappe.local._response.set_cookie("user_id", "") | |||||
frappe.local._response.set_cookie("sid", "") | |||||
class CookieManager: | |||||
def __init__(self): | |||||
pass | |||||
def set_cookies(self): | |||||
if not frappe.local.session.get('sid'): return | |||||
import datetime | |||||
# sid expires in 3 days | |||||
expires = datetime.datetime.now() + datetime.timedelta(days=3) | |||||
if frappe.session.sid: | |||||
frappe.local._response.set_cookie("sid", frappe.session.sid, expires = expires) | |||||
if frappe.session.session_country: | |||||
frappe.local._response.set_cookie('country', frappe.session.get("session_country")) | |||||
def set_remember_me(self): | |||||
from frappe.utils import cint | |||||
if not cint(frappe.form_dict.get('remember_me')): return | |||||
remember_days = frappe.conn.get_value('Control Panel', None, | |||||
'remember_for_days') or 7 | |||||
import datetime | |||||
expires = datetime.datetime.now() + \ | |||||
datetime.timedelta(days=remember_days) | |||||
frappe.local._response.set_cookie["remember_me"] = 1 | |||||
def _update_password(user, password): | |||||
frappe.conn.sql("""insert into __Auth (user, `password`) | |||||
values (%s, password(%s)) | |||||
on duplicate key update `password`=password(%s)""", (user, | |||||
password, password)) | |||||
@frappe.whitelist() | |||||
def get_logged_user(): | |||||
return frappe.session.user |
@@ -0,0 +1,139 @@ | |||||
# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors | |||||
# MIT License. See license.txt | |||||
from __future__ import unicode_literals | |||||
""" | |||||
bootstrap client session | |||||
""" | |||||
import frappe | |||||
import frappe.defaults | |||||
import frappe.model.doc | |||||
import frappe.widgets.page | |||||
import json | |||||
def get_bootinfo(): | |||||
"""build and return boot info""" | |||||
bootinfo = frappe._dict() | |||||
hooks = frappe.get_hooks() | |||||
doclist = [] | |||||
# profile | |||||
get_profile(bootinfo) | |||||
# control panel | |||||
cp = frappe.model.doc.getsingle('Control Panel') | |||||
# system info | |||||
bootinfo['control_panel'] = frappe._dict(cp.copy()) | |||||
bootinfo['sysdefaults'] = frappe.defaults.get_defaults() | |||||
bootinfo['server_date'] = frappe.utils.nowdate() | |||||
bootinfo["send_print_in_body_and_attachment"] = frappe.conn.get_value("Email Settings", | |||||
None, "send_print_in_body_and_attachment") | |||||
if frappe.session['user'] != 'Guest': | |||||
bootinfo['user_info'] = get_fullnames() | |||||
bootinfo['sid'] = frappe.session['sid']; | |||||
# home page | |||||
bootinfo.modules = {} | |||||
for app in frappe.get_installed_apps(): | |||||
try: | |||||
bootinfo.modules.update(frappe.get_attr(app + ".config.desktop.data") or {}) | |||||
except ImportError, e: | |||||
pass | |||||
bootinfo.hidden_modules = frappe.conn.get_global("hidden_modules") | |||||
bootinfo.doctype_icons = dict(frappe.conn.sql("""select name, icon from | |||||
tabDocType where ifnull(icon,'')!=''""")) | |||||
bootinfo.doctype_icons.update(dict(frappe.conn.sql("""select name, icon from | |||||
tabPage where ifnull(icon,'')!=''"""))) | |||||
add_home_page(bootinfo, doclist) | |||||
add_allowed_pages(bootinfo) | |||||
load_translations(bootinfo) | |||||
load_conf_settings(bootinfo) | |||||
load_startup_js(bootinfo) | |||||
# ipinfo | |||||
if frappe.session['data'].get('ipinfo'): | |||||
bootinfo['ipinfo'] = frappe.session['data']['ipinfo'] | |||||
# add docs | |||||
bootinfo['docs'] = doclist | |||||
for method in hooks.boot_session or []: | |||||
frappe.get_attr(method)(bootinfo) | |||||
from frappe.model.utils import compress | |||||
bootinfo['docs'] = compress(bootinfo['docs']) | |||||
if bootinfo.lang: | |||||
bootinfo.lang = unicode(bootinfo.lang) | |||||
bootinfo.metadata_version = frappe.cache().get_value("metadata_version") | |||||
if not bootinfo.metadata_version: | |||||
bootinfo.metadata_version = frappe.reset_metadata_version() | |||||
return bootinfo | |||||
def load_conf_settings(bootinfo): | |||||
from frappe import conf | |||||
for key in ['developer_mode']: | |||||
if key in conf: bootinfo[key] = conf.get(key) | |||||
def add_allowed_pages(bootinfo): | |||||
bootinfo.page_info = dict(frappe.conn.sql("""select distinct parent, modified from `tabPage Role` | |||||
where role in ('%s')""" % "', '".join(frappe.get_roles()))) | |||||
def load_translations(bootinfo): | |||||
frappe.set_user_lang(frappe.session.user) | |||||
if frappe.lang != 'en': | |||||
bootinfo["__messages"] = frappe.get_lang_dict("include") | |||||
bootinfo["lang"] = frappe.lang | |||||
def get_fullnames(): | |||||
"""map of user fullnames""" | |||||
ret = frappe.conn.sql("""select name, | |||||
concat(ifnull(first_name, ''), | |||||
if(ifnull(last_name, '')!='', ' ', ''), ifnull(last_name, '')), | |||||
user_image, gender, email | |||||
from tabProfile where ifnull(enabled, 0)=1""", as_list=1) | |||||
d = {} | |||||
for r in ret: | |||||
if not r[2]: | |||||
r[2] = '/assets/frappe/images/ui/avatar.png' | |||||
else: | |||||
r[2] = r[2] | |||||
d[r[0]]= {'fullname': r[1], 'image': r[2], 'gender': r[3], | |||||
'email': r[4] or r[0]} | |||||
return d | |||||
def load_startup_js(bootinfo): | |||||
bootinfo.startup_js = "" | |||||
for method in frappe.get_hooks().startup_js or []: | |||||
bootinfo.startup_js += frappe.get_attr(method)() | |||||
def get_profile(bootinfo): | |||||
"""get profile info""" | |||||
bootinfo['profile'] = frappe.user.load_profile() | |||||
def add_home_page(bootinfo, doclist): | |||||
"""load home page""" | |||||
if frappe.session.user=="Guest": | |||||
return | |||||
home_page = frappe.get_application_home_page(frappe.session.user) | |||||
try: | |||||
page_doclist = frappe.widgets.page.get(home_page) | |||||
except (frappe.DoesNotExistError, frappe.PermissionError), e: | |||||
page_doclist = frappe.widgets.page.get('desktop') | |||||
bootinfo['home_page_html'] = page_doclist[0].content | |||||
bootinfo['home_page'] = page_doclist[0].name | |||||
doclist += page_doclist |
@@ -0,0 +1,138 @@ | |||||
# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors | |||||
# MIT License. See license.txt | |||||
from __future__ import unicode_literals | |||||
from frappe.utils.minify import JavascriptMinify | |||||
""" | |||||
Build the `public` folders and setup languages | |||||
""" | |||||
import os, sys, frappe, json, shutil | |||||
from cssmin import cssmin | |||||
import frappe.translate | |||||
def bundle(no_compress): | |||||
"""concat / minify js files""" | |||||
# build js files | |||||
make_asset_dirs() | |||||
build(no_compress) | |||||
frappe.translate.clear_cache() | |||||
def watch(no_compress): | |||||
"""watch and rebuild if necessary""" | |||||
import time | |||||
build(no_compress=True) | |||||
while True: | |||||
if files_dirty(): | |||||
build(no_compress=True) | |||||
time.sleep(3) | |||||
def make_asset_dirs(): | |||||
assets_path = os.path.join(frappe.local.sites_path, "assets") | |||||
site_public_path = os.path.join(frappe.local.site_path, 'public') | |||||
for dir_path in [ | |||||
os.path.join(assets_path, 'js'), | |||||
os.path.join(assets_path, 'css')]: | |||||
if not os.path.exists(dir_path): | |||||
os.makedirs(dir_path) | |||||
# symlink app/public > assets/app | |||||
for app_name in frappe.get_all_apps(True): | |||||
pymodule = frappe.get_module(app_name) | |||||
source = os.path.join(os.path.abspath(os.path.dirname(pymodule.__file__)), 'public') | |||||
target = os.path.join(assets_path, app_name) | |||||
if not os.path.exists(target) and os.path.exists(source): | |||||
os.symlink(os.path.abspath(source), target) | |||||
def build(no_compress=False): | |||||
assets_path = os.path.join(frappe.local.sites_path, "assets") | |||||
for target, sources in get_build_maps().iteritems(): | |||||
pack(os.path.join(assets_path, target), sources, no_compress) | |||||
shutil.copy(os.path.join(os.path.dirname(os.path.abspath(frappe.__file__)), 'data', 'languages.txt'), frappe.local.sites_path) | |||||
# reset_app_html() | |||||
def get_build_maps(): | |||||
"""get all build.jsons with absolute paths""" | |||||
# framework js and css files | |||||
pymodules = [frappe.get_module(app) for app in frappe.get_all_apps(True)] | |||||
app_paths = [os.path.dirname(pymodule.__file__) for pymodule in pymodules] | |||||
build_maps = {} | |||||
for app_path in app_paths: | |||||
path = os.path.join(app_path, 'public', 'build.json') | |||||
if os.path.exists(path): | |||||
with open(path) as f: | |||||
try: | |||||
for target, sources in json.loads(f.read()).iteritems(): | |||||
# update app path | |||||
source_paths = [] | |||||
for source in sources: | |||||
if isinstance(source, list): | |||||
s = frappe.get_pymodule_path(source[0], *source[1].split("/")) | |||||
else: | |||||
s = os.path.join(app_path, source) | |||||
source_paths.append(s) | |||||
build_maps[target] = source_paths | |||||
except Exception, e: | |||||
print path | |||||
raise | |||||
return build_maps | |||||
timestamps = {} | |||||
def pack(target, sources, no_compress): | |||||
from cStringIO import StringIO | |||||
outtype, outtxt = target.split(".")[-1], '' | |||||
jsm = JavascriptMinify() | |||||
for f in sources: | |||||
suffix = None | |||||
if ':' in f: f, suffix = f.split(':') | |||||
if not os.path.exists(f) or os.path.isdir(f): continue | |||||
timestamps[f] = os.path.getmtime(f) | |||||
try: | |||||
with open(f, 'r') as sourcefile: | |||||
data = unicode(sourcefile.read(), 'utf-8', errors='ignore') | |||||
if outtype=="js" and (not no_compress) and suffix!="concat" and (".min." not in f): | |||||
tmpin, tmpout = StringIO(data.encode('utf-8')), StringIO() | |||||
jsm.minify(tmpin, tmpout) | |||||
outtxt += unicode(tmpout.getvalue() or '', 'utf-8').strip('\n') + ';' | |||||
else: | |||||
outtxt += ('\n/*\n *\t%s\n */' % f) | |||||
outtxt += '\n' + data + '\n' | |||||
except Exception, e: | |||||
print "--Error in:" + f + "--" | |||||
print frappe.get_traceback() | |||||
if not no_compress and outtype == 'css': | |||||
pass | |||||
#outtxt = cssmin(outtxt) | |||||
with open(target, 'w') as f: | |||||
f.write(outtxt.encode("utf-8")) | |||||
print "Wrote %s - %sk" % (target, str(int(os.path.getsize(target)/1024))) | |||||
def files_dirty(): | |||||
for target, sources in get_build_maps().iteritems(): | |||||
for f in sources: | |||||
if ':' in f: f, suffix = f.split(':') | |||||
if not os.path.exists(f) or os.path.isdir(f): continue | |||||
if os.path.getmtime(f) != timestamps.get(f): | |||||
print f + ' dirty' | |||||
return True | |||||
else: | |||||
return False | |||||
@@ -0,0 +1,823 @@ | |||||
#!/usr/bin/env python2.7 | |||||
# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors | |||||
# MIT License. See license.txt | |||||
from __future__ import unicode_literals | |||||
import sys, os | |||||
import frappe | |||||
site_arg_optional = [] | |||||
def main(): | |||||
parsed_args = frappe._dict(vars(setup_parser())) | |||||
fn = get_function(parsed_args) | |||||
if not parsed_args.get("sites_path"): | |||||
parsed_args["sites_path"] = "." | |||||
if not parsed_args.get("make_app"): | |||||
if parsed_args.get("site")=="all": | |||||
for site in get_sites(parsed_args["sites_path"]): | |||||
args = parsed_args.copy() | |||||
args["site"] = site | |||||
frappe.init(site) | |||||
run(fn, args) | |||||
else: | |||||
if not fn in site_arg_optional: | |||||
if not parsed_args.get("site") and os.path.exists("currentsite.txt"): | |||||
with open("currentsite.txt", "r") as sitefile: | |||||
site = sitefile.read() | |||||
else: | |||||
site = parsed_args.get("site") | |||||
if not site: | |||||
print "Site argument required" | |||||
exit(1) | |||||
if fn != 'install' and not os.path.exists(site): | |||||
print "Did not find folder '{}'. Are you in sites folder?".format(parsed_args.get("site")) | |||||
exit(1) | |||||
frappe.init(site) | |||||
run(fn, parsed_args) | |||||
else: | |||||
run(fn, parsed_args) | |||||
def cmd(fn): | |||||
def new_fn(*args, **kwargs): | |||||
import inspect | |||||
fnargs, varargs, varkw, defaults = inspect.getargspec(fn) | |||||
new_kwargs = {} | |||||
for i, a in enumerate(fnargs): | |||||
# should not pass an argument more than once | |||||
if i >= len(args) and a in kwargs: | |||||
new_kwargs[a] = kwargs.get(a) | |||||
return fn(*args, **new_kwargs) | |||||
return new_fn | |||||
def run(fn, args): | |||||
if isinstance(args.get(fn), (list, tuple)): | |||||
out = globals().get(fn)(*args.get(fn), **args) | |||||
else: | |||||
out = globals().get(fn)(**args) | |||||
return out | |||||
def get_function(args): | |||||
for fn, val in args.items(): | |||||
if (val or isinstance(val, list)) and globals().get(fn): | |||||
return fn | |||||
def get_sites(sites_path=None): | |||||
import os | |||||
if not sites_path: | |||||
sites_path = '.' | |||||
return [site for site in os.listdir(sites_path) | |||||
if not os.path.islink(os.path.join(sites_path, site)) | |||||
and os.path.isdir(os.path.join(sites_path, site)) | |||||
and not site in ('assets',)] | |||||
def setup_parser(): | |||||
import argparse | |||||
parser = argparse.ArgumentParser(description="Run frappe utility functions") | |||||
setup_install(parser) | |||||
setup_utilities(parser) | |||||
setup_translation(parser) | |||||
setup_test(parser) | |||||
parser.add_argument("site", nargs="?") | |||||
# common | |||||
parser.add_argument("-f", "--force", default=False, action="store_true", | |||||
help="Force execution where applicable (look for [-f] in help)") | |||||
parser.add_argument("-v", "--verbose", default=False, action="store_true", dest="verbose", | |||||
help="Show verbose output where applicable") | |||||
return parser.parse_args() | |||||
def setup_install(parser): | |||||
parser.add_argument("--make_app", default=False, action="store_true", | |||||
help="Make a new application with boilerplate") | |||||
parser.add_argument("--install", metavar="DB-NAME", nargs=1, | |||||
help="Install a new db") | |||||
parser.add_argument("--install_app", metavar="APP-NAME", nargs=1, | |||||
help="Install a new app") | |||||
parser.add_argument("--root-password", nargs=1, | |||||
help="Root password for new app") | |||||
parser.add_argument("--reinstall", default=False, action="store_true", | |||||
help="Install a fresh app in db_name specified in conf.py") | |||||
parser.add_argument("--restore", metavar=("DB-NAME", "SQL-FILE"), nargs=2, | |||||
help="Restore from an sql file") | |||||
parser.add_argument("--add_system_manager", nargs="+", | |||||
metavar=("EMAIL", "[FIRST-NAME] [LAST-NAME]"), help="Add a user with all roles") | |||||
def setup_test(parser): | |||||
parser.add_argument("--run_tests", default=False, action="store_true", | |||||
help="Run tests options [-d doctype], [-m module]") | |||||
parser.add_argument("--app", metavar="APP-NAME", nargs=1, | |||||
help="Run command for specified app") | |||||
parser.add_argument("-d", "--doctype", metavar="DOCTYPE", nargs=1, | |||||
help="Run command for specified doctype") | |||||
parser.add_argument("-m", "--module", metavar="MODULE", nargs=1, | |||||
help="Run command for specified module") | |||||
def setup_utilities(parser): | |||||
# update | |||||
parser.add_argument("-u", "--update", nargs="*", metavar=("REMOTE", "BRANCH"), | |||||
help="Perform git pull, run patches, sync schema and rebuild files/translations") | |||||
parser.add_argument("--reload_gunicorn", default=False, action="store_true", help="reload gunicorn on update") | |||||
parser.add_argument("--patch", nargs=1, metavar="PATCH-MODULE", | |||||
help="Run a particular patch [-f]") | |||||
parser.add_argument("-l", "--latest", default=False, action="store_true", | |||||
help="Run patches, sync schema and rebuild files/translations") | |||||
parser.add_argument("--sync_all", default=False, action="store_true", | |||||
help="Reload all doctypes, pages, etc. using txt files [-f]") | |||||
parser.add_argument("--update_all_sites", nargs="*", metavar=("REMOTE", "BRANCH"), | |||||
help="Perform git pull, run patches, sync schema and rebuild files/translations") | |||||
parser.add_argument("--reload_doc", nargs=3, | |||||
metavar=('"MODULE"', '"DOCTYPE"', '"DOCNAME"')) | |||||
# build | |||||
parser.add_argument("-b", "--build", default=False, action="store_true", | |||||
help="Minify + concatenate JS and CSS files, build translations") | |||||
parser.add_argument("-w", "--watch", default=False, action="store_true", | |||||
help="Watch and concatenate JS and CSS files as and when they change") | |||||
# misc | |||||
parser.add_argument("--backup", default=False, action="store_true", | |||||
help="Take backup of database in backup folder [--with_files]") | |||||
parser.add_argument("--move", default=False, action="store_true", | |||||
help="Move site to different directory defined by --dest_dir") | |||||
parser.add_argument("--dest_dir", nargs=1, metavar="DEST-DIR", | |||||
help="Move site to different directory") | |||||
parser.add_argument("--with_files", default=False, action="store_true", | |||||
help="Also take backup of files") | |||||
parser.add_argument("--domain", nargs="*", | |||||
help="Get or set domain in Website Settings") | |||||
parser.add_argument("--make_conf", nargs="*", metavar=("DB-NAME", "DB-PASSWORD"), | |||||
help="Create new conf.py file") | |||||
parser.add_argument("--make_custom_server_script", nargs=1, metavar="DOCTYPE", | |||||
help="Create new conf.py file") | |||||
parser.add_argument("--set_admin_password", metavar='ADMIN-PASSWORD', nargs=1, | |||||
help="Set administrator password") | |||||
parser.add_argument("--request", metavar='URL-ARGS', nargs=1, help="Run request as admin") | |||||
parser.add_argument("--mysql", action="store_true", help="get mysql shell for a site") | |||||
parser.add_argument("--serve", action="store_true", help="Run development server") | |||||
parser.add_argument("--profile", action="store_true", help="enable profiling in development server") | |||||
parser.add_argument("--smtp", action="store_true", help="Run smtp debug server", | |||||
dest="smtp_debug_server") | |||||
parser.add_argument("--python", action="store_true", help="get python shell for a site") | |||||
parser.add_argument("--flush_memcache", action="store_true", help="flush memcached") | |||||
parser.add_argument("--ipython", action="store_true", help="get ipython shell for a site") | |||||
parser.add_argument("--get_site_status", action="store_true", help="Get site details") | |||||
parser.add_argument("--update_site_config", nargs=1, | |||||
metavar="site-CONFIG-JSON", | |||||
help="Update site_config.json for a given site") | |||||
parser.add_argument("--port", default=8000, type=int, help="port for development server") | |||||
parser.add_argument("--use", action="store_true", help="Set current site for development.") | |||||
# clear | |||||
parser.add_argument("--clear_web", default=False, action="store_true", | |||||
help="Clear website cache") | |||||
parser.add_argument("--build_sitemap", default=False, action="store_true", | |||||
help="Build Website Sitemap") | |||||
parser.add_argument("--sync_statics", default=False, action="store_true", | |||||
help="Sync files from templates/statics to Web Pages") | |||||
parser.add_argument("--clear_cache", default=False, action="store_true", | |||||
help="Clear cache, doctype cache and defaults") | |||||
parser.add_argument("--reset_perms", default=False, action="store_true", | |||||
help="Reset permissions for all doctypes") | |||||
# scheduler | |||||
parser.add_argument("--run_scheduler", default=False, action="store_true", | |||||
help="Trigger scheduler") | |||||
parser.add_argument("--run_scheduler_event", nargs=1, | |||||
metavar="all | daily | weekly | monthly", | |||||
help="Run a scheduler event") | |||||
# replace | |||||
parser.add_argument("--replace", nargs=3, | |||||
metavar=("SEARCH-REGEX", "REPLACE-BY", "FILE-EXTN"), | |||||
help="Multi-file search-replace [-f]") | |||||
# import/export | |||||
parser.add_argument("--export_doc", nargs=2, metavar=('"DOCTYPE"', '"DOCNAME"')) | |||||
parser.add_argument("--export_doclist", nargs=3, metavar=("DOCTYPE", "NAME", "PATH"), | |||||
help="""Export doclist as json to the given path, use '-' as name for Singles.""") | |||||
parser.add_argument("--export_csv", nargs=2, metavar=("DOCTYPE", "PATH"), | |||||
help="""Dump DocType as csv""") | |||||
parser.add_argument("--import_doclist", nargs=1, metavar="PATH", | |||||
help="""Import (insert/update) doclist. If the argument is a directory, all files ending with .json are imported""") | |||||
def setup_translation(parser): | |||||
parser.add_argument("--build_message_files", default=False, action="store_true", | |||||
help="Build message files for translation.") | |||||
parser.add_argument("--get_untranslated", nargs=2, metavar=("LANG-CODE", "TARGET-FILE-PATH"), | |||||
help="""Get untranslated strings for lang.""") | |||||
parser.add_argument("--update_translations", nargs=3, | |||||
metavar=("LANG-CODE", "UNTRANSLATED-FILE-PATH", "TRANSLATED-FILE-PATH"), | |||||
help="""Update translated strings.""") | |||||
# methods | |||||
@cmd | |||||
def make_app(): | |||||
from frappe.utils.boilerplate import make_boilerplate | |||||
make_boilerplate() | |||||
@cmd | |||||
def use(): | |||||
with open("currentsite.txt", "w") as sitefile: | |||||
sitefile.write(frappe.local.site) | |||||
# install | |||||
@cmd | |||||
def install(db_name, root_login="root", root_password=None, source_sql=None, | |||||
admin_password = 'admin', verbose=True, force=False, site_config=None, reinstall=False): | |||||
from frappe.installer import install_db, install_app, make_site_dirs | |||||
install_db(root_login=root_login, root_password=root_password, db_name=db_name, source_sql=source_sql, | |||||
admin_password = admin_password, verbose=verbose, force=force, site_config=site_config, reinstall=reinstall) | |||||
make_site_dirs() | |||||
install_app("frappe", verbose=verbose) | |||||
frappe.destroy() | |||||
@cmd | |||||
def install_app(app_name, verbose=False): | |||||
from frappe.installer import install_app | |||||
frappe.connect() | |||||
install_app(app_name, verbose=verbose) | |||||
frappe.destroy() | |||||
@cmd | |||||
def reinstall(verbose=True): | |||||
install(db_name=frappe.conf.db_name, verbose=verbose, force=True, reinstall=True) | |||||
@cmd | |||||
def restore(db_name, source_sql, verbose=True, force=False): | |||||
install(db_name, source_sql=source_sql, verbose=verbose, force=force) | |||||
@cmd | |||||
def install_fixtures(): | |||||
from frappe.install_lib.install import install_fixtures | |||||
install_fixtures() | |||||
frappe.destroy() | |||||
@cmd | |||||
def add_system_manager(email, first_name=None, last_name=None): | |||||
frappe.connect() | |||||
frappe.profile.add_system_manager(email, first_name, last_name) | |||||
frappe.conn.commit() | |||||
frappe.destroy() | |||||
# utilities | |||||
@cmd | |||||
def update(remote=None, branch=None, reload_gunicorn=False): | |||||
pull(remote=remote, branch=branch) | |||||
# maybe there are new framework changes, any consequences? | |||||
reload(frappe) | |||||
build() | |||||
latest() | |||||
if reload_gunicorn: | |||||
import subprocess | |||||
subprocess.check_output("killall -HUP gunicorn".split()) | |||||
@cmd | |||||
def latest(verbose=True): | |||||
import frappe.modules.patch_handler | |||||
import frappe.model.sync | |||||
from frappe.website import rebuild_config | |||||
frappe.connect() | |||||
try: | |||||
# run patches | |||||
frappe.local.patch_log_list = [] | |||||
frappe.modules.patch_handler.run_all() | |||||
if verbose: | |||||
print "\n".join(frappe.local.patch_log_list) | |||||
# sync | |||||
frappe.model.sync.sync_all() | |||||
# build website config if any changes in templates etc. | |||||
rebuild_config() | |||||
except frappe.modules.patch_handler.PatchError, e: | |||||
print "\n".join(frappe.local.patch_log_list) | |||||
raise | |||||
finally: | |||||
frappe.destroy() | |||||
@cmd | |||||
def sync_all(force=False): | |||||
import frappe.model.sync | |||||
frappe.connect() | |||||
frappe.model.sync.sync_all(force=force) | |||||
frappe.destroy() | |||||
@cmd | |||||
def patch(patch_module, force=False): | |||||
import frappe.modules.patch_handler | |||||
frappe.connect() | |||||
frappe.local.patch_log_list = [] | |||||
frappe.modules.patch_handler.run_single(patch_module, force=force) | |||||
print "\n".join(frappe.local.patch_log_list) | |||||
frappe.destroy() | |||||
@cmd | |||||
def update_all_sites(remote=None, branch=None, verbose=True): | |||||
pull(remote, branch) | |||||
# maybe there are new framework changes, any consequences? | |||||
reload(frappe) | |||||
build() | |||||
for site in get_sites(): | |||||
frappe.init(site) | |||||
latest(verbose=verbose) | |||||
@cmd | |||||
def reload_doc(module, doctype, docname, force=False): | |||||
frappe.connect() | |||||
frappe.reload_doc(module, doctype, docname, force=force) | |||||
frappe.conn.commit() | |||||
frappe.destroy() | |||||
@cmd | |||||
def build(): | |||||
import frappe.build | |||||
import frappe | |||||
frappe.build.bundle(False) | |||||
@cmd | |||||
def watch(): | |||||
import frappe.build | |||||
frappe.build.watch(True) | |||||
@cmd | |||||
def backup(with_files=False, verbose=True, backup_path_db=None, backup_path_files=None): | |||||
from frappe.utils.backups import scheduled_backup | |||||
frappe.connect() | |||||
odb = scheduled_backup(ignore_files=not with_files, backup_path_db=backup_path_db, backup_path_files=backup_path_files) | |||||
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() | |||||
return odb | |||||
@cmd | |||||
def move(dest_dir=None): | |||||
import os | |||||
if not dest_dir: | |||||
raise Exception, "--dest_dir is required for --move" | |||||
if not os.path.isdir(dest_dir): | |||||
raise Exception, "destination is not a directory or does not exist" | |||||
old_path = frappe.utils.get_site() | |||||
new_path = os.path.join(dest_dir, site) | |||||
# check if site dump of same name already exists | |||||
site_dump_exists = True | |||||
count = 0 | |||||
while site_dump_exists: | |||||
final_new_path = new_path + (count and str(count) or "") | |||||
site_dump_exists = os.path.exists(final_new_path) | |||||
count = int(count or 0) + 1 | |||||
os.rename(old_path, final_new_path) | |||||
frappe.destroy() | |||||
return os.path.basename(final_new_path) | |||||
@cmd | |||||
def domain(host_url=None): | |||||
frappe.connect() | |||||
if host_url: | |||||
frappe.conn.set_value("Website Settings", None, "subdomain", host_url) | |||||
frappe.conn.commit() | |||||
else: | |||||
print frappe.conn.get_value("Website Settings", None, "subdomain") | |||||
frappe.destroy() | |||||
@cmd | |||||
def make_conf(db_name=None, db_password=None, site_config=None): | |||||
from frappe.install_lib.install import make_conf | |||||
make_conf(db_name=db_name, db_password=db_password, site_config=site_config) | |||||
@cmd | |||||
def make_custom_server_script(doctype): | |||||
from frappe.core.doctype.custom_script.custom_script import make_custom_server_script_file | |||||
frappe.connect() | |||||
make_custom_server_script_file(doctype) | |||||
frappe.destroy() | |||||
# clear | |||||
@cmd | |||||
def clear_cache(): | |||||
import frappe.sessions | |||||
frappe.connect() | |||||
frappe.clear_cache() | |||||
frappe.destroy() | |||||
@cmd | |||||
def clear_web(): | |||||
import frappe.website.render | |||||
frappe.connect() | |||||
frappe.website.render.clear_cache() | |||||
frappe.destroy() | |||||
@cmd | |||||
def build_sitemap(): | |||||
from frappe.website import rebuild_config | |||||
frappe.connect() | |||||
rebuild_config() | |||||
frappe.destroy() | |||||
@cmd | |||||
def sync_statics(): | |||||
from frappe.website.doctype.web_page import web_page | |||||
frappe.connect() | |||||
web_page.sync_statics() | |||||
frappe.conn.commit() | |||||
frappe.destroy() | |||||
@cmd | |||||
def reset_perms(): | |||||
frappe.connect() | |||||
for d in frappe.conn.sql_list("""select name from `tabDocType` | |||||
where ifnull(istable, 0)=0 and ifnull(custom, 0)=0"""): | |||||
frappe.clear_cache(doctype=d) | |||||
frappe.reset_perms(d) | |||||
frappe.destroy() | |||||
# scheduler | |||||
@cmd | |||||
def run_scheduler(): | |||||
from frappe.utils.file_lock import create_lock, delete_lock | |||||
import frappe.utils.scheduler | |||||
if create_lock('scheduler'): | |||||
frappe.connect() | |||||
print frappe.utils.scheduler.execute() | |||||
delete_lock('scheduler') | |||||
frappe.destroy() | |||||
@cmd | |||||
def run_scheduler_event(event): | |||||
import frappe.utils.scheduler | |||||
frappe.connect() | |||||
print frappe.utils.scheduler.trigger(event) | |||||
frappe.destroy() | |||||
# replace | |||||
@cmd | |||||
def replace(search_regex, replacement, extn, force=False): | |||||
print search_regex, replacement, extn | |||||
replace_code('.', search_regex, replacement, extn, force=force) | |||||
# import/export | |||||
@cmd | |||||
def export_doc(doctype, docname): | |||||
import frappe.modules | |||||
frappe.connect() | |||||
frappe.modules.export_doc(doctype, docname) | |||||
frappe.destroy() | |||||
@cmd | |||||
def export_doclist(doctype, name, path): | |||||
from frappe.core.page.data_import_tool import data_import_tool | |||||
frappe.connect() | |||||
data_import_tool.export_json(doctype, name, path) | |||||
frappe.destroy() | |||||
@cmd | |||||
def export_csv(doctype, path): | |||||
from frappe.core.page.data_import_tool import data_import_tool | |||||
frappe.connect() | |||||
data_import_tool.export_csv(doctype, path) | |||||
frappe.destroy() | |||||
@cmd | |||||
def import_doclist(path, force=False): | |||||
from frappe.core.page.data_import_tool import data_import_tool | |||||
frappe.connect() | |||||
data_import_tool.import_doclist(path, overwrite=force) | |||||
frappe.destroy() | |||||
# translation | |||||
@cmd | |||||
def build_message_files(): | |||||
import frappe.translate | |||||
frappe.connect() | |||||
frappe.translate.rebuild_all_translation_files() | |||||
frappe.destroy() | |||||
@cmd | |||||
def get_untranslated(lang, untranslated_file): | |||||
import frappe.translate | |||||
frappe.connect() | |||||
frappe.translate.get_untranslated(lang, untranslated_file) | |||||
frappe.destroy() | |||||
@cmd | |||||
def update_translations(lang, untranslated_file, translated_file): | |||||
import frappe.translate | |||||
frappe.connect() | |||||
frappe.translate.update_translations(lang, untranslated_file, translated_file) | |||||
frappe.destroy() | |||||
# git | |||||
@cmd | |||||
def git(param): | |||||
if isinstance(param, (list, tuple)): | |||||
param = " ".join(param) | |||||
import os | |||||
os.system("""cd lib && git %s""" % param) | |||||
os.system("""cd app && git %s""" % param) | |||||
def get_remote_and_branch(remote=None, branch=None): | |||||
if not (remote and branch): | |||||
if not frappe.conf.branch: | |||||
raise Exception("Please specify remote and branch") | |||||
remote = remote or "origin" | |||||
branch = branch or frappe.conf.branch | |||||
frappe.destroy() | |||||
return remote, branch | |||||
@cmd | |||||
def pull(remote=None, branch=None): | |||||
remote, branch = get_remote_and_branch(remote, branch) | |||||
git(("pull", remote, branch)) | |||||
@cmd | |||||
def push(remote=None, branch=None): | |||||
remote, branch = get_remote_and_branch(remote, branch) | |||||
git(("push", remote, branch)) | |||||
@cmd | |||||
def status(): | |||||
git("status") | |||||
@cmd | |||||
def commit(message): | |||||
git("""commit -a -m "%s" """ % message.replace('"', '\"')) | |||||
@cmd | |||||
def checkout(branch): | |||||
git(("checkout", branch)) | |||||
@cmd | |||||
def set_admin_password(admin_password): | |||||
import frappe | |||||
frappe.connect() | |||||
frappe.conn.sql("""update __Auth set `password`=password(%s) | |||||
where user='Administrator'""", (admin_password,)) | |||||
frappe.conn.commit() | |||||
frappe.destroy() | |||||
@cmd | |||||
def mysql(): | |||||
import frappe | |||||
import commands, os | |||||
msq = commands.getoutput('which mysql') | |||||
os.execv(msq, [msq, '-u', frappe.conf.db_name, '-p'+frappe.conf.db_password, frappe.conf.db_name, '-h', frappe.conf.db_host or "localhost", "-A"]) | |||||
frappe.destroy() | |||||
@cmd | |||||
def python(site): | |||||
import frappe | |||||
import commands, os | |||||
python = commands.getoutput('which python') | |||||
if site: | |||||
os.environ["site"] = site | |||||
os.environ["PYTHONSTARTUP"] = os.path.join(os.path.dirname(frappe.__file__), "pythonrc.py") | |||||
os.execv(python, [python]) | |||||
frappe.destroy() | |||||
@cmd | |||||
def ipython(): | |||||
import frappe | |||||
frappe.connect() | |||||
import IPython | |||||
IPython.embed() | |||||
@cmd | |||||
def smtp_debug_server(): | |||||
import commands, os | |||||
python = commands.getoutput('which python') | |||||
os.execv(python, [python, '-m', "smtpd", "-n", "-c", "DebuggingServer", "localhost:25"]) | |||||
@cmd | |||||
def run_tests(app=None, module=None, doctype=None, verbose=False): | |||||
import frappe.test_runner | |||||
ret = frappe.test_runner.main(app and app[0], module and module[0], doctype and doctype[0], verbose) | |||||
if len(ret.failures) > 0 or len(ret.errors) > 0: | |||||
exit(1) | |||||
@cmd | |||||
def serve(port=8000, profile=False): | |||||
import frappe.app | |||||
frappe.app.serve(port=port, profile=profile, site=frappe.local.site) | |||||
@cmd | |||||
def request(args): | |||||
import frappe.handler | |||||
import frappe.api | |||||
frappe.connect() | |||||
if "?" in args: | |||||
frappe.local.form_dict = frappe._dict([a.split("=") for a in args.split("?")[-1].split("&")]) | |||||
else: | |||||
frappe.local.form_dict = frappe._dict() | |||||
if args.startswith("/api/method"): | |||||
frappe.local.form_dict.cmd = args.split("?")[0].split("/")[-1] | |||||
frappe.handler.execute_cmd(frappe.form_dict.cmd) | |||||
print frappe.response | |||||
frappe.destroy() | |||||
@cmd | |||||
def flush_memcache(): | |||||
frappe.cache().flush_all() | |||||
def replace_code(start, txt1, txt2, extn, search=None, force=False): | |||||
"""replace all txt1 by txt2 in files with extension (extn)""" | |||||
import frappe.utils | |||||
import os, re | |||||
esc = frappe.utils.make_esc('[]') | |||||
if not search: search = esc(txt1) | |||||
for wt in os.walk(start, followlinks=1): | |||||
for fn in wt[2]: | |||||
if fn.split('.')[-1]==extn: | |||||
fpath = os.path.join(wt[0], fn) | |||||
with open(fpath, 'r') as f: | |||||
content = f.read() | |||||
if re.search(search, content): | |||||
res = search_replace_with_prompt(fpath, txt1, txt2, force) | |||||
if res == 'skip': | |||||
return 'skip' | |||||
def search_replace_with_prompt(fpath, txt1, txt2, force=False): | |||||
""" Search and replace all txt1 by txt2 in the file with confirmation""" | |||||
from termcolor import colored | |||||
with open(fpath, 'r') as f: | |||||
content = f.readlines() | |||||
tmp = [] | |||||
for c in content: | |||||
if c.find(txt1) != -1: | |||||
print fpath | |||||
print colored(txt1, 'red').join(c[:-1].split(txt1)) | |||||
a = '' | |||||
if force: | |||||
c = c.replace(txt1, txt2) | |||||
else: | |||||
while a.lower() not in ['y', 'n', 'skip']: | |||||
a = raw_input('Do you want to Change [y/n/skip]?') | |||||
if a.lower() == 'y': | |||||
c = c.replace(txt1, txt2) | |||||
elif a.lower() == 'skip': | |||||
return 'skip' | |||||
tmp.append(c) | |||||
with open(fpath, 'w') as f: | |||||
f.write(''.join(tmp)) | |||||
print colored('Updated', 'green') | |||||
@cmd | |||||
def get_site_status(verbose=False): | |||||
import frappe | |||||
import frappe.utils | |||||
from frappe.profile import get_system_managers | |||||
from frappe.core.doctype.profile.profile import get_total_users, get_active_users, \ | |||||
get_website_users, get_active_website_users | |||||
import json | |||||
frappe.connect() | |||||
ret = { | |||||
'last_backup_on': frappe.local.conf.last_backup_on, | |||||
'active_users': get_active_users(), | |||||
'total_users': get_total_users(), | |||||
'active_website_users': get_active_website_users(), | |||||
'website_users': get_website_users(), | |||||
'system_managers': "\n".join(get_system_managers()), | |||||
'default_company': frappe.conn.get_default("company"), | |||||
'disk_usage': frappe.utils.get_disk_usage(), | |||||
'working_directory': frappe.local.site_path | |||||
} | |||||
# country, timezone, industry | |||||
control_panel_details = frappe.conn.get_value("Control Panel", "Control Panel", | |||||
["country", "time_zone", "industry"], as_dict=True) | |||||
if control_panel_details: | |||||
ret.update(control_panel_details) | |||||
# basic usage/progress analytics | |||||
for doctype in ("Company", "Customer", "Item", "Quotation", "Sales Invoice", | |||||
"Journal Voucher", "Stock Ledger Entry"): | |||||
key = doctype.lower().replace(" ", "_") + "_exists" | |||||
ret[key] = 1 if frappe.conn.count(doctype) else 0 | |||||
frappe.destroy() | |||||
if verbose: | |||||
print json.dumps(ret, indent=1, sort_keys=True) | |||||
return ret | |||||
@cmd | |||||
def update_site_config(site_config, verbose=False): | |||||
import json | |||||
if isinstance(site_config, basestring): | |||||
site_config = json.loads(site_config) | |||||
config = frappe.get_site_config() | |||||
config.update(site_config) | |||||
site_config_path = os.path.join(frappe.local.site_path, "site_config.json") | |||||
with open(site_config_path, "w") as f: | |||||
json.dump(config, f, indent=1, sort_keys=True) | |||||
frappe.destroy() | |||||
@cmd | |||||
def bump(repo, bump_type): | |||||
import json | |||||
assert repo in ['lib', 'app'] | |||||
assert bump_type in ['minor', 'major', 'patch'] | |||||
def validate(repo_path): | |||||
import git | |||||
repo = git.Repo(repo_path) | |||||
if repo.active_branch != 'master': | |||||
raise Exception, "Current branch not master in {}".format(repo_path) | |||||
def bump_version(version, version_type): | |||||
import semantic_version | |||||
v = semantic_version.Version(version) | |||||
if version_type == 'minor': | |||||
v.minor += 1 | |||||
elif version_type == 'major': | |||||
v.major += 1 | |||||
elif version_type == 'patch': | |||||
v.patch += 1 | |||||
return unicode(v) | |||||
def add_tag(repo_path, version): | |||||
import git | |||||
repo = git.Repo(repo_path) | |||||
repo.index.add(['config.json']) | |||||
repo.index.commit('bumped to version {}'.format(version)) | |||||
repo.create_tag('v' + version, repo.head) | |||||
def update_framework_requirement(version): | |||||
with open('app/config.json') as f: | |||||
config = json.load(f) | |||||
config['requires_framework_version'] = '==' + version | |||||
with open('app/config.json', 'w') as f: | |||||
json.dump(config, f, indent=1, sort_keys=True) | |||||
validate('lib/') | |||||
validate('app/') | |||||
if repo == 'app': | |||||
with open('app/config.json') as f: | |||||
config = json.load(f) | |||||
new_version = bump_version(config['app_version'], bump_type) | |||||
config['app_version'] = new_version | |||||
with open('app/config.json', 'w') as f: | |||||
json.dump(config, f, indent=1, sort_keys=True) | |||||
add_tag('app/', new_version) | |||||
elif repo == 'lib': | |||||
with open('lib/config.json') as f: | |||||
config = json.load(f) | |||||
new_version = bump_version(config['framework_version'], bump_type) | |||||
config['framework_version'] = new_version | |||||
with open('lib/config.json', 'w') as f: | |||||
json.dump(config, f, indent=1, sort_keys=True) | |||||
add_tag('lib/', new_version) | |||||
update_framework_requirement(new_version) | |||||
bump('app', bump_type) | |||||
if __name__=="__main__": | |||||
main() |
@@ -0,0 +1,138 @@ | |||||
# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors | |||||
# MIT License. See license.txt | |||||
from __future__ import unicode_literals | |||||
import frappe | |||||
from frappe import _ | |||||
import frappe.model | |||||
import frappe.utils | |||||
import json, os | |||||
@frappe.whitelist() | |||||
def get(doctype, name=None, filters=None): | |||||
if filters and not name: | |||||
name = frappe.conn.get_value(doctype, json.loads(filters)) | |||||
if not name: | |||||
raise Exception, "No document found for given filters" | |||||
return [d.fields for d in frappe.bean(doctype, name).doclist] | |||||
@frappe.whitelist() | |||||
def get_value(doctype, fieldname, filters=None, as_dict=True, debug=False): | |||||
if not frappe.has_permission(doctype): | |||||
frappe.msgprint("No Permission", raise_exception=True) | |||||
if fieldname and fieldname.startswith("["): | |||||
fieldname = json.loads(fieldname) | |||||
return frappe.conn.get_value(doctype, json.loads(filters), fieldname, as_dict=as_dict, debug=debug) | |||||
@frappe.whitelist() | |||||
def set_value(doctype, name, fieldname, value): | |||||
if fieldname in frappe.model.default_fields: | |||||
frappe.throw(_("Cannot edit standard fields")) | |||||
doc = frappe.conn.get_value(doctype, name, ["parenttype", "parent"], as_dict=True) | |||||
if doc and doc.parent: | |||||
bean = frappe.bean(doc.parenttype, doc.parent) | |||||
child = bean.doclist.getone({"doctype": doctype, "name": name}) | |||||
child.fields[fieldname] = value | |||||
else: | |||||
bean = frappe.bean(doctype, name) | |||||
bean.doc.fields[fieldname] = value | |||||
bean.save() | |||||
return [d.fields for d in bean.doclist] | |||||
@frappe.whitelist() | |||||
def insert(doclist): | |||||
if isinstance(doclist, basestring): | |||||
doclist = json.loads(doclist) | |||||
doclist[0]["__islocal"] = 1 | |||||
return save(doclist) | |||||
@frappe.whitelist() | |||||
def save(doclist): | |||||
if isinstance(doclist, basestring): | |||||
doclist = json.loads(doclist) | |||||
doclistobj = frappe.bean(doclist) | |||||
doclistobj.save() | |||||
return [d.fields for d in doclist] | |||||
@frappe.whitelist() | |||||
def rename_doc(doctype, old_name, new_name, merge=False): | |||||
new_name = frappe.rename_doc(doctype, old_name, new_name, merge=merge) | |||||
return new_name | |||||
@frappe.whitelist() | |||||
def submit(doclist): | |||||
if isinstance(doclist, basestring): | |||||
doclist = json.loads(doclist) | |||||
doclistobj = frappe.bean(doclist) | |||||
doclistobj.submit() | |||||
return [d.fields for d in doclist] | |||||
@frappe.whitelist() | |||||
def cancel(doctype, name): | |||||
wrapper = frappe.bean(doctype, name) | |||||
wrapper.cancel() | |||||
return [d.fields for d in wrapper.doclist] | |||||
@frappe.whitelist() | |||||
def delete(doctype, name): | |||||
frappe.delete_doc(doctype, name) | |||||
@frappe.whitelist() | |||||
def set_default(key, value, parent=None): | |||||
"""set a user default value""" | |||||
frappe.conn.set_default(key, value, parent or frappe.session.user) | |||||
frappe.clear_cache(user=frappe.session.user) | |||||
@frappe.whitelist() | |||||
def make_width_property_setter(): | |||||
doclist = json.loads(frappe.form_dict.doclist) | |||||
if doclist[0]["doctype"]=="Property Setter" and doclist[0]["property"]=="width": | |||||
bean = frappe.bean(doclist) | |||||
bean.ignore_permissions = True | |||||
bean.insert() | |||||
@frappe.whitelist() | |||||
def bulk_update(docs): | |||||
docs = json.loads(docs) | |||||
failed_docs = [] | |||||
for doc in docs: | |||||
try: | |||||
ddoc = {key: val for key, val in doc.iteritems() if key not in ['doctype', 'docname']} | |||||
doctype = doc['doctype'] | |||||
docname = doc['docname'] | |||||
bean = frappe.bean(doctype, docname) | |||||
bean.doc.update(ddoc) | |||||
bean.save() | |||||
except: | |||||
failed_docs.append({ | |||||
'doc': doc, | |||||
'exc': frappe.utils.get_traceback() | |||||
}) | |||||
return {'failed_docs': failed_docs} | |||||
@frappe.whitelist() | |||||
def has_permission(doctype, docname, perm_type="read"): | |||||
# perm_type can be one of read, write, create, submit, cancel, report | |||||
return {"has_permission": frappe.has_permission(doctype, perm_type.lower(), docname)} | |||||
@frappe.whitelist() | |||||
def get_js(src): | |||||
contentpath = os.path.join(frappe.local.sites_path, src) | |||||
with open(contentpath, "r") as srcfile: | |||||
code = srcfile.read() | |||||
if frappe.local.lang != "en": | |||||
code += "\n\n$.extend(wn._messages, {})".format(json.dumps(\ | |||||
frappe.get_lang_dict("jsfile", contentpath))) | |||||
return code | |||||
@@ -0,0 +1,45 @@ | |||||
from frappe import _ | |||||
data = { | |||||
"Calendar": { | |||||
"color": "#2980b9", | |||||
"icon": "icon-calendar", | |||||
"label": _("Calendar"), | |||||
"link": "Calendar/Event", | |||||
"type": "view" | |||||
}, | |||||
"Messages": { | |||||
"color": "#9b59b6", | |||||
"icon": "icon-comments", | |||||
"label": _("Messages"), | |||||
"link": "messages", | |||||
"type": "page" | |||||
}, | |||||
"To Do": { | |||||
"color": "#f1c40f", | |||||
"icon": "icon-check", | |||||
"label": _("To Do"), | |||||
"link": "List/ToDo", | |||||
"doctype": "ToDo", | |||||
"type": "list" | |||||
}, | |||||
"Website": { | |||||
"color": "#16a085", | |||||
"icon": "icon-globe", | |||||
"link": "website-home", | |||||
"type": "module" | |||||
}, | |||||
"Installer": { | |||||
"color": "#888", | |||||
"icon": "icon-download", | |||||
"link": "applications", | |||||
"type": "page", | |||||
"label": _("Installer") | |||||
}, | |||||
"Setup": { | |||||
"color": "#bdc3c7", | |||||
"icon": "icon-wrench", | |||||
"link": "Setup", | |||||
"type": "setup" | |||||
}, | |||||
} |
@@ -0,0 +1,145 @@ | |||||
from frappe import _ | |||||
data = [ | |||||
{ | |||||
"label": _("Users and Permissions"), | |||||
"icon": "icon-group", | |||||
"items": [ | |||||
{ | |||||
"type": "doctype", | |||||
"name": "Profile", | |||||
"description": _("System and Website Users") | |||||
}, | |||||
{ | |||||
"type": "doctype", | |||||
"name": "Role", | |||||
"description": _("User Roles") | |||||
}, | |||||
{ | |||||
"type": "page", | |||||
"name": "permission-manager", | |||||
"label": "Permission Manager", | |||||
"icon": "icon-lock", | |||||
"description": _("Set Permissions on Document Types and Roles") | |||||
}, | |||||
{ | |||||
"type": "page", | |||||
"name": "user-properties", | |||||
"label": "User Properties", | |||||
"icon": "icon-user", | |||||
"description": _("Set Defaults and Restrictions for Users") | |||||
} | |||||
] | |||||
}, | |||||
{ | |||||
"label": _("Tools"), | |||||
"icon": "icon-wrench", | |||||
"items": [ | |||||
{ | |||||
"type": "page", | |||||
"name": "data-import-tool", | |||||
"label": _("Import / Export Data"), | |||||
"icon": "icon-upload", | |||||
"description": _("Import / Export Data from .csv files.") | |||||
}, | |||||
{ | |||||
"type": "page", | |||||
"name": "modules_setup", | |||||
"label": _("Show / Hide Modules"), | |||||
"icon": "icon-upload", | |||||
"description": _("Show or hide modules globally.") | |||||
}, | |||||
{ | |||||
"type": "doctype", | |||||
"name": "Naming Series", | |||||
"description": _("Set numbering series for transactions.") | |||||
}, | |||||
{ | |||||
"type": "doctype", | |||||
"name": "Rename Tool", | |||||
"description": _("Rename many items by uploading a .csv file.") | |||||
}, | |||||
{ | |||||
"type": "doctype", | |||||
"name": "File Data", | |||||
"description": _("Manage uploaded files.") | |||||
} | |||||
] | |||||
}, | |||||
{ | |||||
"label": _("Workflow"), | |||||
"icon": "icon-random", | |||||
"items": [ | |||||
{ | |||||
"type": "doctype", | |||||
"name": "Workflow", | |||||
"description": _("Define workflows for forms.") | |||||
}, | |||||
{ | |||||
"type": "doctype", | |||||
"name": "Workflow State", | |||||
"description": _("States for workflow (e.g. Draft, Approved, Cancelled).") | |||||
}, | |||||
{ | |||||
"type": "doctype", | |||||
"name": "Workflow Action", | |||||
"description": _("Actions for workflow (e.g. Approve, Cancel).") | |||||
}, | |||||
] | |||||
}, | |||||
{ | |||||
"label": _("Email"), | |||||
"icon": "icon-envelope", | |||||
"items": [ | |||||
{ | |||||
"type": "doctype", | |||||
"name": "Email Settings", | |||||
"description": _("Set outgoing mail server.") | |||||
}, | |||||
] | |||||
}, | |||||
{ | |||||
"label": _("Customize"), | |||||
"icon": "icon-glass", | |||||
"items": [ | |||||
{ | |||||
"type": "doctype", | |||||
"name": "Customize Form", | |||||
"description": _("Change field properties (hide, readonly, permission etc.)") | |||||
}, | |||||
{ | |||||
"type": "doctype", | |||||
"name": "Custom Field", | |||||
"description": _("Add fields to forms.") | |||||
}, | |||||
{ | |||||
"type": "doctype", | |||||
"name": "Custom Script", | |||||
"description": _("Add custom javascript to forms.") | |||||
} | |||||
] | |||||
}, | |||||
{ | |||||
"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") | |||||
}, | |||||
{ | |||||
"type": "doctype", | |||||
"name": "Scheduler Log", | |||||
"description": _("Log of error on automated events (scheduler).") | |||||
}, | |||||
] | |||||
} | |||||
] |
@@ -0,0 +1,9 @@ | |||||
# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors | |||||
# MIT License. See license.txt | |||||
from __future__ import unicode_literals | |||||
import frappe | |||||
class DocType: | |||||
def __init__(self, d, dl): | |||||
self.doc, self.doclist = d, dl |
@@ -0,0 +1,70 @@ | |||||
# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors | |||||
# MIT License. See license.txt | |||||
from __future__ import unicode_literals | |||||
import frappe, json | |||||
class DocType: | |||||
def __init__(self, d, dl): | |||||
self.doc, self.doclist = d, dl | |||||
def validate(self): | |||||
if frappe.conn.sql("""select count(*) from tabComment where comment_doctype=%s | |||||
and comment_docname=%s""", (self.doc.doctype, self.doc.name))[0][0] >= 50: | |||||
frappe.msgprint("Max Comments reached!", raise_exception=True) | |||||
def on_update(self): | |||||
self.update_comment_in_doc() | |||||
def update_comment_in_doc(self): | |||||
if self.doc.comment_doctype and self.doc.comment_docname and self.doc.comment: | |||||
try: | |||||
_comments = self.get_comments_from_parent() | |||||
updated = False | |||||
for c in _comments: | |||||
if c.get("name")==self.doc.name: | |||||
c["comment"] = self.doc.comment | |||||
updated = True | |||||
if not updated: | |||||
_comments.append({ | |||||
"comment": self.doc.comment, | |||||
"by": self.doc.comment_by or self.doc.owner, | |||||
"name":self.doc.name | |||||
}) | |||||
self.update_comments_in_parent(_comments) | |||||
except Exception, e: | |||||
if e.args[0]==1054: | |||||
from frappe.model.db_schema import add_column | |||||
add_column(self.doc.comment_doctype, "_comments", "Text") | |||||
self.update_comment_in_doc() | |||||
elif e.args[0]==1146: | |||||
# no table | |||||
pass | |||||
else: | |||||
raise | |||||
def get_comments_from_parent(self): | |||||
_comments = frappe.conn.get_value(self.doc.comment_doctype, | |||||
self.doc.comment_docname, "_comments") or "[]" | |||||
return json.loads(_comments) | |||||
def update_comments_in_parent(self, _comments): | |||||
# use sql, so that we do not mess with the timestamp | |||||
frappe.conn.sql("""update `tab%s` set `_comments`=%s where name=%s""" % (self.doc.comment_doctype, | |||||
"%s", "%s"), (json.dumps(_comments), self.doc.comment_docname)) | |||||
def on_trash(self): | |||||
_comments = self.get_comments_from_parent() | |||||
for c in _comments: | |||||
if c.get("name")==self.doc.name: | |||||
_comments.remove(c) | |||||
self.update_comments_in_parent(_comments) | |||||
def on_doctype_update(): | |||||
if not frappe.conn.sql("""show index from `tabComment` | |||||
where Key_name="comment_doctype_docname_index" """): | |||||
frappe.conn.commit() | |||||
frappe.conn.sql("""alter table `tabComment` | |||||
add index comment_doctype_docname_index(comment_doctype, comment_docname)""") |
@@ -0,0 +1,52 @@ | |||||
# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors | |||||
# MIT License. See license.txt | |||||
from __future__ import unicode_literals | |||||
import frappe, unittest, json | |||||
# commented due to commits -- only run when comments are modified | |||||
# class TestComment(unittest.TestCase): | |||||
# def setUp(self): | |||||
# self.cleanup() | |||||
# self.test_rec = frappe.bean({ | |||||
# "doctype":"Event", | |||||
# "subject":"__Comment Test Event", | |||||
# "event_type": "Private", | |||||
# "starts_on": "2011-01-01 10:00:00", | |||||
# "ends_on": "2011-01-01 10:00:00", | |||||
# }).insert() | |||||
# | |||||
# def tearDown(self): | |||||
# self.cleanup() | |||||
# | |||||
# def cleanup(self): | |||||
# frappe.conn.sql("""delete from tabEvent where subject='__Comment Test Event'""") | |||||
# frappe.conn.sql("""delete from tabComment where comment='__Test Comment'""") | |||||
# frappe.conn.commit() | |||||
# if "_comments" in frappe.conn.get_table_columns("Event"): | |||||
# frappe.conn.commit() | |||||
# frappe.conn.sql("""alter table `tabEvent` drop column `_comments`""") | |||||
# | |||||
# def test_add_comment(self): | |||||
# self.comment = frappe.bean({ | |||||
# "doctype":"Comment", | |||||
# "comment_doctype": self.test_rec.doc.doctype, | |||||
# "comment_docname": self.test_rec.doc.name, | |||||
# "comment": "__Test Comment" | |||||
# }).insert() | |||||
# | |||||
# test_rec = frappe.doc(self.test_rec.doc.doctype, self.test_rec.doc.name) | |||||
# _comments = json.loads(test_rec.get("_comments")) | |||||
# self.assertTrue(_comments[0].get("comment")=="__Test Comment") | |||||
# | |||||
# def test_remove_comment(self): | |||||
# self.test_add_comment() | |||||
# frappe.delete_doc("Comment", self.comment.doc.name) | |||||
# test_rec = frappe.doc(self.test_rec.doc.doctype, self.test_rec.doc.name) | |||||
# _comments = json.loads(test_rec.get("_comments")) | |||||
# self.assertEqual(len(_comments), 0) | |||||
# | |||||
# | |||||
# if __name__=="__main__": | |||||
# unittest.main() |
@@ -0,0 +1,10 @@ | |||||
cur_frm.cscript.onload = function(doc) { | |||||
cur_frm.fields_dict.user.get_query = function() { | |||||
return { | |||||
query: "frappe.core.doctype.communication.communication.get_user" | |||||
} | |||||
}; | |||||
if(doc.content) | |||||
doc.content = frappe.utils.remove_script_and_style(doc.content); | |||||
} |
@@ -0,0 +1,159 @@ | |||||
# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors | |||||
# MIT License. See license.txt | |||||
from __future__ import unicode_literals | |||||
import frappe | |||||
import json | |||||
import urllib | |||||
from email.utils import formataddr | |||||
from frappe.website.utils import is_signup_enabled | |||||
from frappe.utils import get_url, cstr | |||||
from frappe.utils.email_lib.email_body import get_email | |||||
from frappe.utils.email_lib.smtp import send | |||||
from frappe.utils import scrub_urls | |||||
class DocType(): | |||||
def __init__(self, doc, doclist=None): | |||||
self.doc = doc | |||||
self.doclist = doclist | |||||
def get_parent_bean(self): | |||||
return frappe.bean(self.doc.parenttype, self.doc.parent) | |||||
def update_parent(self): | |||||
"""update status of parent Lead or Contact based on who is replying""" | |||||
observer = self.get_parent_bean().get_attr("on_communication") | |||||
if observer: | |||||
observer() | |||||
def on_update(self): | |||||
self.update_parent() | |||||
@frappe.whitelist() | |||||
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, attachments='[]', send_me_a_copy=False, set_lead=True, date=None): | |||||
if doctype and name and not frappe.has_permission(doctype, "email", name): | |||||
raise frappe.PermissionError("You are not allowed to send emails related to: {doctype} {name}".format( | |||||
doctype=doctype, name=name)) | |||||
_make(doctype=doctype, name=name, content=content, subject=subject, sent_or_received=sent_or_received, | |||||
sender=sender, recipients=recipients, communication_medium=communication_medium, send_email=send_email, | |||||
print_html=print_html, attachments=attachments, send_me_a_copy=send_me_a_copy, set_lead=set_lead, | |||||
date=date) | |||||
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, attachments='[]', send_me_a_copy=False, set_lead=True, date=None): | |||||
# add to Communication | |||||
sent_via = None | |||||
# since we are using fullname and email, | |||||
# if the fullname has any incompatible characters,formataddr can deal with it | |||||
try: | |||||
sender = json.loads(sender) | |||||
except ValueError: | |||||
pass | |||||
if isinstance(sender, (tuple, list)) and len(sender)==2: | |||||
sender = formataddr(sender) | |||||
comm = frappe.new_bean('Communication') | |||||
d = comm.doc | |||||
d.subject = subject | |||||
d.content = content | |||||
d.sent_or_received = sent_or_received | |||||
d.sender = sender or frappe.conn.get_value("Profile", frappe.session.user, "email") | |||||
d.recipients = recipients | |||||
# add as child | |||||
sent_via = frappe.get_obj(doctype, name) | |||||
d.parent = name | |||||
d.parenttype = doctype | |||||
d.parentfield = "communications" | |||||
if date: | |||||
d.communication_date = date | |||||
d.communication_medium = communication_medium | |||||
comm.ignore_permissions = True | |||||
comm.insert() | |||||
if send_email: | |||||
d = comm.doc | |||||
send_comm_email(d, name, sent_via, print_html, attachments, send_me_a_copy) | |||||
@frappe.whitelist() | |||||
def get_customer_supplier(args=None): | |||||
""" | |||||
Get Customer/Supplier, given a contact, if a unique match exists | |||||
""" | |||||
if not args: args = frappe.local.form_dict | |||||
if not args.get('contact'): | |||||
raise Exception, "Please specify a contact to fetch Customer/Supplier" | |||||
result = frappe.conn.sql("""\ | |||||
select customer, supplier | |||||
from `tabContact` | |||||
where name = %s""", args.get('contact'), as_dict=1) | |||||
if result and len(result)==1 and (result[0]['customer'] or result[0]['supplier']): | |||||
return { | |||||
'fieldname': result[0]['customer'] and 'customer' or 'supplier', | |||||
'value': result[0]['customer'] or result[0]['supplier'] | |||||
} | |||||
return {} | |||||
def send_comm_email(d, name, sent_via=None, print_html=None, attachments='[]', send_me_a_copy=False): | |||||
footer = None | |||||
if sent_via: | |||||
if hasattr(sent_via, "get_sender"): | |||||
d.sender = sent_via.get_sender(d) or d.sender | |||||
if hasattr(sent_via, "get_subject"): | |||||
d.subject = sent_via.get_subject(d) | |||||
if hasattr(sent_via, "get_content"): | |||||
d.content = sent_via.get_content(d) | |||||
footer = set_portal_link(sent_via, d) | |||||
send_print_in_body = frappe.conn.get_value("Email Settings", None, "send_print_in_body_and_attachment") | |||||
if not send_print_in_body: | |||||
d.content += "<p>Please see attachment for document details.</p>" | |||||
mail = get_email(d.recipients, sender=d.sender, subject=d.subject, | |||||
msg=d.content, footer=footer, print_html=print_html if send_print_in_body else None) | |||||
if send_me_a_copy: | |||||
mail.cc.append(frappe.conn.get_value("Profile", frappe.session.user, "email")) | |||||
if print_html: | |||||
print_html = scrub_urls(print_html) | |||||
mail.add_attachment(name.replace(' ','').replace('/','-') + '.html', print_html) | |||||
for a in json.loads(attachments): | |||||
try: | |||||
mail.attach_file(a) | |||||
except IOError, e: | |||||
frappe.msgprint("""Unable to find attachment %s. Please resend without attaching this file.""" % a, | |||||
raise_exception=True) | |||||
send(mail) | |||||
def set_portal_link(sent_via, comm): | |||||
"""set portal link in footer""" | |||||
footer = None | |||||
if is_signup_enabled() and hasattr(sent_via, "get_portal_page"): | |||||
portal_page = sent_via.get_portal_page() | |||||
if portal_page: | |||||
is_valid_recipient = cstr(sent_via.doc.email or sent_via.doc.email_id or | |||||
sent_via.doc.contact_email) in comm.recipients | |||||
if is_valid_recipient: | |||||
url = "%s/%s?name=%s" % (get_url(), portal_page, urllib.quote(sent_via.doc.name)) | |||||
footer = """<!-- Portal Link --><hr> | |||||
<a href="%s" target="_blank">View this on our website</a>""" % url | |||||
return footer |
@@ -0,0 +1,18 @@ | |||||
# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors | |||||
# MIT License. See license.txt | |||||
from __future__ import unicode_literals | |||||
import frappe | |||||
from frappe import form, msgprint | |||||
import frappe.defaults | |||||
class DocType: | |||||
def __init__(self, doc, doclist): | |||||
self.doc = doc | |||||
self.doclist = doclist | |||||
def on_update(self): | |||||
# clear cache on save | |||||
frappe.cache().delete_value('time_zone') | |||||
frappe.clear_cache() |
@@ -0,0 +1,73 @@ | |||||
// Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors | |||||
// MIT License. See license.txt | |||||
//168 | |||||
// Refresh | |||||
// -------- | |||||
cur_frm.cscript.refresh = function(doc, cdt, cdn) { | |||||
cur_frm.toggle_enable('dt', doc.__islocal); | |||||
cur_frm.cscript.dt(doc, cdt, cdn); | |||||
cur_frm.toggle_reqd('label', !doc.fieldname); | |||||
} | |||||
cur_frm.cscript.has_special_chars = function(t) { | |||||
var iChars = "!@#$%^&*()+=-[]\\\';,./{}|\":<>?"; | |||||
for (var i = 0; i < t.length; i++) { | |||||
if (iChars.indexOf(t.charAt(i)) != -1) { | |||||
return true; | |||||
} | |||||
} | |||||
return false; | |||||
} | |||||
// Label | |||||
// ------ | |||||
cur_frm.cscript.label = function(doc){ | |||||
if(doc.label && cur_frm.cscript.has_special_chars(doc.label)){ | |||||
cur_frm.fields_dict['label_help'].disp_area.innerHTML = '<font color = "red">Special Characters are not allowed</font>'; | |||||
doc.label = ''; | |||||
refresh_field('label'); | |||||
} | |||||
else | |||||
cur_frm.fields_dict['label_help'].disp_area.innerHTML = ''; | |||||
} | |||||
cur_frm.fields_dict['dt'].get_query = function(doc, dt, dn) { | |||||
filters = [ | |||||
['DocType', 'issingle', '=', 0], | |||||
]; | |||||
if(user!=="Administrator") { | |||||
filters.push(['DocType', 'module', '!=', 'Core']) | |||||
} | |||||
return filters | |||||
} | |||||
cur_frm.cscript.fieldtype = function(doc, dt, dn) { | |||||
if(doc.fieldtype == 'Link') cur_frm.fields_dict['options_help'].disp_area.innerHTML = 'Please enter name of the document you want this field to be linked to in <b>Options</b>.<br> Eg.: Customer'; | |||||
else if(doc.fieldtype == 'Select') cur_frm.fields_dict['options_help'].disp_area.innerHTML = 'Please enter values in <b>Options</b> separated by enter. <br>Eg.: <b>Field:</b> Country <br><b>Options:</b><br>China<br>India<br>United States<br><br><b> OR </b><br>You can also link it to existing Documents.<br>Eg.: <b>link:</b>Customer'; | |||||
else cur_frm.fields_dict['options_help'].disp_area.innerHTML = ''; | |||||
} | |||||
cur_frm.cscript.dt = function(doc, dt, dn) { | |||||
if(!doc.dt) { | |||||
set_field_options('insert_after', ''); | |||||
return; | |||||
} | |||||
return frappe.call({ | |||||
method: 'frappe.core.doctype.custom_field.custom_field.get_fields_label', | |||||
args: { doctype: doc.dt, fieldname: doc.fieldname }, | |||||
callback: function(r, rt) { | |||||
doc = locals[doc.doctype][doc.name]; | |||||
var insert_after_val = null; | |||||
if(doc.insert_after) { | |||||
insert_after_val = doc.insert_after; | |||||
} | |||||
set_field_options('insert_after', r.message, insert_after_val); | |||||
} | |||||
}); | |||||
} |
@@ -0,0 +1,127 @@ | |||||
# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors | |||||
# MIT License. See license.txt | |||||
from __future__ import unicode_literals | |||||
import frappe | |||||
from frappe.utils import cint, cstr | |||||
from frappe import _ | |||||
class DocType: | |||||
def __init__(self, d, dl): | |||||
self.doc, self.doclist = d, dl | |||||
def autoname(self): | |||||
self.set_fieldname() | |||||
self.doc.name = self.doc.dt + "-" + self.doc.fieldname | |||||
def set_fieldname(self): | |||||
if not self.doc.fieldname: | |||||
if not self.doc.label: | |||||
frappe.throw(_("Label is mandatory")) | |||||
# remove special characters from fieldname | |||||
self.doc.fieldname = filter(lambda x: x.isdigit() or x.isalpha() or '_', | |||||
cstr(self.doc.label).lower().replace(' ','_')) | |||||
def validate(self): | |||||
from frappe.model.doctype import get | |||||
temp_doclist = get(self.doc.dt).get_parent_doclist() | |||||
# set idx | |||||
if not self.doc.idx: | |||||
max_idx = max(d.idx for d in temp_doclist if d.doctype=='DocField') | |||||
self.doc.idx = cint(max_idx) + 1 | |||||
def on_update(self): | |||||
# validate field | |||||
from frappe.core.doctype.doctype.doctype import validate_fields_for_doctype | |||||
validate_fields_for_doctype(self.doc.dt) | |||||
frappe.clear_cache(doctype=self.doc.dt) | |||||
# create property setter to emulate insert after | |||||
self.create_property_setter() | |||||
# update the schema | |||||
from frappe.model.db_schema import updatedb | |||||
updatedb(self.doc.dt) | |||||
def on_trash(self): | |||||
# delete property setter entries | |||||
frappe.conn.sql("""\ | |||||
DELETE FROM `tabProperty Setter` | |||||
WHERE doc_type = %s | |||||
AND field_name = %s""", | |||||
(self.doc.dt, self.doc.fieldname)) | |||||
frappe.clear_cache(doctype=self.doc.dt) | |||||
def create_property_setter(self): | |||||
if not self.doc.insert_after: return | |||||
idx_label_list, field_list = get_fields_label(self.doc.dt, 0) | |||||
label_index = idx_label_list.index(self.doc.insert_after) | |||||
if label_index==-1: return | |||||
prev_field = field_list[label_index] | |||||
frappe.conn.sql("""\ | |||||
DELETE FROM `tabProperty Setter` | |||||
WHERE doc_type = %s | |||||
AND field_name = %s | |||||
AND property = 'previous_field'""", (self.doc.dt, self.doc.fieldname)) | |||||
frappe.make_property_setter({ | |||||
"doctype":self.doc.dt, | |||||
"fieldname": self.doc.fieldname, | |||||
"property": "previous_field", | |||||
"value": prev_field | |||||
}) | |||||
@frappe.whitelist() | |||||
def get_fields_label(dt=None, form=1): | |||||
""" | |||||
if form=1: Returns a string of field labels separated by \n | |||||
if form=0: Returns lists of field labels and field names | |||||
""" | |||||
import frappe | |||||
from frappe.utils import cstr | |||||
import frappe.model.doctype | |||||
fieldname = None | |||||
if not dt: | |||||
dt = frappe.form_dict.get('doctype') | |||||
fieldname = frappe.form_dict.get('fieldname') | |||||
if not dt: return "" | |||||
doclist = frappe.model.doctype.get(dt) | |||||
docfields = sorted(doclist.get({"parent": dt, "doctype": "DocField"}), | |||||
key=lambda d: d.idx) | |||||
if fieldname: | |||||
idx_label_list = [cstr(d.label) or cstr(d.fieldname) or cstr(d.fieldtype) | |||||
for d in docfields if d.fieldname != fieldname] | |||||
else: | |||||
idx_label_list = [cstr(d.label) or cstr(d.fieldname) or cstr(d.fieldtype) | |||||
for d in docfields] | |||||
if form: | |||||
return "\n".join(idx_label_list) | |||||
else: | |||||
# return idx_label_list, field_list | |||||
field_list = [cstr(d.fieldname) for d in docfields] | |||||
return idx_label_list, field_list | |||||
def create_custom_field_if_values_exist(doctype, df): | |||||
df = frappe._dict(df) | |||||
if df.fieldname in frappe.conn.get_table_columns(doctype) and \ | |||||
frappe.conn.sql("""select count(*) from `tab{doctype}` | |||||
where ifnull({fieldname},'')!=''""".format(doctype=doctype, fieldname=df.fieldname))[0][0] and \ | |||||
not frappe.conn.get_value("Custom Field", {"dt": doctype, "fieldname": df.fieldname}): | |||||
frappe.bean({ | |||||
"doctype":"Custom Field", | |||||
"dt": doctype, | |||||
"permlevel": df.permlevel or 0, | |||||
"label": df.label, | |||||
"fieldname": df.fieldname, | |||||
"fieldtype": df.fieldtype, | |||||
"options": df.options, | |||||
"insert_after": df.insert_after | |||||
}).insert() | |||||
@@ -0,0 +1,19 @@ | |||||
# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors | |||||
# MIT License. See license.txt | |||||
from __future__ import unicode_literals | |||||
import frappe | |||||
from frappe.utils import cstr | |||||
class DocType: | |||||
def __init__(self, d, dl): | |||||
self.doc, self.doclist = d, dl | |||||
def autoname(self): | |||||
self.doc.name = self.doc.dt + "-" + self.doc.script_type | |||||
def on_update(self): | |||||
frappe.clear_cache(doctype=self.doc.dt) | |||||
def on_trash(self): | |||||
frappe.clear_cache(doctype=self.doc.dt) | |||||
@@ -0,0 +1,241 @@ | |||||
// Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors | |||||
// MIT License. See license.txt | |||||
$(cur_frm.wrapper).on("grid-row-render", function(e, grid_row) { | |||||
if(grid_row.doc && grid_row.doc.fieldtype=="Section Break") { | |||||
$(grid_row.row).css({"font-weight": "bold"}); | |||||
} | |||||
}) | |||||
cur_frm.cscript.doc_type = function() { | |||||
return cur_frm.call({ | |||||
method: "get", | |||||
doc: cur_frm.doc, | |||||
callback: function(r) { | |||||
cur_frm.refresh(); | |||||
} | |||||
}); | |||||
} | |||||
cur_frm.cscript.onload = function(doc, dt, dn) { | |||||
cur_frm.fields_dict.fields.grid.cannot_add_rows = true; | |||||
cur_frm.add_fields_help(); | |||||
} | |||||
cur_frm.fields_dict.doc_type.get_query = function(doc, dt, dn) { | |||||
return{ | |||||
filters:[ | |||||
['DocType', 'issingle', '=', 0], | |||||
['DocType', 'in_create', '=', 0], | |||||
['DocType', 'name', 'not in', 'DocType, DocField, DocPerm, Profile, Role, UserRole,\ | |||||
Page, Page Role, Module Def, Print Format, Report, Customize Form, Customize Form Field'] | |||||
] | |||||
} | |||||
} | |||||
cur_frm.cscript.refresh = function() { | |||||
cur_frm.disable_save(); | |||||
cur_frm.frm_head.appframe.iconbar.clear("1"); | |||||
cur_frm.appframe.set_title_right("Update", function() { | |||||
if(cur_frm.doc.doc_type) { | |||||
return cur_frm.call({ | |||||
doc: cur_frm.doc, | |||||
method: "post", | |||||
callback: function(r) { | |||||
if(!r.exc && r.server_messages) { | |||||
cur_frm.script_manager.trigger("doc_type"); | |||||
cur_frm.frm_head.set_label(['Updated', 'label-success']); | |||||
} | |||||
} | |||||
}); | |||||
} | |||||
}); | |||||
cur_frm.add_custom_button('Refresh Form', function() { | |||||
cur_frm.script_manager.trigger("doc_type"); | |||||
}, "icon-refresh"); | |||||
cur_frm.add_custom_button('Reset to defaults', function() { | |||||
cur_frm.confirm('This will <b>remove the customizations</b> defined for this form.<br /><br />' | |||||
+ 'Are you sure you want to <i>reset to defaults</i>?', cur_frm.doc, cur_frm.doctype, cur_frm.docname); | |||||
}, "icon-eraser"); | |||||
if(!cur_frm.doc.doc_type) { | |||||
var frm_head = cur_frm.frm_head.appframe; | |||||
$(frm_head.buttons['Update']).prop('disabled', true); | |||||
$(frm_head.buttons['Refresh Form']).prop('disabled', true); | |||||
$(frm_head.buttons['Reset to defaults']).prop('disabled', true); | |||||
} | |||||
cur_frm.cscript.hide_allow_attach(cur_frm.doc); | |||||
if(frappe.route_options) { | |||||
frappe.model.set_value("Customize Form", null, "doc_type", frappe.route_options.doctype) | |||||
frappe.route_options = null; | |||||
} | |||||
} | |||||
cur_frm.cscript.hide_allow_attach = function(doc) { | |||||
var allow_attach_list = ['Website Settings', 'Web Page', 'Timesheet', 'Ticket', | |||||
'Support Ticket', 'Supplier', 'Style Settings', 'Stock Reconciliation', | |||||
'Stock Entry', 'Serial No', 'Sales Order', 'Sales Invoice', | |||||
'Quotation', 'Question', 'Purchase Receipt', 'Purchase Order', | |||||
'Project', 'Profile', 'Production Order', 'Product', 'Print Format', | |||||
'Price List', 'Purchase Invoice', 'Page', 'Module Def', | |||||
'Maintenance Visit', 'Maintenance Schedule', 'Letter Head', | |||||
'Leave Application', 'Lead', 'Journal Voucher', 'Item', 'Material Request', | |||||
'Expense Claim', 'Opportunity', 'Employee', 'Delivery Note', | |||||
'Customer Issue', 'Customer', 'Contact Us Settings', 'Company', | |||||
'Blog Post', 'BOM', 'About Us Settings', 'Batch']; | |||||
if(inList(allow_attach_list, doc.doc_type)) { | |||||
unhide_field('allow_attach'); | |||||
} else { | |||||
hide_field('allow_attach'); | |||||
} | |||||
} | |||||
cur_frm.confirm = function(msg, doc, dt, dn) { | |||||
var d = new frappe.ui.Dialog({ | |||||
title: 'Reset To Defaults', | |||||
width: 500 | |||||
}); | |||||
$y(d.body, {padding: '32px', textAlign: 'center'}); | |||||
$a(d.body, 'div', '', '', msg); | |||||
var button_wrapper = $a(d.body, 'div'); | |||||
$y(button_wrapper, {paddingTop: '15px'}); | |||||
var proceed_btn = $btn(button_wrapper, 'Proceed', function() { | |||||
return cur_frm.call({ | |||||
doc: cur_frm.doc, | |||||
method: "delete", | |||||
callback: function(r) { | |||||
if(r.exc) { | |||||
msgprint(r.exc); | |||||
} else { | |||||
cur_frm.confirm.dialog.hide(); | |||||
cur_frm.refresh(); | |||||
cur_frm.frm_head.set_label(['Saved', 'label-success']); | |||||
} | |||||
} | |||||
}); | |||||
}); | |||||
$y(proceed_btn, {marginRight: '20px', fontWeight: 'bold'}); | |||||
var cancel_btn = $btn(button_wrapper, 'Cancel', function() { | |||||
cur_frm.confirm.dialog.hide(); | |||||
}); | |||||
$(cancel_btn).addClass('btn-small btn-info'); | |||||
$y(cancel_btn, {fontWeight: 'bold'}); | |||||
cur_frm.confirm.dialog = d; | |||||
d.show(); | |||||
} | |||||
cur_frm.add_fields_help = function() { | |||||
$(cur_frm.grids[0].parent).before( | |||||
'<div style="padding: 10px">\ | |||||
<a id="fields_help" class="link_type">Help</a>\ | |||||
</div>'); | |||||
$('#fields_help').click(function() { | |||||
var d = new frappe.ui.Dialog({ | |||||
title: 'Help: Field Properties', | |||||
width: 600 | |||||
}); | |||||
var help = | |||||
"<table cellspacing='25'>\ | |||||
<tr>\ | |||||
<td><b>Label</b></td>\ | |||||
<td>Set the display label for the field</td>\ | |||||
</tr>\ | |||||
<tr>\ | |||||
<td><b>Type</b></td>\ | |||||
<td>Change type of field. (Currently, Type change is \ | |||||
allowed among 'Currency and Float')</td>\ | |||||
</tr>\ | |||||
<tr>\ | |||||
<td width='25%'><b>Options</b></td>\ | |||||
<td width='75%'>Specify the value of the field</td>\ | |||||
</tr>\ | |||||
<tr>\ | |||||
<td><b>Perm Level</b></td>\ | |||||
<td>\ | |||||
Assign a permission level to the field.<br />\ | |||||
(Permissions can be managed via Setup > Permission Manager)\ | |||||
</td>\ | |||||
</tr>\ | |||||
<tr>\ | |||||
<td><b>Width</b></td>\ | |||||
<td>\ | |||||
Width of the input box<br />\ | |||||
Example: <i>120px</i>\ | |||||
</td>\ | |||||
</tr>\ | |||||
<tr>\ | |||||
<td><b>Reqd</b></td>\ | |||||
<td>Mark the field as Mandatory</td>\ | |||||
</tr>\ | |||||
<tr>\ | |||||
<td><b>In Filter</b></td>\ | |||||
<td>Use the field to filter records</td>\ | |||||
</tr>\ | |||||
<tr>\ | |||||
<td><b>Hidden</b></td>\ | |||||
<td>Hide field in form</td>\ | |||||
</tr>\ | |||||
<tr>\ | |||||
<td><b>Print Hide</b></td>\ | |||||
<td>Hide field in Standard Print Format</td>\ | |||||
</tr>\ | |||||
<tr>\ | |||||
<td><b>Report Hide</b></td>\ | |||||
<td>Hide field in Report Builder</td>\ | |||||
</tr>\ | |||||
<tr>\ | |||||
<td><b>Allow on Submit</b></td>\ | |||||
<td>Allow field to remain editable even after submission</td>\ | |||||
</tr>\ | |||||
<tr>\ | |||||
<td><b>Depends On</b></td>\ | |||||
<td>\ | |||||
Show field if a condition is met<br />\ | |||||
Example: <code>eval:doc.status=='Cancelled'</code>\ | |||||
on a field like \"reason_for_cancellation\" will reveal \ | |||||
\"Reason for Cancellation\" only if the record is Cancelled.\ | |||||
</td>\ | |||||
</tr>\ | |||||
<tr>\ | |||||
<td><b>Description</b></td>\ | |||||
<td>Show a description below the field</td>\ | |||||
</tr>\ | |||||
<tr>\ | |||||
<td><b>Default</b></td>\ | |||||
<td>Specify a default value</td>\ | |||||
</tr>\ | |||||
<tr>\ | |||||
<td></td>\ | |||||
<td><a class='link_type' \ | |||||
onclick='cur_frm.fields_help_dialog.hide()'\ | |||||
style='color:grey'>Press Esc to close</a>\ | |||||
</td>\ | |||||
</tr>\ | |||||
</table>" | |||||
$y(d.body, {padding: '32px', textAlign: 'center', lineHeight: '200%'}); | |||||
$a(d.body, 'div', '', {textAlign: 'left'}, help); | |||||
d.show(); | |||||
cur_frm.fields_help_dialog = d; | |||||
}); | |||||
} |
@@ -0,0 +1,351 @@ | |||||
# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors | |||||
# MIT License. See license.txt | |||||
from __future__ import unicode_literals | |||||
""" | |||||
Customize Form is a Single DocType used to mask the Property Setter | |||||
Thus providing a better UI from user perspective | |||||
""" | |||||
import frappe, json | |||||
from frappe.utils import cstr | |||||
class DocType: | |||||
def __init__(self, doc, doclist=[]): | |||||
self.doc, self.doclist = doc, doclist | |||||
self.doctype_properties = [ | |||||
'search_fields', | |||||
'default_print_format', | |||||
'read_only_onload', | |||||
'allow_print', | |||||
'allow_email', | |||||
'allow_copy', | |||||
'allow_attach', | |||||
'max_attachments' | |||||
] | |||||
self.docfield_properties = [ | |||||
'idx', | |||||
'label', | |||||
'fieldtype', | |||||
'fieldname', | |||||
'options', | |||||
'permlevel', | |||||
'width', | |||||
'print_width', | |||||
'reqd', | |||||
'ignore_restrictions', | |||||
'in_filter', | |||||
'in_list_view', | |||||
'hidden', | |||||
'print_hide', | |||||
'report_hide', | |||||
'allow_on_submit', | |||||
'depends_on', | |||||
'description', | |||||
'default', | |||||
'name', | |||||
] | |||||
self.property_restrictions = { | |||||
'fieldtype': [['Currency', 'Float'], ['Small Text', 'Data'], ['Text', 'Text Editor', 'Code']], | |||||
} | |||||
def get(self): | |||||
""" | |||||
Gets DocFields applied with Property Setter customizations via Customize Form Field | |||||
""" | |||||
self.clear() | |||||
if self.doc.doc_type: | |||||
from frappe.model.doc import addchild | |||||
for d in self.get_ref_doclist(): | |||||
if d.doctype=='DocField': | |||||
new = addchild(self.doc, 'fields', 'Customize Form Field', | |||||
self.doclist) | |||||
self.set( | |||||
{ | |||||
'list': self.docfield_properties, | |||||
'doc' : d, | |||||
'doc_to_set': new | |||||
} | |||||
) | |||||
elif d.doctype=='DocType': | |||||
self.set({ 'list': self.doctype_properties, 'doc': d }) | |||||
def get_ref_doclist(self): | |||||
""" | |||||
* Gets doclist of type self.doc.doc_type | |||||
* Applies property setter properties on the doclist | |||||
* returns the modified doclist | |||||
""" | |||||
from frappe.model.doctype import get | |||||
ref_doclist = get(self.doc.doc_type) | |||||
ref_doclist = frappe.doclist([ref_doclist[0]] | |||||
+ ref_doclist.get({"parent": self.doc.doc_type})) | |||||
return ref_doclist | |||||
def clear(self): | |||||
""" | |||||
Clear fields in the doc | |||||
""" | |||||
# Clear table before adding new doctype's fields | |||||
self.doclist = self.doc.clear_table(self.doclist, 'fields') | |||||
self.set({ 'list': self.doctype_properties, 'value': None }) | |||||
def set(self, args): | |||||
""" | |||||
Set a list of attributes of a doc to a value | |||||
or to attribute values of a doc passed | |||||
args can contain: | |||||
* list --> list of attributes to set | |||||
* doc_to_set --> defaults to self.doc | |||||
* value --> to set all attributes to one value eg. None | |||||
* doc --> copy attributes from doc to doc_to_set | |||||
""" | |||||
if not 'doc_to_set' in args: | |||||
args['doc_to_set'] = self.doc | |||||
if 'list' in args: | |||||
if 'value' in args: | |||||
for f in args['list']: | |||||
args['doc_to_set'].fields[f] = None | |||||
elif 'doc' in args: | |||||
for f in args['list']: | |||||
args['doc_to_set'].fields[f] = args['doc'].fields.get(f) | |||||
else: | |||||
frappe.msgprint("Please specify args['list'] to set", raise_exception=1) | |||||
def post(self): | |||||
""" | |||||
Save diff between Customize Form Bean and DocType Bean as property setter entries | |||||
""" | |||||
if self.doc.doc_type: | |||||
from frappe.model import doc | |||||
from frappe.core.doctype.doctype.doctype import validate_fields_for_doctype | |||||
this_doclist = frappe.doclist([self.doc] + self.doclist) | |||||
ref_doclist = self.get_ref_doclist() | |||||
dt_doclist = doc.get('DocType', self.doc.doc_type) | |||||
# get a list of property setter docs | |||||
self.idx_dirty = False | |||||
diff_list = self.diff(this_doclist, ref_doclist, dt_doclist) | |||||
if self.idx_dirty: | |||||
self.make_idx_property_setter(this_doclist, diff_list) | |||||
self.set_properties(diff_list) | |||||
validate_fields_for_doctype(self.doc.doc_type) | |||||
frappe.clear_cache(doctype=self.doc.doc_type) | |||||
frappe.msgprint("Updated") | |||||
def diff(self, new_dl, ref_dl, dt_dl): | |||||
""" | |||||
Get difference between new_dl doclist and ref_dl doclist | |||||
then check how it differs from dt_dl i.e. default doclist | |||||
""" | |||||
import re | |||||
self.defaults = self.get_defaults() | |||||
diff_list = [] | |||||
for new_d in new_dl: | |||||
for ref_d in ref_dl: | |||||
if ref_d.doctype == 'DocField' and new_d.name == ref_d.name: | |||||
for prop in self.docfield_properties: | |||||
# do not set forbidden properties like idx | |||||
if prop=="idx": | |||||
if ref_d.idx != new_d.idx: | |||||
self.idx_dirty = True | |||||
continue | |||||
# check if its custom field: | |||||
if ref_d.get("__custom_field"): | |||||
# update custom field | |||||
if self.has_property_changed(ref_d, new_d, prop): | |||||
# using set_value not bean because validations are called | |||||
# in the end anyways | |||||
frappe.conn.set_value("Custom Field", ref_d.name, prop, new_d.get(prop)) | |||||
else: | |||||
d = self.prepare_to_set(prop, new_d, ref_d, dt_dl) | |||||
if d: diff_list.append(d) | |||||
break | |||||
elif ref_d.doctype == 'DocType' and new_d.doctype == 'Customize Form': | |||||
for prop in self.doctype_properties: | |||||
d = self.prepare_to_set(prop, new_d, ref_d, dt_dl) | |||||
if d: diff_list.append(d) | |||||
break | |||||
return diff_list | |||||
def get_defaults(self): | |||||
""" | |||||
Get fieldtype and default value for properties of a field | |||||
""" | |||||
df_defaults = frappe.conn.sql(""" | |||||
SELECT fieldname, fieldtype, `default`, label | |||||
FROM `tabDocField` | |||||
WHERE parent='DocField' or parent='DocType'""", as_dict=1) | |||||
defaults = {} | |||||
for d in df_defaults: | |||||
defaults[d['fieldname']] = d | |||||
defaults['idx'] = {'fieldname' : 'idx', 'fieldtype' : 'Int', 'default' : 1, 'label' : 'idx'} | |||||
defaults['previous_field'] = {'fieldname' : 'previous_field', 'fieldtype' : 'Data', 'default' : None, 'label' : 'Previous Field'} | |||||
return defaults | |||||
def has_property_changed(self, ref_d, new_d, prop): | |||||
return new_d.fields.get(prop) != ref_d.fields.get(prop) \ | |||||
and not \ | |||||
( \ | |||||
new_d.fields.get(prop) in [None, 0] \ | |||||
and ref_d.fields.get(prop) in [None, 0] \ | |||||
) and not \ | |||||
( \ | |||||
new_d.fields.get(prop) in [None, ''] \ | |||||
and ref_d.fields.get(prop) in [None, ''] \ | |||||
) | |||||
def prepare_to_set(self, prop, new_d, ref_d, dt_doclist, delete=0): | |||||
""" | |||||
Prepares docs of property setter | |||||
sets delete property if it is required to be deleted | |||||
""" | |||||
# Check if property has changed compared to when it was loaded | |||||
if self.has_property_changed(ref_d, new_d, prop): | |||||
#frappe.msgprint("new: " + str(new_d.fields[prop]) + " | old: " + str(ref_d.fields[prop])) | |||||
# Check if the new property is same as that in original doctype | |||||
# If yes, we need to delete the property setter entry | |||||
for dt_d in dt_doclist: | |||||
if dt_d.name == ref_d.name \ | |||||
and (new_d.fields.get(prop) == dt_d.fields.get(prop) \ | |||||
or \ | |||||
( \ | |||||
new_d.fields.get(prop) in [None, 0] \ | |||||
and dt_d.fields.get(prop) in [None, 0] \ | |||||
) or \ | |||||
( \ | |||||
new_d.fields.get(prop) in [None, ''] \ | |||||
and dt_d.fields.get(prop) in [None, ''] \ | |||||
)): | |||||
delete = 1 | |||||
break | |||||
value = new_d.fields.get(prop) | |||||
if prop in self.property_restrictions: | |||||
allow_change = False | |||||
for restrict_list in self.property_restrictions.get(prop): | |||||
if value in restrict_list and \ | |||||
ref_d.fields.get(prop) in restrict_list: | |||||
allow_change = True | |||||
break | |||||
if not allow_change: | |||||
frappe.msgprint("""\ | |||||
You cannot change '%s' of '%s' from '%s' to '%s'. | |||||
%s can only be changed among %s. | |||||
<i>Ignoring this change and saving.</i>""" % \ | |||||
(self.defaults.get(prop, {}).get("label") or prop, | |||||
new_d.fields.get("label") or new_d.fields.get("idx"), | |||||
ref_d.fields.get(prop), value, | |||||
self.defaults.get(prop, {}).get("label") or prop, | |||||
" -or- ".join([", ".join(r) for r in \ | |||||
self.property_restrictions.get(prop)])), raise_exception=True) | |||||
return None | |||||
# If the above conditions are fulfilled, | |||||
# create a property setter doc, but dont save it yet. | |||||
from frappe.model.doc import Document | |||||
d = Document('Property Setter') | |||||
d.doctype_or_field = ref_d.doctype=='DocField' and 'DocField' or 'DocType' | |||||
d.doc_type = self.doc.doc_type | |||||
d.field_name = ref_d.fieldname | |||||
d.property = prop | |||||
d.value = value | |||||
d.property_type = self.defaults[prop]['fieldtype'] | |||||
#d.default_value = self.defaults[prop]['default'] | |||||
if delete: d.delete = 1 | |||||
if d.select_item: | |||||
d.select_item = self.remove_forbidden(d.select_item) | |||||
# return the property setter doc | |||||
return d | |||||
else: return None | |||||
def make_idx_property_setter(self, doclist, diff_list): | |||||
fields = [] | |||||
doclist.sort(lambda a, b: a.idx < b.idx) | |||||
for d in doclist: | |||||
if d.doctype=="Customize Form Field": | |||||
fields.append(d.fieldname) | |||||
d = frappe.doc('Property Setter') | |||||
d.doctype_or_field = 'DocType' | |||||
d.doc_type = self.doc.doc_type | |||||
d.property = "_idx" | |||||
d.value = json.dumps(fields) | |||||
d.property_type = "Text" | |||||
diff_list.append(d) | |||||
def set_properties(self, ps_doclist): | |||||
""" | |||||
* Delete a property setter entry | |||||
+ if it already exists | |||||
+ if marked for deletion | |||||
* Save the property setter doc in the list | |||||
""" | |||||
for d in ps_doclist: | |||||
# Delete existing property setter entry | |||||
if not d.fields.get("field_name"): | |||||
frappe.conn.sql(""" | |||||
DELETE FROM `tabProperty Setter` | |||||
WHERE doc_type = %(doc_type)s | |||||
AND property = %(property)s""", d.fields) | |||||
else: | |||||
frappe.conn.sql(""" | |||||
DELETE FROM `tabProperty Setter` | |||||
WHERE doc_type = %(doc_type)s | |||||
AND field_name = %(field_name)s | |||||
AND property = %(property)s""", d.fields) | |||||
# Save the property setter doc if not marked for deletion i.e. delete=0 | |||||
if not d.delete: | |||||
d.insert() | |||||
def delete(self): | |||||
""" | |||||
Deletes all property setter entries for the selected doctype | |||||
and resets it to standard | |||||
""" | |||||
if self.doc.doc_type: | |||||
frappe.conn.sql(""" | |||||
DELETE FROM `tabProperty Setter` | |||||
WHERE doc_type = %s""", self.doc.doc_type) | |||||
frappe.clear_cache(doctype=self.doc.doc_type) | |||||
self.get() | |||||
def remove_forbidden(self, string): | |||||
""" | |||||
Replace forbidden characters with a space | |||||
""" | |||||
forbidden = ['%', "'", '"', '#', '*', '?', '`'] | |||||
for f in forbidden: | |||||
string.replace(f, ' ') |
@@ -0,0 +1,9 @@ | |||||
# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors | |||||
# MIT License. See license.txt | |||||
from __future__ import unicode_literals | |||||
import frappe | |||||
class DocType: | |||||
def __init__(self, d, dl): | |||||
self.doc, self.doclist = d, dl |
@@ -0,0 +1,9 @@ | |||||
# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors | |||||
# MIT License. See license.txt | |||||
from __future__ import unicode_literals | |||||
import frappe | |||||
class DocType: | |||||
def __init__(self, d, dl): | |||||
self.doc, self.doclist = d, dl |
@@ -0,0 +1,22 @@ | |||||
# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors | |||||
# MIT License. See license.txt | |||||
from __future__ import unicode_literals | |||||
import frappe | |||||
class DocType: | |||||
def __init__(self, d, dl): | |||||
self.doc, self.doclist = d, dl | |||||
def on_doctype_update(): | |||||
if not frappe.conn.sql("""show index from `tabDefaultValue` | |||||
where Key_name="defaultvalue_parent_defkey_index" """): | |||||
frappe.conn.commit() | |||||
frappe.conn.sql("""alter table `tabDefaultValue` | |||||
add index defaultvalue_parent_defkey_index(parent, defkey)""") | |||||
if not frappe.conn.sql("""show index from `tabDefaultValue` | |||||
where Key_name="defaultvalue_parent_parenttype_index" """): | |||||
frappe.conn.commit() | |||||
frappe.conn.sql("""alter table `tabDefaultValue` | |||||
add index defaultvalue_parent_parenttype_index(parent, parenttype)""") |
@@ -0,0 +1,9 @@ | |||||
# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors | |||||
# MIT License. See license.txt | |||||
from __future__ import unicode_literals | |||||
import frappe | |||||
class DocType: | |||||
def __init__(self, d, dl): | |||||
self.doc, self.doclist = d, dl |
@@ -0,0 +1,9 @@ | |||||
# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors | |||||
# MIT License. See license.txt | |||||
from __future__ import unicode_literals | |||||
import frappe | |||||
class DocType: | |||||
def __init__(self, d, dl): | |||||
self.doc, self.doclist = d, dl |
@@ -0,0 +1,350 @@ | |||||
# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors | |||||
# MIT License. See license.txt | |||||
from __future__ import unicode_literals | |||||
import frappe | |||||
from frappe import msgprint, _ | |||||
import os | |||||
from frappe.utils import now, cint | |||||
from frappe.model import no_value_fields | |||||
class DocType: | |||||
def __init__(self, doc=None, doclist=[]): | |||||
self.doc = doc | |||||
self.doclist = doclist | |||||
def validate(self): | |||||
if not frappe.conf.get("developer_mode"): | |||||
frappe.throw("Not in Developer Mode! Set in site_config.json") | |||||
for c in [".", "/", "#", "&", "=", ":", "'", '"']: | |||||
if c in self.doc.name: | |||||
frappe.msgprint(c + " not allowed in name", raise_exception=1) | |||||
self.validate_series() | |||||
self.scrub_field_names() | |||||
self.validate_title_field() | |||||
validate_fields(self.doclist.get({"doctype":"DocField"})) | |||||
validate_permissions(self.doclist.get({"doctype":"DocPerm"})) | |||||
self.make_amendable() | |||||
self.check_link_replacement_error() | |||||
def change_modified_of_parent(self): | |||||
if frappe.flags.in_import: | |||||
return | |||||
parent_list = frappe.conn.sql("""SELECT parent | |||||
from tabDocField where fieldtype="Table" and options="%s" """ % self.doc.name) | |||||
for p in parent_list: | |||||
frappe.conn.sql('''UPDATE tabDocType SET modified="%s" | |||||
WHERE `name`="%s"''' % (now(), p[0])) | |||||
def scrub_field_names(self): | |||||
restricted = ('name','parent','idx','owner','creation','modified','modified_by', | |||||
'parentfield','parenttype',"file_list") | |||||
for d in self.doclist: | |||||
if d.parent and d.fieldtype: | |||||
if (not d.fieldname): | |||||
if d.label: | |||||
d.fieldname = d.label.strip().lower().replace(' ','_') | |||||
if d.fieldname in restricted: | |||||
d.fieldname = d.fieldname + '1' | |||||
else: | |||||
d.fieldname = d.fieldtype.lower().replace(" ","_") + "_" + str(d.idx) | |||||
def validate_title_field(self): | |||||
if self.doc.title_field and \ | |||||
self.doc.title_field not in [d.fieldname for d in self.doclist.get({"doctype":"DocField"})]: | |||||
frappe.throw(_("Title field must be a valid fieldname")) | |||||
def validate_series(self, autoname=None, name=None): | |||||
if not autoname: autoname = self.doc.autoname | |||||
if not name: name = self.doc.name | |||||
if not autoname and self.doclist.get({"fieldname":"naming_series"}): | |||||
self.doc.autoname = "naming_series:" | |||||
if autoname and (not autoname.startswith('field:')) and (not autoname.startswith('eval:')) \ | |||||
and (not autoname=='Prompt') and (not autoname.startswith('naming_series:')): | |||||
prefix = autoname.split('.')[0] | |||||
used_in = frappe.conn.sql('select name from tabDocType where substring_index(autoname, ".", 1) = %s and name!=%s', (prefix, name)) | |||||
if used_in: | |||||
msgprint('<b>Series already in use:</b> The series "%s" is already used in "%s"' % (prefix, used_in[0][0]), raise_exception=1) | |||||
def on_update(self): | |||||
from frappe.model.db_schema import updatedb | |||||
updatedb(self.doc.name) | |||||
self.change_modified_of_parent() | |||||
make_module_and_roles(self.doclist) | |||||
from frappe import conf | |||||
if (not frappe.flags.in_import) and conf.get('developer_mode') or 0: | |||||
self.export_doc() | |||||
self.make_controller_template() | |||||
# update index | |||||
if not self.doc.custom: | |||||
from frappe.model.code import load_doctype_module | |||||
module = load_doctype_module( self.doc.name, self.doc.module) | |||||
if hasattr(module, "on_doctype_update"): | |||||
module.on_doctype_update() | |||||
frappe.clear_cache(doctype=self.doc.name) | |||||
def check_link_replacement_error(self): | |||||
for d in self.doclist.get({"doctype":"DocField", "fieldtype":"Select"}): | |||||
if (frappe.conn.get_value("DocField", d.name, "options") or "").startswith("link:") \ | |||||
and not d.options.startswith("link:"): | |||||
frappe.msgprint("link: type Select fields are getting replaced. Please check for %s" % d.label, | |||||
raise_exception=True) | |||||
def on_trash(self): | |||||
frappe.conn.sql("delete from `tabCustom Field` where dt = %s", self.doc.name) | |||||
frappe.conn.sql("delete from `tabCustom Script` where dt = %s", self.doc.name) | |||||
frappe.conn.sql("delete from `tabProperty Setter` where doc_type = %s", self.doc.name) | |||||
frappe.conn.sql("delete from `tabReport` where ref_doctype=%s", self.doc.name) | |||||
def before_rename(self, old, new, merge=False): | |||||
if merge: | |||||
frappe.throw(_("DocType can not be merged")) | |||||
def after_rename(self, old, new, merge=False): | |||||
if self.doc.issingle: | |||||
frappe.conn.sql("""update tabSingles set doctype=%s where doctype=%s""", (new, old)) | |||||
else: | |||||
frappe.conn.sql("rename table `tab%s` to `tab%s`" % (old, new)) | |||||
def export_doc(self): | |||||
from frappe.modules.export_file import export_to_files | |||||
export_to_files(record_list=[['DocType', self.doc.name]]) | |||||
def import_doc(self): | |||||
from frappe.modules.import_module import import_from_files | |||||
import_from_files(record_list=[[self.doc.module, 'doctype', self.doc.name]]) | |||||
def make_controller_template(self): | |||||
from frappe.modules import get_doc_path, get_module_path, scrub | |||||
pypath = os.path.join(get_doc_path(self.doc.module, | |||||
self.doc.doctype, self.doc.name), scrub(self.doc.name) + '.py') | |||||
if not os.path.exists(pypath): | |||||
with open(pypath, 'w') as pyfile: | |||||
with open(os.path.join(get_module_path("core"), "doctype", "doctype", | |||||
"doctype_template.py"), 'r') as srcfile: | |||||
pyfile.write(srcfile.read()) | |||||
def make_amendable(self): | |||||
""" | |||||
if is_submittable is set, add amended_from docfields | |||||
""" | |||||
if self.doc.is_submittable: | |||||
if not frappe.conn.sql("""select name from tabDocField | |||||
where fieldname = 'amended_from' and parent = %s""", self.doc.name): | |||||
new = self.doc.addchild('fields', 'DocField', self.doclist) | |||||
new.label = 'Amended From' | |||||
new.fieldtype = 'Link' | |||||
new.fieldname = 'amended_from' | |||||
new.options = self.doc.name | |||||
new.permlevel = 0 | |||||
new.read_only = 1 | |||||
new.print_hide = 1 | |||||
new.no_copy = 1 | |||||
new.idx = self.get_max_idx() + 1 | |||||
def get_max_idx(self): | |||||
max_idx = frappe.conn.sql("""select max(idx) from `tabDocField` where parent = %s""", | |||||
self.doc.name) | |||||
return max_idx and max_idx[0][0] or 0 | |||||
def validate_fields_for_doctype(doctype): | |||||
from frappe.model.doctype import get | |||||
validate_fields(get(doctype, cached=False).get({"parent":doctype, | |||||
"doctype":"DocField"})) | |||||
def validate_fields(fields): | |||||
def check_illegal_characters(fieldname): | |||||
for c in ['.', ',', ' ', '-', '&', '%', '=', '"', "'", '*', '$', | |||||
'(', ')', '[', ']', '/']: | |||||
if c in fieldname: | |||||
frappe.msgprint("'%s' not allowed in fieldname (%s)" % (c, fieldname)) | |||||
def check_unique_fieldname(fieldname): | |||||
duplicates = filter(None, map(lambda df: df.fieldname==fieldname and str(df.idx) or None, fields)) | |||||
if len(duplicates) > 1: | |||||
frappe.msgprint('Fieldname <b>%s</b> appears more than once in rows (%s). Please rectify' \ | |||||
% (fieldname, ', '.join(duplicates)), raise_exception=1) | |||||
def check_illegal_mandatory(d): | |||||
if d.fieldtype in ('HTML', 'Button', 'Section Break', 'Column Break') and d.reqd: | |||||
frappe.msgprint('%(label)s [%(fieldtype)s] cannot be mandatory' % d.fields, | |||||
raise_exception=1) | |||||
def check_link_table_options(d): | |||||
if d.fieldtype in ("Link", "Table"): | |||||
if not d.options: | |||||
frappe.msgprint("""#%(idx)s %(label)s: Options must be specified for Link and Table type fields""" % d.fields, | |||||
raise_exception=1) | |||||
if d.options=="[Select]": | |||||
return | |||||
if d.options != d.parent and not frappe.conn.exists("DocType", d.options): | |||||
frappe.msgprint("""#%(idx)s %(label)s: Options %(options)s must be a valid "DocType" for Link and Table type fields""" % d.fields, | |||||
raise_exception=1) | |||||
def check_hidden_and_mandatory(d): | |||||
if d.hidden and d.reqd and not d.default: | |||||
frappe.msgprint("""#%(idx)s %(label)s: Cannot be hidden and mandatory (reqd) without default""" % d.fields, | |||||
raise_exception=True) | |||||
def check_max_items_in_list(fields): | |||||
count = 0 | |||||
for d in fields: | |||||
if d.in_list_view: count+=1 | |||||
if count > 5: | |||||
frappe.msgprint("""Max 5 Fields can be set as 'In List View', please unselect a field before selecting a new one.""") | |||||
def check_width(d): | |||||
if d.fieldtype == "Currency" and cint(d.width) < 100: | |||||
frappe.msgprint("Minimum width for FieldType 'Currency' is 100px", raise_exception=1) | |||||
def check_in_list_view(d): | |||||
if d.in_list_view and d.fieldtype!="Image" and (d.fieldtype in no_value_fields): | |||||
frappe.msgprint("'In List View' not allowed for field of type '%s'" % d.fieldtype, raise_exception=1) | |||||
for d in fields: | |||||
if not d.permlevel: d.permlevel = 0 | |||||
if not d.fieldname: | |||||
frappe.msgprint("Fieldname is mandatory in row %s" % d.idx, raise_exception=1) | |||||
check_illegal_characters(d.fieldname) | |||||
check_unique_fieldname(d.fieldname) | |||||
check_illegal_mandatory(d) | |||||
check_link_table_options(d) | |||||
check_hidden_and_mandatory(d) | |||||
check_in_list_view(d) | |||||
def validate_permissions_for_doctype(doctype, for_remove=False): | |||||
from frappe.model.doctype import get | |||||
validate_permissions(get(doctype, cached=False).get({"parent":doctype, | |||||
"doctype":"DocPerm"}), for_remove) | |||||
def validate_permissions(permissions, for_remove=False): | |||||
doctype = permissions and permissions[0].parent | |||||
issingle = issubmittable = isimportable = False | |||||
if doctype and not doctype.startswith("New DocType"): | |||||
values = frappe.conn.get_value("DocType", doctype, | |||||
["issingle", "is_submittable", "allow_import"], as_dict=True) | |||||
issingle = cint(values.issingle) | |||||
issubmittable = cint(values.is_submittable) | |||||
isimportable = cint(values.allow_import) | |||||
def get_txt(d): | |||||
return "For %s (level %s) in %s, row #%s:" % (d.role, d.permlevel, d.parent, d.idx) | |||||
def check_atleast_one_set(d): | |||||
if not d.read and not d.write and not d.submit and not d.cancel and not d.create: | |||||
frappe.msgprint(get_txt(d) + " Atleast one of Read, Write, Create, Submit, Cancel must be set.", | |||||
raise_exception=True) | |||||
def check_double(d): | |||||
similar = permissions.get({ | |||||
"role":d.role, | |||||
"permlevel":d.permlevel, | |||||
"match": d.match | |||||
}) | |||||
if len(similar) > 1: | |||||
frappe.msgprint(get_txt(d) + " Only one rule allowed for a particular Role and Level.", | |||||
raise_exception=True) | |||||
def check_level_zero_is_set(d): | |||||
if cint(d.permlevel) > 0 and d.role != 'All': | |||||
if not permissions.get({"role": d.role, "permlevel": 0}): | |||||
frappe.msgprint(get_txt(d) + " Higher level permissions are meaningless if level 0 permission is not set.", | |||||
raise_exception=True) | |||||
if d.create or d.submit or d.cancel or d.amend or d.match: | |||||
frappe.msgprint("Create, Submit, Cancel, Amend, Match has no meaning at level " + d.permlevel, | |||||
raise_exception=True) | |||||
def check_permission_dependency(d): | |||||
if d.write and not d.read: | |||||
frappe.msgprint(get_txt(d) + " Cannot set Write permission if Read is not set.", | |||||
raise_exception=True) | |||||
if d.cancel and not d.submit: | |||||
frappe.msgprint(get_txt(d) + " Cannot set Cancel permission if Submit is not set.", | |||||
raise_exception=True) | |||||
if (d.submit or d.cancel or d.amend) and not d.write: | |||||
frappe.msgprint(get_txt(d) + " Cannot set Submit, Cancel, Amend permission if Write is not set.", | |||||
raise_exception=True) | |||||
if d.amend and not d.write: | |||||
frappe.msgprint(get_txt(d) + " Cannot set Amend if Cancel is not set.", | |||||
raise_exception=True) | |||||
if (d.fields.get("import") or d.export) and not d.report: | |||||
frappe.msgprint(get_txt(d) + " Cannot set Import or Export permission if Report is not set.", | |||||
raise_exception=True) | |||||
if d.fields.get("import") and not d.create: | |||||
frappe.msgprint(get_txt(d) + " Cannot set Import if Create is not set.", | |||||
raise_exception=True) | |||||
def remove_rights_for_single(d): | |||||
if not issingle: | |||||
return | |||||
if d.report: | |||||
frappe.msgprint("{doctype} {meaningless}".format(doctype=doctype, | |||||
meaningless=_("is a single DocType, permission of type Report is meaningless."))) | |||||
d.report = 0 | |||||
d.fields["import"] = 0 | |||||
d.fields["export"] = 0 | |||||
if d.restrict: | |||||
frappe.msgprint("{doctype} {meaningless}".format(doctype=doctype, | |||||
meaningless=_("is a single DocType, permission of type Restrict is meaningless."))) | |||||
d.restrict = 0 | |||||
def check_if_submittable(d): | |||||
if d.submit and not issubmittable: | |||||
frappe.msgprint(doctype + " is not Submittable, cannot assign submit rights.", | |||||
raise_exception=True) | |||||
elif d.amend and not issubmittable: | |||||
frappe.msgprint(doctype + " is not Submittable, cannot assign amend rights.", | |||||
raise_exception=True) | |||||
def check_if_importable(d): | |||||
if d.fields.get("import") and not isimportable: | |||||
frappe.throw("{doctype}: {not_importable}".format(doctype=doctype, | |||||
not_importable=_("is not allowed to be imported, cannot assign import rights."))) | |||||
for d in permissions: | |||||
if not d.permlevel: | |||||
d.permlevel=0 | |||||
check_atleast_one_set(d) | |||||
if not for_remove: | |||||
check_double(d) | |||||
check_permission_dependency(d) | |||||
check_if_submittable(d) | |||||
check_if_importable(d) | |||||
check_level_zero_is_set(d) | |||||
remove_rights_for_single(d) | |||||
def make_module_and_roles(doclist, perm_doctype="DocPerm"): | |||||
try: | |||||
if not frappe.conn.exists("Module Def", doclist[0].module): | |||||
m = frappe.bean({"doctype": "Module Def", "module_name": doclist[0].module}) | |||||
m.insert() | |||||
default_roles = ["Administrator", "Guest", "All"] | |||||
roles = [p.role for p in doclist.get({"doctype": perm_doctype})] + default_roles | |||||
for role in list(set(roles)): | |||||
if not frappe.conn.exists("Role", role): | |||||
r = frappe.bean({"doctype": "Role", "role_name": role}) | |||||
r.doc.role_name = role | |||||
r.insert() | |||||
except frappe.DoesNotExistError, e: | |||||
pass | |||||
except frappe.SQLError, e: | |||||
if e.args[0]==1146: | |||||
pass | |||||
else: | |||||
raise |
@@ -0,0 +1,11 @@ | |||||
# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors | |||||
# MIT License. See license.txt | |||||
# For license information, please see license.txt | |||||
from __future__ import unicode_literals | |||||
import frappe | |||||
class DocType: | |||||
def __init__(self, d, dl): | |||||
self.doc, self.doclist = d, dl |
@@ -0,0 +1,185 @@ | |||||
# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors | |||||
# MIT License. See license.txt | |||||
from __future__ import unicode_literals | |||||
import frappe | |||||
from frappe.utils import getdate, cint, add_months, date_diff, add_days, nowdate | |||||
weekdays = ["monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"] | |||||
class DocType: | |||||
def __init__(self, d, dl): | |||||
self.doc, self.doclist = d, dl | |||||
def validate(self): | |||||
if self.doc.starts_on and self.doc.ends_on and self.doc.starts_on > self.doc.ends_on: | |||||
frappe.msgprint(frappe._("Event End must be after Start"), raise_exception=True) | |||||
def get_permission_query_conditions(): | |||||
return """(tabEvent.event_type='Public' or tabEvent.owner='%(user)s' | |||||
or exists(select * from `tabEvent User` where | |||||
`tabEvent User`.parent=tabEvent.name and `tabEvent User`.person='%(user)s') | |||||
or exists(select * from `tabEvent Role` where | |||||
`tabEvent Role`.parent=tabEvent.name | |||||
and `tabEvent Role`.role in ('%(roles)s'))) | |||||
""" % { | |||||
"user": frappe.session.user, | |||||
"roles": "', '".join(frappe.get_roles(frappe.session.user)) | |||||
} | |||||
def has_permission(doc): | |||||
if doc.event_type=="Public" or doc.owner==frappe.session.user: | |||||
return True | |||||
# need full doclist to check roles and users | |||||
bean = frappe.bean("Event", doc.name) | |||||
if len(bean.doclist)==1: | |||||
return False | |||||
if bean.doclist.get({"doctype":"Event User", "person":frappe.session.user}): | |||||
return True | |||||
if bean.doclist.get({"doctype":"Event Role", "role":("in", frappe.get_roles())}): | |||||
return True | |||||
return False | |||||
def send_event_digest(): | |||||
today = nowdate() | |||||
for user in frappe.conn.sql("""select name, email, language | |||||
from tabProfile where ifnull(enabled,0)=1 | |||||
and user_type='System User' and name not in ('Guest', 'Administrator')""", as_dict=1): | |||||
events = get_events(today, today, user.name, for_reminder=True) | |||||
if events: | |||||
text = "" | |||||
frappe.set_user_lang(user.name, user.language) | |||||
text = "<h3>" + frappe._("Events In Today's Calendar") + "</h3>" | |||||
for e in events: | |||||
if e.all_day: | |||||
e.starts_on = "All Day" | |||||
text += "<h4>%(starts_on)s: %(subject)s</h4><p>%(description)s</p>" % e | |||||
text += '<p style="color: #888; font-size: 80%; margin-top: 20px; padding-top: 10px; border-top: 1px solid #eee;">'\ | |||||
+ frappe._("Daily Event Digest is sent for Calendar Events where reminders are set.")+'</p>' | |||||
from frappe.utils.email_lib import sendmail | |||||
sendmail(recipients=user.email, subject=frappe._("Upcoming Events for Today"), | |||||
msg = text) | |||||
@frappe.whitelist() | |||||
def get_events(start, end, user=None, for_reminder=False): | |||||
if not user: | |||||
user = frappe.session.user | |||||
roles = frappe.get_roles(user) | |||||
events = frappe.conn.sql("""select name, subject, description, | |||||
starts_on, ends_on, owner, all_day, event_type, repeat_this_event, repeat_on, | |||||
monday, tuesday, wednesday, thursday, friday, saturday, sunday | |||||
from tabEvent where (( | |||||
(date(starts_on) between date('%(start)s') and date('%(end)s')) | |||||
or (date(ends_on) between date('%(start)s') and date('%(end)s')) | |||||
or (date(starts_on) <= date('%(start)s') and date(ends_on) >= date('%(end)s')) | |||||
) or ( | |||||
date(starts_on) <= date('%(start)s') and ifnull(repeat_this_event,0)=1 and | |||||
ifnull(repeat_till, "3000-01-01") > date('%(start)s') | |||||
)) | |||||
%(reminder_condition)s | |||||
and (event_type='Public' or owner='%(user)s' | |||||
or exists(select * from `tabEvent User` where | |||||
`tabEvent User`.parent=tabEvent.name and person='%(user)s') | |||||
or exists(select * from `tabEvent Role` where | |||||
`tabEvent Role`.parent=tabEvent.name | |||||
and `tabEvent Role`.role in ('%(roles)s'))) | |||||
order by starts_on""" % { | |||||
"start": start, | |||||
"end": end, | |||||
"reminder_condition": "and ifnull(send_reminder,0)=1" if for_reminder else "", | |||||
"user": user, | |||||
"roles": "', '".join(roles) | |||||
}, as_dict=1) | |||||
# process recurring events | |||||
start = start.split(" ")[0] | |||||
end = end.split(" ")[0] | |||||
add_events = [] | |||||
remove_events = [] | |||||
def add_event(e, date): | |||||
new_event = e.copy() | |||||
new_event.starts_on = date + " " + e.starts_on.split(" ")[1] | |||||
if e.ends_on: | |||||
new_event.ends_on = date + " " + e.ends_on.split(" ")[1] | |||||
add_events.append(new_event) | |||||
for e in events: | |||||
if e.repeat_this_event: | |||||
event_start, time_str = e.starts_on.split(" ") | |||||
if e.repeat_on=="Every Year": | |||||
start_year = cint(start.split("-")[0]) | |||||
end_year = cint(end.split("-")[0]) | |||||
event_start = "-".join(event_start.split("-")[1:]) | |||||
# repeat for all years in period | |||||
for year in range(start_year, end_year+1): | |||||
date = str(year) + "-" + event_start | |||||
if date >= start and date <= end: | |||||
add_event(e, date) | |||||
remove_events.append(e) | |||||
if e.repeat_on=="Every Month": | |||||
date = start.split("-")[0] + "-" + start.split("-")[1] + "-" + event_start.split("-")[2] | |||||
# last day of month issue, start from prev month! | |||||
try: | |||||
getdate(date) | |||||
except ValueError: | |||||
date = date.split("-") | |||||
date = date[0] + "-" + str(cint(date[1]) - 1) + "-" + date[2] | |||||
start_from = date | |||||
for i in xrange(int(date_diff(end, start) / 30) + 3): | |||||
if date >= start and date <= end and date >= event_start: | |||||
add_event(e, date) | |||||
date = add_months(start_from, i+1) | |||||
remove_events.append(e) | |||||
if e.repeat_on=="Every Week": | |||||
weekday = getdate(event_start).weekday() | |||||
# monday is 0 | |||||
start_weekday = getdate(start).weekday() | |||||
# start from nearest weeday after last monday | |||||
date = add_days(start, weekday - start_weekday) | |||||
for cnt in xrange(int(date_diff(end, start) / 7) + 3): | |||||
if date >= start and date <= end and date >= event_start: | |||||
add_event(e, date) | |||||
date = add_days(date, 7) | |||||
remove_events.append(e) | |||||
if e.repeat_on=="Every Day": | |||||
for cnt in xrange(date_diff(end, start) + 1): | |||||
date = add_days(start, cnt) | |||||
if date >= event_start and date <= end \ | |||||
and e[weekdays[getdate(date).weekday()]]: | |||||
add_event(e, date) | |||||
remove_events.append(e) | |||||
for e in remove_events: | |||||
events.remove(e) | |||||
events = events + add_events | |||||
for e in events: | |||||
# remove weekday properties (to reduce message size) | |||||
for w in weekdays: | |||||
del e[w] | |||||
return events |
@@ -0,0 +1,15 @@ | |||||
frappe.views.calendar["Event"] = { | |||||
field_map: { | |||||
"start": "starts_on", | |||||
"end": "ends_on", | |||||
"id": "name", | |||||
"allDay": "all_day", | |||||
"title": "subject", | |||||
"status": "event_type", | |||||
}, | |||||
style_map: { | |||||
"Public": "success", | |||||
"Private": "info" | |||||
}, | |||||
get_events_method: "frappe.core.doctype.event.event.get_events" | |||||
} |
@@ -0,0 +1,67 @@ | |||||
# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors | |||||
# MIT License. See license.txt | |||||
"""Use blog post test to test permission restriction logic""" | |||||
test_records = [ | |||||
[{ | |||||
"doctype": "Event", | |||||
"subject":"_Test Event 1", | |||||
"starts_on": "2014-01-01", | |||||
"event_type": "Public", | |||||
}], | |||||
[{ | |||||
"doctype": "Event", | |||||
"starts_on": "2014-01-01", | |||||
"subject":"_Test Event 2", | |||||
"event_type": "Private", | |||||
}], | |||||
[{ | |||||
"doctype": "Event", | |||||
"starts_on": "2014-01-01", | |||||
"subject":"_Test Event 3", | |||||
"event_type": "Private", | |||||
}, { | |||||
"doctype": "Event User", | |||||
"parentfield": "event_individuals", | |||||
"person": "test1@example.com" | |||||
}], | |||||
] | |||||
import frappe | |||||
import frappe.defaults | |||||
import unittest | |||||
class TestEvent(unittest.TestCase): | |||||
# def setUp(self): | |||||
# profile = frappe.bean("Profile", "test1@example.com") | |||||
# profile.get_controller().add_roles("Website Manager") | |||||
def tearDown(self): | |||||
frappe.set_user("Administrator") | |||||
def test_allowed_public(self): | |||||
frappe.set_user("test1@example.com") | |||||
doc = frappe.doc("Event", frappe.conn.get_value("Event", {"subject":"_Test Event 1"})) | |||||
self.assertTrue(frappe.has_permission("Event", refdoc=doc)) | |||||
def test_not_allowed_private(self): | |||||
frappe.set_user("test1@example.com") | |||||
doc = frappe.doc("Event", frappe.conn.get_value("Event", {"subject":"_Test Event 2"})) | |||||
self.assertFalse(frappe.has_permission("Event", refdoc=doc)) | |||||
def test_allowed_private_if_in_event_user(self): | |||||
frappe.set_user("test1@example.com") | |||||
doc = frappe.doc("Event", frappe.conn.get_value("Event", {"subject":"_Test Event 3"})) | |||||
self.assertTrue(frappe.has_permission("Event", refdoc=doc)) | |||||
def test_event_list(self): | |||||
frappe.set_user("test1@example.com") | |||||
res = frappe.get_list("Event", filters=[["Event", "subject", "like", "_Test Event%"]], fields=["name", "subject"]) | |||||
self.assertEquals(len(res), 2) | |||||
subjects = [r.subject for r in res] | |||||
self.assertTrue("_Test Event 1" in subjects) | |||||
self.assertTrue("_Test Event 3" in subjects) | |||||
self.assertFalse("_Test Event 2" in subjects) | |||||
@@ -0,0 +1,9 @@ | |||||
# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors | |||||
# MIT License. See license.txt | |||||
from __future__ import unicode_literals | |||||
import frappe | |||||
class DocType: | |||||
def __init__(self, d, dl): | |||||
self.doc, self.doclist = d, dl |
@@ -0,0 +1,9 @@ | |||||
# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors | |||||
# MIT License. See license.txt | |||||
from __future__ import unicode_literals | |||||
import frappe | |||||
class DocType: | |||||
def __init__(self, d, dl): | |||||
self.doc, self.doclist = d, dl |
@@ -0,0 +1,57 @@ | |||||
# Copyright (c) 2013, Web Notes 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 | |||||
class DocType(): | |||||
def __init__(self, d, dl): | |||||
self.doc, self.doclist = d, dl | |||||
def before_insert(self): | |||||
frappe.local.rollback_observers.append(self) | |||||
def on_update(self): | |||||
# check duplicate assignement | |||||
n_records = frappe.conn.sql("""select name from `tabFile Data` | |||||
where file_name=%s | |||||
and name!=%s | |||||
and attached_to_doctype=%s | |||||
and attached_to_name=%s""", (self.doc.file_name, self.doc.name, self.doc.attached_to_doctype, | |||||
self.doc.attached_to_name)) | |||||
if len(n_records) > 0: | |||||
self.doc.duplicate_entry = n_records[0][0] | |||||
frappe.msgprint(frappe._("Same file has already been attached to the record")) | |||||
frappe.conn.rollback() | |||||
raise frappe.DuplicateEntryError | |||||
def on_trash(self): | |||||
if self.doc.attached_to_name: | |||||
# check persmission | |||||
try: | |||||
if not frappe.has_permission(self.doc.attached_to_doctype, | |||||
"write", self.doc.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.doc.file_name and not frappe.conn.count("File Data", | |||||
{"file_name": self.doc.file_name, "name": ["!=", self.doc.name]}): | |||||
if self.doc.file_name.startswith("files/"): | |||||
path = frappe.utils.get_site_path("public", self.doc.file_name) | |||||
else: | |||||
path = frappe.utils.get_site_path("public", "files", self.doc.file_name) | |||||
if os.path.exists(path): | |||||
os.remove(path) | |||||
def on_rollback(self): | |||||
self.on_trash() |
@@ -0,0 +1,11 @@ | |||||
// Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors | |||||
// MIT License. See license.txt | |||||
cur_frm.cscript.refresh = function(doc) { | |||||
cur_frm.set_intro(""); | |||||
if(doc.__islocal) { | |||||
cur_frm.set_intro(frappe._("Step 1: Set the name and save.")); | |||||
} else if(!cur_frm.doc.content) { | |||||
cur_frm.set_intro(frappe._("Step 2: Set Letterhead content.")); | |||||
} | |||||
} |
@@ -0,0 +1,30 @@ | |||||
# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors | |||||
# MIT License. See license.txt | |||||
from __future__ import unicode_literals | |||||
import frappe | |||||
class DocType: | |||||
def __init__(self, doc, doclist=[]): | |||||
self.doc = doc | |||||
self.doclist = doclist | |||||
def validate(self): | |||||
self.set_as_default() | |||||
# clear the cache so that the new letter head is uploaded | |||||
frappe.clear_cache() | |||||
def set_as_default(self): | |||||
from frappe.utils import set_default | |||||
if not self.doc.is_default: | |||||
if not frappe.conn.sql("""select count(*) from `tabLetter Head` where ifnull(is_default,0)=1"""): | |||||
self.doc.is_default = 1 | |||||
if self.doc.is_default: | |||||
frappe.conn.sql("update `tabLetter Head` set is_default=0 where name != %s", | |||||
self.doc.name) | |||||
set_default('letter_head', self.doc.name) | |||||
# update control panel - so it loads new letter directly | |||||
frappe.conn.set_value('Control Panel', None, 'letter_head', self.doc.content) |