@@ -4,23 +4,23 @@ include *.json | |||
include *.md | |||
include *.py | |||
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 |
@@ -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) |