@@ -14,7 +14,7 @@ import os, sys, importlib, inspect, json | |||
from .exceptions import * | |||
from .utils.jinja import get_jenv, get_template, render_template, get_email_from_template | |||
__version__ = '8.4.1' | |||
__version__ = '8.5.7' | |||
__title__ = "Frappe Framework" | |||
local = Local() | |||
@@ -380,7 +380,7 @@ def sendmail(recipients=[], sender="", subject="No Subject", message="No Message | |||
attachments=None, content=None, doctype=None, name=None, reply_to=None, | |||
cc=[], message_id=None, in_reply_to=None, send_after=None, expose_recipients=None, | |||
send_priority=1, communication=None, retry=1, now=None, read_receipt=None, is_notification=False, | |||
inline_images=None, template=None, args=None, header=False): | |||
inline_images=None, template=None, args=None, header=None): | |||
"""Send email using user's default **Email Account** or global default **Email Account**. | |||
@@ -492,6 +492,7 @@ def clear_cache(user=None, doctype=None): | |||
frappe.sessions.clear_cache() | |||
translate.clear_cache() | |||
reset_metadata_version() | |||
clear_domainification_cache() | |||
local.cache = {} | |||
local.new_doc_templates = {} | |||
@@ -1371,6 +1372,22 @@ def get_active_domains(): | |||
return active_domains | |||
def get_active_modules(): | |||
""" get the active modules from Module Def""" | |||
active_modules = cache().hget("modules", "active_modules") or None | |||
if active_modules is None: | |||
domains = get_active_domains() | |||
modules = get_all("Module Def", filters={"restrict_to_domain": ("in", domains)}) | |||
active_modules = [module.name for module in modules] | |||
cache().hset("modules", "active_modules", active_modules) | |||
return active_modules | |||
def clear_domainification_cache(): | |||
_cache = cache() | |||
_cache.delete_key("domains", "active_domains") | |||
_cache.delete_key("modules", "active_modules") | |||
def get_system_settings(key): | |||
if not local.system_settings.has_key(key): | |||
local.system_settings.update({key: db.get_single_value('System Settings', key)}) | |||
@@ -165,6 +165,8 @@ def get_task_log_file_path(task_id, stream_type): | |||
@frappe.whitelist(allow_guest=True) | |||
def can_subscribe_doc(doctype, docname, sid): | |||
if os.environ.get('CI'): | |||
return True | |||
from frappe.sessions import Session | |||
from frappe.exceptions import PermissionError | |||
session = Session(None, resume=True).get_session_data() | |||
@@ -494,7 +494,7 @@ def get_qr_svg_code(totp_uri): | |||
from base64 import b64encode | |||
url = qrcreate(totp_uri) | |||
stream = StringIO() | |||
url.svg(stream, scale=5) | |||
url.svg(stream, scale=3) | |||
svg = stream.getvalue().replace('\n','') | |||
svg = b64encode(bytes(svg)) | |||
return svg |
@@ -32,10 +32,15 @@ frappe.ui.form.on("Address", { | |||
} | |||
}, | |||
after_save: function() { | |||
var last_route = frappe.route_history.slice(-2, -1)[0]; | |||
if(frappe.dynamic_link && frappe.dynamic_link.doc | |||
&& frappe.dynamic_link.doc.name == last_route[2]){ | |||
frappe.set_route(last_route[0], last_route[1], last_route[2]); | |||
} | |||
frappe.run_serially([ | |||
() => frappe.timeout(1), | |||
() => { | |||
var last_route = frappe.route_history.slice(-2, -1)[0]; | |||
if(frappe.dynamic_link && frappe.dynamic_link.doc | |||
&& frappe.dynamic_link.doc.name == last_route[2]){ | |||
frappe.set_route(last_route[0], last_route[1], last_route[2]); | |||
} | |||
} | |||
]); | |||
} | |||
}); |
@@ -135,15 +135,34 @@ def get_list_context(context=None): | |||
} | |||
def get_address_list(doctype, txt, filters, limit_start, limit_page_length = 20, order_by = None): | |||
from frappe.www.list import get_list | |||
user = frappe.session.user | |||
ignore_permissions = False | |||
if is_website_user(): | |||
if not filters: filters = [] | |||
filters.append(("Address", "owner", "=", user)) | |||
ignore_permissions = True | |||
return get_list(doctype, txt, filters, limit_start, limit_page_length, ignore_permissions=ignore_permissions) | |||
from frappe.www.list import get_list | |||
user = frappe.session.user | |||
ignore_permissions = False | |||
if is_website_user(): | |||
if not filters: filters = [] | |||
add_name = [] | |||
contact = frappe.db.sql(""" | |||
select | |||
address.name | |||
from | |||
`tabDynamic Link` as link | |||
join | |||
`tabAddress` as address on link.parent = address.name | |||
where | |||
link.parenttype = 'Address' and | |||
link_name in( | |||
select | |||
link.link_name from `tabContact` as contact | |||
join | |||
`tabDynamic Link` as link on contact.name = link.parent | |||
where | |||
contact.user = %s)""",(user)) | |||
for c in contact: | |||
add_name.append(c[0]) | |||
filters.append(("Address", "name", "in", add_name)) | |||
ignore_permissions = True | |||
return get_list(doctype, txt, filters, limit_start, limit_page_length, ignore_permissions=ignore_permissions) | |||
def has_website_permission(doc, ptype, user, verbose=False): | |||
"""Returns true if there is a related lead or contact related to this document""" | |||
@@ -185,12 +204,12 @@ def get_shipping_address(company): | |||
address_as_dict = address[0] | |||
name, address_template = get_address_templates(address_as_dict) | |||
return address_as_dict.get("name"), frappe.render_template(address_template, address_as_dict) | |||
def get_company_address(company): | |||
ret = frappe._dict() | |||
ret.company_address = get_default_address('Company', company) | |||
ret.company_address_display = get_address_display(ret.company_address) | |||
return ret | |||
def address_query(doctype, txt, searchfield, start, page_len, filters): | |||
@@ -1,5 +1,5 @@ | |||
/* eslint-disable */ | |||
frappe.listview_settings['{doctype}'] = {{ | |||
add_fields: ["status"], | |||
filters:[["status","=", "Open"]] | |||
// add_fields: ["status"], | |||
// filters:[["status","=", "Open"]] | |||
}}; |
@@ -0,0 +1,23 @@ | |||
/* eslint-disable */ | |||
// rename this file from _test_[name] to test_[name] to activate | |||
// and remove above this line | |||
QUnit.test("test: {doctype}", function (assert) {{ | |||
let done = assert.async(); | |||
// number of asserts | |||
assert.expect(1); | |||
frappe.run_serially('{doctype}', [ | |||
// insert a new {doctype} | |||
() => frappe.tests.make([ | |||
// values to be set | |||
{{key: 'value'}} | |||
]), | |||
() => {{ | |||
assert.equal(cur_frm.doc.key, 'value'); | |||
}}, | |||
() => done() | |||
]); | |||
}}); |
@@ -337,6 +337,10 @@ class DocType(Document): | |||
if not self.istable: | |||
make_boilerplate("controller.js", self.as_dict()) | |||
#make_boilerplate("controller_list.js", self.as_dict()) | |||
if not os.path.exists(frappe.get_module_path(frappe.scrub(self.module), | |||
'doctype', frappe.scrub(self.name), 'tests')): | |||
make_boilerplate("test_controller.js", self.as_dict()) | |||
if self.has_web_view: | |||
templates_path = frappe.get_module_path(frappe.scrub(self.module), 'doctype', frappe.scrub(self.name), 'templates') | |||
@@ -8,5 +8,4 @@ from frappe.model.document import Document | |||
class DomainSettings(Document): | |||
def on_update(self): | |||
cache = frappe.cache() | |||
cache.delete_key("domains", "active_domains") | |||
frappe.clear_domainification_cache() |
@@ -71,6 +71,37 @@ | |||
"search_index": 0, | |||
"set_only_once": 0, | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
"columns": 0, | |||
"fieldname": "restrict_to_domain", | |||
"fieldtype": "Link", | |||
"hidden": 0, | |||
"ignore_user_permissions": 0, | |||
"ignore_xss_filter": 0, | |||
"in_filter": 0, | |||
"in_global_search": 0, | |||
"in_list_view": 0, | |||
"in_standard_filter": 0, | |||
"label": "Restrict To Domain", | |||
"length": 0, | |||
"no_copy": 0, | |||
"options": "Domain", | |||
"permlevel": 0, | |||
"precision": "", | |||
"print_hide": 0, | |||
"print_hide_if_no_value": 0, | |||
"read_only": 0, | |||
"remember_last_selected_value": 0, | |||
"report_hide": 0, | |||
"reqd": 0, | |||
"search_index": 0, | |||
"set_only_once": 0, | |||
"unique": 0 | |||
} | |||
], | |||
"has_web_view": 0, | |||
@@ -84,7 +115,7 @@ | |||
"issingle": 0, | |||
"istable": 0, | |||
"max_attachments": 0, | |||
"modified": "2017-06-20 14:35:17.407968", | |||
"modified": "2017-07-13 03:05:28.213656", | |||
"modified_by": "Administrator", | |||
"module": "Core", | |||
"name": "Module Def", | |||
@@ -0,0 +1,23 @@ | |||
/* eslint-disable */ | |||
// rename this file from _test_[name] to test_[name] to activate | |||
// and remove above this line | |||
QUnit.test("test: Test Runner", function (assert) { | |||
let done = assert.async(); | |||
// number of asserts | |||
assert.expect(1); | |||
frappe.run_serially('Test Runner', [ | |||
// insert a new Test Runner | |||
() => frappe.tests.make([ | |||
// values to be set | |||
{key: 'value'} | |||
]), | |||
() => { | |||
assert.equal(cur_frm.doc.key, 'value'); | |||
}, | |||
() => done() | |||
]); | |||
}); |
@@ -32,32 +32,33 @@ frappe.ui.form.on('Test Runner', { | |||
frappe.dom.eval(f.script); | |||
}); | |||
// if(frm.doc.module_name) { | |||
// QUnit.module.only(frm.doc.module_name); | |||
// } | |||
QUnit.testDone(function(details) { | |||
var result = { | |||
"Module name": details.module, | |||
"Test name": details.name, | |||
"Assertions": { | |||
"Total": details.total, | |||
"Passed": details.passed, | |||
"Failed": details.failed | |||
}, | |||
"Skipped": details.skipped, | |||
"Todo": details.todo, | |||
"Runtime": details.runtime | |||
}; | |||
// var result = { | |||
// "Module name": details.module, | |||
// "Test name": details.name, | |||
// "Assertions": { | |||
// "Total": details.total, | |||
// "Passed": details.passed, | |||
// "Failed": details.failed | |||
// }, | |||
// "Skipped": details.skipped, | |||
// "Todo": details.todo, | |||
// "Runtime": details.runtime | |||
// }; | |||
// eslint-disable-next-line | |||
console.log(JSON.stringify(result, null, 2)); | |||
// console.log(JSON.stringify(result, null, 2)); | |||
details.assertions.map(a => { | |||
// eslint-disable-next-line | |||
console.log(`${a.result ? '✔' : '✗'} ${a.message}`); | |||
}); | |||
}); | |||
QUnit.load(); | |||
QUnit.done(({ total, failed, passed, runtime }) => { | |||
// flag for selenium that test is done | |||
$('<div id="frappe-qunit-done"></div>').appendTo($('body')); | |||
console.log( `Total: ${total}, Failed: ${failed}, Passed: ${passed}, Runtime: ${runtime}` ); // eslint-disable-line | |||
@@ -67,6 +68,9 @@ frappe.ui.form.on('Test Runner', { | |||
console.log('Tests Passed'); // eslint-disable-line | |||
} | |||
frappe.set_route('Form', 'Test Runner', 'Test Runner'); | |||
$('<div id="frappe-qunit-done"></div>').appendTo($('body')); | |||
}); | |||
}); | |||
@@ -42,6 +42,36 @@ | |||
"set_only_once": 0, | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
"columns": 0, | |||
"fieldname": "app", | |||
"fieldtype": "Data", | |||
"hidden": 0, | |||
"ignore_user_permissions": 0, | |||
"ignore_xss_filter": 0, | |||
"in_filter": 0, | |||
"in_global_search": 0, | |||
"in_list_view": 0, | |||
"in_standard_filter": 0, | |||
"label": "App", | |||
"length": 0, | |||
"no_copy": 0, | |||
"permlevel": 0, | |||
"precision": "", | |||
"print_hide": 0, | |||
"print_hide_if_no_value": 0, | |||
"read_only": 0, | |||
"remember_last_selected_value": 0, | |||
"report_hide": 0, | |||
"reqd": 0, | |||
"search_index": 0, | |||
"set_only_once": 0, | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
@@ -83,7 +113,7 @@ | |||
"issingle": 1, | |||
"istable": 0, | |||
"max_attachments": 0, | |||
"modified": "2017-07-12 23:16:15.910891", | |||
"modified": "2017-07-19 03:22:33.221169", | |||
"modified_by": "Administrator", | |||
"module": "Core", | |||
"name": "Test Runner", | |||
@@ -13,37 +13,23 @@ class TestRunner(Document): | |||
def get_test_js(): | |||
'''Get test + data for app, example: app/tests/ui/test_name.js''' | |||
test_path = frappe.db.get_single_value('Test Runner', 'module_path') | |||
test_js = [] | |||
# split | |||
app, test_path = test_path.split(os.path.sep, 1) | |||
test_js = get_test_data(app) | |||
# full path | |||
# now full path | |||
test_path = frappe.get_app_path(app, test_path) | |||
with open(test_path, 'r') as fileobj: | |||
test_js.append(dict( | |||
script = fileobj.read() | |||
)) | |||
return test_js | |||
def get_test_data(app): | |||
'''Get the test fixtures from all js files in app/tests/ui/data''' | |||
test_js = [] | |||
def add_file(path): | |||
with open(path, 'r') as fileobj: | |||
test_js.append(dict( | |||
script = fileobj.read() | |||
)) | |||
data_path = frappe.get_app_path(app, 'tests', 'ui', 'data') | |||
if os.path.exists(data_path): | |||
for fname in os.listdir(data_path): | |||
if fname.endswith('.js'): | |||
add_file(os.path.join(data_path, fname)) | |||
if app != 'frappe': | |||
add_file(frappe.get_app_path('frappe', 'tests', 'ui', 'data', 'test_lib.js')) | |||
# add test_lib.js | |||
add_file(frappe.get_app_path('frappe', 'tests', 'ui', 'data', 'test_lib.js')) | |||
add_file(test_path) | |||
return test_js | |||
@@ -279,7 +279,7 @@ class User(Document): | |||
sender = frappe.session.user not in STANDARD_USERS and get_formatted_email(frappe.session.user) or None | |||
frappe.sendmail(recipients=self.email, sender=sender, subject=subject, | |||
template=template, args=args, | |||
template=template, args=args, header=[subject, "green"], | |||
delayed=(not now) if now!=None else self.flags.delay_emails, retry=3) | |||
def a_system_manager_should_exist(self): | |||
@@ -3,9 +3,37 @@ | |||
from __future__ import unicode_literals | |||
import frappe | |||
import unittest | |||
test_records = frappe.get_test_records('Version') | |||
import unittest, copy | |||
from frappe.test_runner import make_test_objects | |||
from frappe.core.doctype.version.version import get_diff | |||
class TestVersion(unittest.TestCase): | |||
pass | |||
def test_get_diff(self): | |||
test_records = make_test_objects('Event', reset = True) | |||
old_doc = frappe.get_doc("Event", test_records[0]) | |||
new_doc = copy.deepcopy(old_doc) | |||
old_doc.color = None | |||
diff = get_diff(old_doc, new_doc)['changed'] | |||
self.assertEquals(get_fieldnames(diff)[0], 'color') | |||
self.assertTrue(get_old_values(diff)[0] is None) | |||
self.assertEquals(get_new_values(diff)[0], 'blue') | |||
new_doc.starts_on = "2017-07-20" | |||
diff = get_diff(old_doc, new_doc)['changed'] | |||
self.assertEquals(get_fieldnames(diff)[0], 'starts_on') | |||
self.assertEquals(get_old_values(diff)[0], '01-01-2014 00:00:00') | |||
self.assertEquals(get_new_values(diff)[0], '07-20-2017 00:00:00') | |||
def get_fieldnames(change_array): | |||
return [d[0] for d in change_array] | |||
def get_old_values(change_array): | |||
return [d[1] for d in change_array] | |||
def get_new_values(change_array): | |||
return [d[2] for d in change_array] |
@@ -69,10 +69,13 @@ def get_diff(old, new, for_child=False): | |||
if not d.name in new_row_by_name: | |||
out.removed.append([df.fieldname, d.as_dict()]) | |||
elif (old_value != new_value | |||
and old.get_formatted(df.fieldname) != new.get_formatted(df.fieldname)): | |||
out.changed.append((df.fieldname, old.get_formatted(df.fieldname), | |||
new.get_formatted(df.fieldname))) | |||
elif (old_value != new_value): | |||
# Check for None values | |||
old_data = old.get_formatted(df.fieldname) if old_value else old_value | |||
new_data = new.get_formatted(df.fieldname) if new_value else new_value | |||
if old_data != new_data: | |||
out.changed.append((df.fieldname, old_data, new_data)) | |||
# docstatus | |||
if not for_child and old.docstatus != new.docstatus: | |||
@@ -53,8 +53,20 @@ class Database: | |||
def connect(self): | |||
"""Connects to a database as set in `site_config.json`.""" | |||
warnings.filterwarnings('ignore', category=MySQLdb.Warning) | |||
self._conn = MySQLdb.connect(user=self.user, host=self.host, passwd=self.password, | |||
use_unicode=True, charset='utf8mb4') | |||
usessl = 0 | |||
if frappe.conf.db_ssl_ca and frappe.conf.db_ssl_cert and frappe.conf.db_ssl_key: | |||
usessl = 1 | |||
self.ssl = { | |||
'ca':frappe.conf.db_ssl_ca, | |||
'cert':frappe.conf.db_ssl_cert, | |||
'key':frappe.conf.db_ssl_key | |||
} | |||
if usessl: | |||
self._conn = MySQLdb.connect(user=self.user, host=self.host, passwd=self.password, | |||
use_unicode=True, charset='utf8mb4', ssl=self.ssl) | |||
else: | |||
self._conn = MySQLdb.connect(user=self.user, host=self.host, passwd=self.password, | |||
use_unicode=True, charset='utf8mb4') | |||
self._conn.converter[246]=float | |||
self._conn.converter[12]=get_datetime | |||
self._conn.encoders[UnicodeWithAttrs] = self._conn.encoders[UnicodeType] | |||
@@ -19,16 +19,8 @@ def update_event(args, field_map): | |||
def get_event_conditions(doctype, filters=None): | |||
"""Returns SQL conditions with user permissions and filters for event queries""" | |||
from frappe.desk.reportview import build_match_conditions | |||
from frappe.desk.reportview import get_filters_cond | |||
if not frappe.has_permission(doctype): | |||
frappe.throw(_("Not Permitted"), frappe.PermissionError) | |||
conditions = build_match_conditions(doctype) | |||
conditions = conditions and (" and " + conditions) or "" | |||
if filters: | |||
filters = json.loads(filters) | |||
for key in filters: | |||
if filters[key]: | |||
conditions += 'and `{0}` = "{1}"'.format(frappe.db.escape(key), frappe.db.escape(filters[key])) | |||
return conditions | |||
return get_filters_cond(doctype, filters, [], with_match_conditions = True) |
@@ -3,18 +3,21 @@ | |||
"doctype": "Event", | |||
"subject":"_Test Event 1", | |||
"starts_on": "2014-01-01", | |||
"event_type": "Public" | |||
"event_type": "Public", | |||
"creation": "2014-01-01" | |||
}, | |||
{ | |||
"doctype": "Event", | |||
"starts_on": "2014-01-01", | |||
"subject":"_Test Event 2", | |||
"event_type": "Private" | |||
"starts_on": "2014-01-01", | |||
"event_type": "Private", | |||
"creation": "2014-01-01" | |||
}, | |||
{ | |||
"doctype": "Event", | |||
"starts_on": "2014-01-01", | |||
"subject": "_Test Event 3", | |||
"event_type": "Private" | |||
"starts_on": "2014-02-01", | |||
"event_type": "Private", | |||
"creation": "2014-02-01" | |||
} | |||
] |
@@ -13,10 +13,12 @@ def get_notifications(): | |||
return | |||
config = get_notification_config() | |||
groups = config.get("for_doctype").keys() + config.get("for_module").keys() | |||
cache = frappe.cache() | |||
notification_count = {} | |||
notification_percent = {} | |||
for name in groups: | |||
count = cache.hget("notification_count:" + name, frappe.session.user) | |||
@@ -27,6 +29,7 @@ def get_notifications(): | |||
"open_count_doctype": get_notifications_for_doctypes(config, notification_count), | |||
"open_count_module": get_notifications_for_modules(config, notification_count), | |||
"open_count_other": get_notifications_for_other(config, notification_count), | |||
"targets": get_notifications_for_targets(config, notification_percent), | |||
"new_messages": get_new_messages() | |||
} | |||
@@ -111,6 +114,49 @@ def get_notifications_for_doctypes(config, notification_count): | |||
return open_count_doctype | |||
def get_notifications_for_targets(config, notification_percent): | |||
"""Notifications for doc targets""" | |||
can_read = frappe.get_user().get_can_read() | |||
doc_target_percents = {} | |||
# doc_target_percents = { | |||
# "Company": { | |||
# "Acme": 87, | |||
# "RobotsRUs": 50, | |||
# }, {}... | |||
# } | |||
for doctype in config.targets: | |||
if doctype in can_read: | |||
if doctype in notification_percent: | |||
doc_target_percents[doctype] = notification_percent[doctype] | |||
else: | |||
doc_target_percents[doctype] = {} | |||
d = config.targets[doctype] | |||
condition = d["filters"] | |||
target_field = d["target_field"] | |||
value_field = d["value_field"] | |||
try: | |||
if isinstance(condition, dict): | |||
doc_list = frappe.get_list(doctype, fields=["name", target_field, value_field], | |||
filters=condition, limit_page_length = 100, ignore_ifnull=True) | |||
except frappe.PermissionError: | |||
frappe.clear_messages() | |||
pass | |||
except Exception as e: | |||
if e.args[0]!=1412: | |||
raise | |||
else: | |||
for doc in doc_list: | |||
value = doc[value_field] | |||
target = doc[target_field] | |||
doc_target_percents[doctype][doc.name] = (value/target * 100) if value < target else 100 | |||
return doc_target_percents | |||
def clear_notifications(user=None): | |||
if frappe.flags.in_install: | |||
return | |||
@@ -163,7 +209,7 @@ def get_notification_config(): | |||
config = frappe._dict() | |||
for notification_config in frappe.get_hooks().notification_config: | |||
nc = frappe.get_attr(notification_config)() | |||
for key in ("for_doctype", "for_module", "for_other"): | |||
for key in ("for_doctype", "for_module", "for_other", "targets"): | |||
config.setdefault(key, {}) | |||
config[key].update(nc.get(key, {})) | |||
return config | |||
@@ -678,21 +678,6 @@ var utils = { | |||
slide.get_field("timezone").set_input(frappe.wizard.values.timezone); | |||
} | |||
country_field.df.description = 'fetching country...'; | |||
country_field.set_description(); | |||
// get location from IP (unreliable) | |||
frappe.call({ | |||
method:"frappe.desk.page.setup_wizard.setup_wizard.load_country", | |||
callback: function(r) { | |||
if(r.message) { | |||
slide.get_field("country").set_input(r.message); | |||
slide.get_input("country").trigger('change'); | |||
} | |||
country_field.df.description = ''; | |||
country_field.set_description(); | |||
} | |||
}); | |||
}, | |||
bind_language_events: function(slide) { | |||
@@ -208,7 +208,7 @@ def add_total_row(result, columns, meta = None): | |||
total_row[i] = result[0][i] | |||
for i in has_percent: | |||
total_row[i] = total_row[i] / len(result) | |||
total_row[i] = flt(total_row[i]) / len(result) | |||
first_col_fieldtype = None | |||
if isinstance(columns[0], basestring): | |||
@@ -334,7 +334,10 @@ def build_match_conditions(doctype, as_condition=True): | |||
else: | |||
return match_conditions | |||
def get_filters_cond(doctype, filters, conditions, ignore_permissions=None): | |||
def get_filters_cond(doctype, filters, conditions, ignore_permissions=None, with_match_conditions=False): | |||
if isinstance(filters, basestring): | |||
filters = json.loads(filters) | |||
if filters: | |||
flt = filters | |||
if isinstance(filters, dict): | |||
@@ -350,10 +353,13 @@ def get_filters_cond(doctype, filters, conditions, ignore_permissions=None): | |||
query = DatabaseQuery(doctype) | |||
query.filters = flt | |||
query.conditions = conditions | |||
if with_match_conditions: | |||
query.build_match_conditions() | |||
query.build_filter_conditions(flt, conditions, ignore_permissions) | |||
cond = ' and ' + ' and '.join(query.conditions) | |||
else: | |||
cond = '' | |||
return cond | |||
@@ -22,11 +22,16 @@ To run a Test Runner based test, use the `run-ui-tests` bench command by passing | |||
This will pass the filename to `test_test_runner.py` that will load the required JS in the browser and execute the tests | |||
### Adding Fixtures / Test Data | |||
### Debugging Tests | |||
You can also add data that you require for all tests in the `tests/ui/data` folder of your app. All the files in this folder will be loaded in the browser before running the test. | |||
To debug a test, you can open it in the **Test Runner** from your UI and run it manually to see where it is exactly failing. | |||
The file `frappe/tests/ui/data/test_lib.js`, which contains library functions for testing is always loaded. | |||
### Test Sequence | |||
In Frappé UI tests are run in a fixed sequence to ensure dependencies. | |||
The sequence in which the tests will be run will be in `tests/ui/tests.txt` | |||
file. | |||
### Running All UI Tests | |||
@@ -20,13 +20,20 @@ Example: | |||
### Optional Settings | |||
- `db_host`: Database host if not `localhost`. | |||
- `admin_password`: Default Password for "Administrator". | |||
- `mute_emails`: Stops email sending if true. | |||
- `deny_multiple_logins`: Stop users from having more than one active session. | |||
- `root_password`: MariaDB root password. | |||
### Defaut Outgoing Email Settings | |||
### Remote Database Host Settings | |||
- `db_host`: Database host if not `localhost`. | |||
To connect to a remote database server using ssl, you must first configure the database host to accept SSL connections. An example of how to do this is available at https://www.digitalocean.com/community/tutorials/how-to-configure-ssl-tls-for-mysql-on-ubuntu-16-04. After you do the configuration, set the following three options. All options must be set for Frappe to attempt to connect using SSL. | |||
- `db_ssl_ca`: Full path to the ca.pem file used for connecting to a database host using ssl. Example value is `"/etc/mysql/ssl/ca.pem"`. | |||
- `db_ssl_cert`: Full path to the cert.pem file used for connecting to a database host using ssl. Example value is `"/etc/mysql/ssl/client-cert.pem"`. | |||
- `db_ssl_key`: Full path to the key.pem file used for connecting to a database host using ssl. Example value is `"/etc/mysql/ssl/client-key.pem"`. | |||
### Default Outgoing Email Settings | |||
- `mail_server`: SMTP server hostname. | |||
- `mail_port`: STMP port. | |||
@@ -0,0 +1,61 @@ | |||
# Making Graphs | |||
The Frappe UI **Graph** object enables you to render simple line and bar graphs for a discreet set of data points. You can also set special checkpoint values and summary stats. | |||
### Example: Line graph | |||
Here's is an example of a simple sales graph: | |||
render_graph: function() { | |||
$('.form-graph').empty(); | |||
var months = ['Aug', 'Sep', 'Oct', 'Nov', 'Dec', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul']; | |||
var values = [2410, 3100, 1700, 1200, 2700, 1600, 2740, 1000, 850, 1500, 400, 2013]; | |||
var goal = 2500; | |||
var current_val = 2013; | |||
new frappe.ui.Graph({ | |||
parent: $('.form-graph'), | |||
width: 700, | |||
height: 140, | |||
mode: 'line-graph', | |||
title: 'Sales', | |||
subtitle: 'Monthly', | |||
y_values: values, | |||
x_points: months, | |||
specific_values: [ | |||
{ | |||
name: "Goal", | |||
line_type: "dashed", // "dashed" or "solid" | |||
value: goal | |||
}, | |||
], | |||
summary_values: [ | |||
{ | |||
name: "This month", | |||
color: 'green', // Indicator colors: 'grey', 'blue', 'red', | |||
// 'green', 'orange', 'purple', 'darkgrey', | |||
// 'black', 'yellow', 'lightblue' | |||
value: '₹ ' + current_val | |||
}, | |||
{ | |||
name: "Goal", | |||
color: 'blue', | |||
value: '₹ ' + goal | |||
}, | |||
{ | |||
name: "Completed", | |||
color: 'green', | |||
value: (current_val/goal*100).toFixed(1) + "%" | |||
} | |||
] | |||
}); | |||
}, | |||
<img src="{{docs_base_url}}/assets/img/desk/line_graph.png" class="screenshot"> | |||
Setting the mode to 'bar-graph': | |||
<img src="{{docs_base_url}}/assets/img/desk/bar_graph.png" class="screenshot"> |
@@ -0,0 +1,28 @@ | |||
# Agregando dominios personalizados a su Site | |||
Puedes agregar **multiples dominios personalizados** para un site, ejecutando el comando: | |||
bench setup add-domain [dominio] | |||
Al ejecutar el comando debes especificar para cual site quieres establecer el dominio personalizado. | |||
También, puedes configurar el SSL para su dominio personalizado usando las opciones: | |||
--ssl-certificate [ruta-al-certificado] | |||
--ssl-certificate-key [ruta-a-la--clave-certificado] | |||
Ejemplo: | |||
bench setup add-domain custom.erpnext.com --ssl-certificate /etc/letsencrypt/live/erpnext.cert --ssl-certificate-key /etc/letsencrypt/live/erpnext.key | |||
La configuración el dominio es almacenada en las configuraciones del site en su archivo site_config.json | |||
"domains": [ | |||
{ | |||
"ssl_certificate": "/etc/letsencrypt/live/erpnext.cert", | |||
"domain": "erpnext.com", | |||
"ssl_certificate_key": "/etc/letsencrypt/live/erpnext.key" | |||
} | |||
], | |||
**Luego debes regenerar las configuraciones de nginx ejecutando el comando `bench setup nginx` y reiniciando el servicio de nginx para que los cambios de los dominios tomen efecto** |
@@ -0,0 +1,48 @@ | |||
# Configurando HTTPS | |||
### Obtener los archivos requeridos | |||
Puede obtener un certificado SSL de una entidad emisora de certificados de confianza o generar su propio certificado. | |||
Para los certificados auto-firmados, el navegador mostrará una advertencia de que el certificado no es de confianza. [Aquí hay un tutorial para usar Let's Encrypt para obtener un certificado SSL gratuito](lets-encrypt-ssl-setup.html) | |||
Los archivos obligatorios son: | |||
* Certificado (Normalmente con extensión .crt) | |||
* Clave privada descifrada | |||
Si tienes varios certificados (primario e intermedios), tendrás que unirlos. Por ejemplo, | |||
cat su_certificado.crt CA.crt >> certificate_bundle.crt | |||
También asegúrese que su clave privada no sea legible. Generalmente, solo puede ser leída por root ya que normalmente es el dueño de la misma. | |||
chown root private.key | |||
chmod 600 private.key | |||
### Mueva los dos archivos a una ruta confiable | |||
mkdir /etc/nginx/conf.d/ssl | |||
mv private.key /etc/nginx/conf.d/ssl/private.key | |||
mv certificate_bundle.crt /etc/nginx/conf.d/ssl/certificate_bundle.crt | |||
### Establecer configuraciones de nginx | |||
Configura las rutas al certificado y la clave privada de su site. | |||
bench set-ssl-certificate site1.local /etc/nginx/conf.d/ssl/certificate_bundle.crt | |||
bench set-ssl-key site1.local /etc/nginx/conf.d/ssl/private.key | |||
### Generar la configuració de nginx | |||
bench setup nginx | |||
### Reiniciar nginx | |||
sudo service nginx reload | |||
o | |||
systemctl reload nginx # for CentOS 7 | |||
Ahora que tienes configurado el SSL, todo el tráfico HTTP va a ser redireccionado a HTTPS |
@@ -0,0 +1,31 @@ | |||
<!-- markdown --> | |||
En caso que estes experimentando inconvenientes con las tareas programadas, puedes ejecutar varios comandos para diagnosticar el problema. | |||
### `bench doctor` | |||
Esto va a mostrar en la consola lo siguiente en orden: | |||
- El estado del Scheduler por site | |||
- Número de Workers | |||
- Tareas Pendientes | |||
Salida deseada: | |||
Workers online: 0 | |||
-----None Jobs----- | |||
### `bench --site [site-name] show-pending-jobs` | |||
Esto va a mostrar en la consola lo siguiente en orden: | |||
- Cola | |||
- Tareas dentro de Cola | |||
Salida deseada: | |||
-----Pending Jobs----- | |||
### `bench purge-jobs` | |||
Esto va a remover todas las tareas programadas de todas las colas. |
@@ -0,0 +1 @@ | |||
{index} |
@@ -0,0 +1,11 @@ | |||
configuring-https | |||
lets-encrypt-ssl-setup | |||
diagnosing-the-scheduler | |||
how-to-change-host-name-from-localhost | |||
manual-setup | |||
setup-multitenancy | |||
setup-production | |||
setup-ssl | |||
stop-production-and-start-development | |||
updating | |||
setting-limits |
@@ -0,0 +1,101 @@ | |||
# Uso de Let's Encrypt para configurar HTTPS | |||
##Prerrequisitos | |||
1. Necesitas tener una configuración Multitenant | |||
2. Su sitio debería ser accesible a traves de un dominio válido | |||
3. Necesitas permisos de administrados en el servidor | |||
**Nota : Los certificados de Let's Encrypt expiran cada 3 meses** | |||
## Usando el comando bench | |||
Ejecutar: | |||
sudo -H bench setup lets-encrypt [site-name] | |||
Van a aparecer varios prompts, responde a todo. Este comando también va a agregar una entrada a crontab del usuario que esta intentando renovar el certificado cada mes. | |||
### Dominios personalizados | |||
Puedes configurar Let's Encrypt para [dominios personalizados](adding-custom-domains.html). Solo usando la opción `--custom-domain` | |||
sudo -H bench setup lets-encrypt [site-name] --custom-domain [custom-domain] | |||
### Renovar Certificados | |||
Para la renovación manual de certificados puedes usar: | |||
sudo bench renew-lets-encrypt | |||
<hr> | |||
## Método Manual | |||
### Descarga el script apropiado de Certbot-auto en el directorio /opt | |||
https://certbot.eff.org/ | |||
### Detener el servicio / proceso nginx | |||
$ sudo service nginx stop | |||
### Ejecutar Certbot | |||
$ ./opt/certbot-auto certonly --standalone | |||
Despues que letsencrypt se inicializa, vas a tener que llenar algunas informaciones. Los prompts pueden variar de si haz usado o no Let's Encrypt antes, pero vamos a guiarte en su primera vez. | |||
En el prompt, ingresar la dirección de correo eléctronico que será usada para notificaciones y recuperación de claves perdidas: | |||
 | |||
Debes aceptar el acuerdo de subscripción de Let's Encrypt, selecciona Agree: | |||
 | |||
Luego ingresa el nombre de su dominio(s). Nota que si deseas un simple certificado para trabajar con | |||
varios nombres de dominios (ejemplo: example.com y www.example.com), asegurate de incluirlos todos: | |||
 | |||
### Archivos de certificados | |||
Despues de obtener el certificado, va a tener los siguientes archivos PEM-encoded: | |||
* **cert.pem**: El certificado de su dominio | |||
* **chain.pem**: La cadena del certificado de Let's Encrypt | |||
* **fullchain.pem**: cert.pem y chain.pem combinados | |||
* **privkey.pem**: La clave privada de su certificado. | |||
Estos certificados estan almacenados en el directorio `/etc/letsencrypt/live/example.com` | |||
### Configurar los certificados para su site(s) | |||
Vaya al archivo site_config.json del site donde tiene erpnext | |||
$ cd frappe-bench/sites/{{nombre_sitio}} | |||
Agrega las siguientes lineas al archivo site_config.json | |||
"ssl_certificate": "/etc/letsencrypt/live/example.com/fullchain.pem", | |||
"ssl_certificate_key": "/etc/letsencrypt/live/example.com/privkey.pem" | |||
Regenerar las configuraciones de nginx | |||
$ bench setup nginx | |||
Reiniciar el servidor nginx | |||
$ sudo service nginx restart | |||
--- | |||
### Renovació Automática (experimental) | |||
Accede como root o como un usuario con privileges de administrador, ejecuta `crontab -e` y presiona enter: | |||
# renovar el certificado de letsencrypt todos los días primero de cada mes y recibe un email si el comando ha sido ejecutado | |||
MAILTO="mail@example.com" | |||
0 0 1-7 * * [ "$(date '+\%a')" = "Mon" ] && sudo service nginx stop && /opt/certbot-auto renew && sudo service nginx start |
@@ -0,0 +1,74 @@ | |||
Instalación Manual | |||
-------------- | |||
Prerrequisitos, | |||
* [Python 2.7](https://www.python.org/download/releases/2.7/) | |||
* [MariaDB](https://mariadb.org/) | |||
* [Redis](http://redis.io/topics/quickstart) | |||
* [WKHTMLtoPDF con QT parcheado](http://wkhtmltopdf.org/downloads.html) (Requerido para la generación de pdf) | |||
[Instalando los Prerrequisitos en OSX](https://github.com/frappe/bench/wiki/Installing-Bench-Pre-requisites-on-MacOSX) | |||
Instalar el bench como usuario normal, **no root** | |||
git clone https://github.com/frappe/bench bench-repo | |||
sudo pip install -e bench-repo | |||
Nota: Favor de no remover el directorio bench que el comando va a crear | |||
Migrando desde una instalación existente | |||
------------------------------------ | |||
Si deseas migrar desde ERPNext v3, sigue las siguientes instrucciones [aquí](https://github.com/frappe/bench/wiki/Migrating-from-ERPNext-version-3) | |||
Si deseas migrar de una versión vieja del bench, sigue las instrucciones [aquí](https://github.com/frappe/bench/wiki/Migrating-from-old-bench) | |||
Uso básico | |||
=========== | |||
* Crea un nuevo bench | |||
El comando init va a crear un directorio conteniendo el framework Frappe instalado. | |||
Va a ser configurado para copias de seguridad periódicas y actualizaciones automáticas una vez por día. | |||
bench init frappe-bench && cd frappe-bench | |||
* Agregar aplicaciones | |||
El comando get-app descarga e instala aplicaciones hechas en frappe. Ejemplos: | |||
- [erpnext](https://github.com/frappe/erpnext) | |||
- [erpnext_shopify](https://github.com/frappe/erpnext_shopify) | |||
- [paypal_integration](https://github.com/frappe/paypal_integration) | |||
bench get-app erpnext https://github.com/frappe/erpnext | |||
* Agregar Site | |||
Las aplicaciones Frappe son montadas en los Sites y por tanto tendras que crear por lo menos un site. | |||
El comando new-site te permite crearlos. | |||
bench new-site site1.local | |||
* Iniciar bench | |||
Para comenzar a utilizar el bench, usa el comando `bench start` | |||
bench start | |||
Para acceder a Frappe / ERPNext, abra su navegador favorito y escriba la ruta `localhost:8000` | |||
El usuario por defecto es "Administrator" y la contraseña es la que específicaste al momento de crear el nuevo site. | |||
Configurando ERPNext | |||
================== | |||
Para instalar ERPNext, ejecuta: | |||
``` | |||
bench install-app erpnext | |||
``` | |||
Ahora puedes usar `bench start` o [configurar el bench para uso en producción](setup-production.html) |
@@ -0,0 +1,39 @@ | |||
# Estableciendo límites para su sitio | |||
La versión 7 de Frappe ha agregado soporte para la configuración de límites y restricciones para su site. | |||
Estas restricciones están en el archivo `site_config.json` dentro de la carpeta del site. | |||
{ | |||
"db_name": "xxxxxxxxxx", | |||
"db_password": "xxxxxxxxxxxx", | |||
"limits": { | |||
"emails": 1500, | |||
"space": 0.157, | |||
"expiry": "2016-07-25", | |||
"users": 1 | |||
} | |||
} | |||
Puedes establecer un límite ejecutando: | |||
bench --site [nombre_sitio] set-limit [limite] [valor] | |||
Puedes establecer varios límites al mismo tiempo ejecutando: | |||
bench --site [nombre_sitio] set-limits --limit [limite] [valor] --limit [limite-2] [valor-2] | |||
Los límites que puedes configurar son: | |||
- **users** - Limita el número de usuarios por site. | |||
- **emails** - Limita el número de correos enviados por mes desde un site. | |||
- **space** - Limita el máximo número de espacio en GB que el site puede usar. | |||
- **email_group** - Limia el número máximo de miembros en un grupo de correos. | |||
- **expiry** - Fecha de expiración para el site. (YYYY-MM-DD en de comillas) | |||
Ejemplo: | |||
bench --site site1.local set-limit users 5 | |||
Puedes verificar el uso abriendo la página de "Usage Info" ubicada en el toolbar / AwesomeBar. Un límite solo va a mostrarse en la página si ha sido configurado. | |||
<img class="screenshot" alt="Doctype Saved" src="{{docs_base_url}}/assets/img/usage_info.png"> |
@@ -0,0 +1,57 @@ | |||
Asumiento que tiene su primer site corriendo y ha realizado los | |||
[pasos para producción](setup-production.html), esta sección explica como montar su segundo site (y más). | |||
Su primer site se configuró como el site por defecto de forma automática. Puedes cambiarlo ejecutando el comando, | |||
bench use nombre_site | |||
Multitenancy basada en puertos | |||
----------------------- | |||
Puedes crear un nuevo site y ponerlo a escuchar por otro puerto (mientras que el primero corre en el puerto 80) | |||
* Desactivar el multitenancy basada en DNS (una vez) | |||
`bench config dns_multitenant off` | |||
* Crea un nuevo site | |||
`bench new-site site2name` | |||
* Configura el puerto | |||
`bench set-nginx-port site2name 82` | |||
* Regenera las configuraciones de nginx | |||
`bench setup nginx` | |||
* Recarga el servicio de nginx | |||
`sudo service nginx reload` | |||
Multitenancy basada en DNS | |||
---------------------- | |||
Puedes nombrar sus sites como los los nombre de dominio que van a rederigirse a ellos. Así, todos los sites agregados al bench van a correr en el mismo puerto y van a ser automáticamente seleccionados basados en el nombre del host. | |||
Para convertir un site nuevo dentro de la multitenancy basada en DNS, realiza los siguientes pasos. | |||
* Desactivar el multitenancy basada en DNS (una vez) | |||
`bench config dns_multitenant on` | |||
* Crea un nuevo site | |||
`bench new-site site2name` | |||
* Regenera las configuraciones de nginx | |||
`bench setup nginx` | |||
* Recarga el servicio de nginx | |||
`sudo service nginx reload` |
@@ -0,0 +1,48 @@ | |||
Puedes configurar el bench para producción configurando dos parametros, Supervisor y nginx. Si quieres volver a ponerlo en desarrollo debes ver [estos comandos](https://github.com/frappe/bench/wiki/Stopping-Production-and-starting-Development) | |||
####Configuración para producción facíl | |||
Estos pasos son automátizados si ejecutas `sudo bench setup production` | |||
####Configuración manual para producción | |||
Supervisor | |||
---------- | |||
Supervisor se asegura de mantener el proceso que inició Frappe corriendo y lo reinicia en caso de cualquier inconveniente. | |||
Puedes generar la configuración necesaria para supervisor ejecutando el comando `bench setup supervisor`. | |||
La configuración va a estar disponible en la carpeta `config/supervisor.conf`. Luego puedes copiar/enlazar este archivo al directorio de configuración | |||
de supervisor y reiniciar el servicio para que tome efecto de los cambios realizados. | |||
Ejemplo, | |||
``` | |||
bench setup supervisor | |||
sudo ln -s `pwd`/config/supervisor.conf /etc/supervisor/conf.d/frappe-bench.conf | |||
``` | |||
Nota: Para CentOS 7, la extensión debería ser `ini`, por lo que el comando sería | |||
``` | |||
bench setup supervisor | |||
sudo ln -s `pwd`/config/supervisor.conf /etc/supervisor/conf.d/frappe-bench.ini #para CentOS 7 solamente | |||
``` | |||
El bench también necesita reiniciar el proceso manejado por supervisor cuando actualizar cualquier aplicación. | |||
Para automatizarlo, vas a tener que agregar el usuario a sudoers ejecutando `sudo bench setup sudoers $(whoami)`. | |||
Nginx | |||
----- | |||
Nginx es un servidor web y lo usamos para servir archivos estáticos y aponderar el resto de la | |||
peticiones a frappe. Puedes generar las configuraciones necesarias para nginx usando el comando `bench setup nginx`. | |||
La configuración va a estar almacenada en el archivo `config/nginx.conf`. Entonces puedes copiar/enlazar este archivo al directorio de | |||
configuración de nginx y reiniar el servicio para poder ver si se han aplicado los cambios. | |||
Ejemplo, | |||
``` | |||
bench setup nginx | |||
sudo ln -s `pwd`/config/nginx.conf /etc/nginx/conf.d/frappe-bench.conf | |||
``` | |||
Nota: Cuando reinicias nginx despues de cualquier cambio en la configuración, podría fallar si tienes otra configuración con el bloque server para el puerto 80 (En la mayoría de veces la página princial de nginx). Vas a tener que deshabilitar esta configuración. Las rutas más probables donde podemos encontrarlo son `/etc/nginx/conf.d/default.conf` y | |||
`/etc/nginx/conf.d/default`. |
@@ -0,0 +1 @@ | |||
{index} |
@@ -0,0 +1,2 @@ | |||
guides | |||
resources |
@@ -0,0 +1,29 @@ | |||
Servicios Externos | |||
----------------- | |||
* MariaDB (Base de datos) | |||
* Redis (Caché y background workers) | |||
* nginx (para producción) | |||
* supervisor (para producción) | |||
Procesos de Frappe | |||
---------------- | |||
* Servidor WSGI | |||
* El servidor WSGI es responsable de responder a las peticiones HTTP. | |||
En entornos de desarrollo (`bench serve` o `bench start`), El servidor WSGI Werkzeug es usado y en producción, | |||
se usa gunicorn (automáticamente configurado en supervisor) | |||
* Procesos de Redis Worker | |||
* Los procesos de Celery se encargan de ejecutar tareas en background en Frappe. | |||
Estos procesos son iniciados automáticamente cuando se ejecuta el comando `bench start` y | |||
para producción se configuran en las configuraciones de supervisor. | |||
* Procesos Scheduler | |||
* Los procesos del Scheduler programan la lista de tareas programadas en Frappe. | |||
Este proceso es iniciado automáticamente cuando se ejecuta el comando `bench start` y | |||
para producción se configuran en las configuraciones de supervisor. |
@@ -0,0 +1,90 @@ | |||
### Uso General | |||
* `bench --version` - Muestra la versión del bench | |||
* `bench src` - Muestra el directorio repo del bench | |||
* `bench --help` - Muestra todos los comandos y ayudas | |||
* `bench [command] --help` - Muestra la ayuda para un comando | |||
* `bench init [bench-name]` - Crea un nuevo bench (Ejecutar desde Home) | |||
* `bench --site [site-name] COMMAND` - Especificar un site para el comando | |||
* `bench update` - Buscar los últimos cambios de bench-repo y todas las aplicaciones, aplica parches, crea los JS y CSS, y realiza la migración. | |||
* `--pull` Hace un Pull a todas las aplicaciones en el bench | |||
* `--patch` Ejecuta las migraciones para todos los sites en el bench | |||
* `--build` Crea los JS y CSS para el bench | |||
* `--bench` Actualiza el bench | |||
* `--requirements` Actualiza los requerimientos | |||
* `--restart-supervisor` Reinicia los procesos de supervisor despues de actualizar | |||
* `--upgrade` Realiza migraciones mayores (Eg. ERPNext 6 -> 7) | |||
* `--no-backup` No crea una copia de respaldo antes de actualizar | |||
* `bench restart` Reinicia todos los servicios del bench | |||
* `bench backup` Copia de respaldo | |||
* `bench backup-all-sites` Copia de respaldo a todos los sites | |||
* `--with-files` Copia de respaldo a los sites con los archivos | |||
* `bench restore` Restaurar | |||
* `--with-private-files` Restaura un site con todos los archivos privados (Ruta al archivo .tar) | |||
* `--with-public-files` Restaura un site con todos los archivos públicos (Ruta al archivo .tar) | |||
* `bench migrate` Leerá los archivos JSON y realizará los cambios en la base de datos. | |||
###Configuración | |||
* `bench config` - Cambiar las configuraciones del bench | |||
* `auto_update [on/off]` Activa/Desactiva las actualizaciones automáticas para el bench | |||
* `dns_multitenant [on/off]` Activa/Desactiva DNS Multitenancy | |||
* `http_timeout` Establece un timeout para http | |||
* `restart_supervisor_on_update` Activa/Desactiva el reinicio automático de supervisor | |||
* `serve_default_site` Configurar nginx para que sirva el sitio predeterminado en... | |||
* `update_bench_on_update` Activa/Desactiva las actualizaciones en un bench corriendo | |||
* `bench setup` - Configurar componentes | |||
* `auto-update` Añade un cronjob para actualizaciones automática del bench | |||
* `backups ` Añade un cronjob para las copias de respaldo del bench | |||
* `config ` sobreescribe o crea config.json | |||
* `env ` Configurar un virtualenv para el bench | |||
* `nginx ` generar configuraciones para nginx | |||
* `procfile ` Configura el archivo Procfile para bench start | |||
* `production ` Configura el bench para producción | |||
* `redis ` genera las configuraciones para redis cache | |||
* `socketio ` Configura las dependencias de node para el servidor socketio | |||
* `sudoers ` Agrega comandos a la sudoers para su ejecución | |||
* `supervisor ` Genera las configuraciones para supervisor | |||
* `add-domain ` agrega un dominio personalizado para un site | |||
* `firewall ` configura un firewall y bloquea todos los puertos en excepción el 22, 80 y 443 | |||
* `ssh-port ` cambia el puerto por defecto para conexiones ssh | |||
###Desarrollo | |||
* `bench new-app [app-name]` Crea una nueva app | |||
* `bench get-app [repo-link]` - Descarga una app desde un repositorio git y la instala | |||
* `bench install-app [app-name]` Instala aplicaciones existentes | |||
* `bench remove-from-installed-apps [app-name]` Remueve aplicaciones de la liste de aplicaciones | |||
* `bench uninstall-app [app-name]` Elimina la aplicación y todo lo relaciones a esa aplicación (Bench necesita estar corriendo) | |||
* `bench remove-app [app-name]` Eliminar una aplicación completamente del bench | |||
* `bench --site [sitename] --force reinstall ` Reiniciar con una base de datos nueva (Atención: Va a borrar la base de datos anterior) | |||
* `bench new-site [sitename]` - Crea un nuevo site | |||
* `--db-name` Nombre de la base de datos | |||
* `--mariadb-root-username` Nombre de usuario de Root | |||
* `--mariadb-root-password` Contraseña del usuario Root | |||
* `--admin-password` Contraseña del usuario Administrator para un nuevo site | |||
* `--verbose` Verbose | |||
* `--force` Forzar la restauración si el site/base de datos existen. | |||
* `--source_sql` Inicializar una base de datos con un archivo SQL | |||
* `--install-app` Instalar una aplicación despues de haber instalado el bench | |||
* `bench use [site]` Configura el site por defecto | |||
* `bench drop-site` Elimina sites del disco y la base de datos completamente | |||
* `--root-login` | |||
* `--root-password` | |||
* `bench set-config [key] [value]` Agrega valores clave-valor al archivo de configuración del site | |||
* `bench console` Abre una consola de IPython en el virtualenv del bench | |||
* `bench execute` Ejecuta un método dentro de una aplicación | |||
* Eg : `bench execute frappe.utils.scheduler.enqueue_scheduler_events` | |||
* `bench mysql` Abre una consola SQL | |||
* `bench run-tests` Ejecuta las pruebas | |||
* `--app` Nombre de la aplicación | |||
* `--doctype` Especificar el DocType para cual correr las pruebas | |||
* `--test` Pruebas específicas | |||
* `--module` Ejecutar un módulo con pruebas en específico | |||
* `--profile` Ejecutar un Python profiler en las pruebas | |||
* `bench disable-production` Desactiva el entorno de producción | |||
###Programador | |||
* `bench enable-scheduler` - Habilita el Programador que ejecutará las tareas programadas | |||
* `bench doctor` - Obtener informaciones de diagnóstico sobre los background workers | |||
* `bench show-pending-jobs`- Obtener las tareas pendientes | |||
* `bench purge-jobs` - Eliminar todas las tareas pendientes |
@@ -0,0 +1,31 @@ | |||
`bench start` usa [honcho](http://honcho.readthedocs.org) para manejar múltiples procesos en **developer mode**. | |||
### Procesos | |||
Los diversos procesos que se necesitan para correr frappe son: | |||
1. `bench start` - El servidor web. | |||
4. `redis_cache` para cache (general) | |||
5. `redis_queue` para manejar las cosas de los background workers | |||
6. `redis_socketio` como un notificador de notificaciones para actualizaciones en tiempo real desde los background workers | |||
7. `web` para el servidor web de frappe. | |||
7. `socketio` para mensajes en tiempo real. | |||
3. `schedule` para disparar tareas periódicas | |||
3. `worker_*` redis workers para manejar trabajos aíncronos | |||
Opcionalmente, si estas desarrollando en frappe puedes agregar: | |||
`bench watch` para automáticamente construir la aplicación javascript desk. | |||
### Ejemplo | |||
redis_cache: redis-server config/redis_cache.conf | |||
redis_socketio: redis-server config/redis_socketio.conf | |||
redis_queue: redis-server config/redis_queue.conf | |||
web: bench serve --port 8000 | |||
socketio: /usr/bin/node apps/frappe/socketio.js | |||
watch: bench watch | |||
schedule: bench schedule | |||
worker_short: bench worker --queue short | |||
worker_long: bench worker --queue long | |||
worker_default: bench worker --queue default |
@@ -0,0 +1 @@ | |||
{index} |
@@ -0,0 +1,3 @@ | |||
background-services | |||
bench-commands-cheatsheet | |||
bench-procfile |
@@ -0,0 +1,3 @@ | |||
# Desarrollo de aplicaciones con Frappe | |||
{index} |
@@ -0,0 +1,4 @@ | |||
tutorial | |||
bench | |||
guides | |||
videos |
@@ -0,0 +1,10 @@ | |||
# Qué es una aplicación | |||
Una aplicación en Frappe es una aplicación estandar en Python. Puedes estructurar una aplicación hecha en Frappe de la misma forma que estructuras una aplicación en Python. | |||
Para implementación, Frappe usa los Python Setuptools, lo que nos permite facilmente instalar la aplicación en cualquier computadora. | |||
El Framework Frappe provee una interfaz WSGI y para el desarrollo puedes usar el servidor interno de frappe llamado Werkzeug. Para implementación en producción, recomendamos usar nginx y gunicorn. | |||
Frappe tambien soporta la architectura multi-tenant. Esto significa que puedes correr varios "sitios" en su instalación, cada uno de ellos estará poniendo a disposición un conjunto de aplicaciones y usuarios. La base de datos para cada sitio es separada. | |||
{next} |
@@ -0,0 +1,71 @@ | |||
# Antes de empezar | |||
<p class="lead">Una lista de recursos que te ayudaran a inicar con el desarrollo de aplicaciones usando Frappe.</p> | |||
--- | |||
#### 1. Python | |||
Frappe usa Python (v2.7) como lenguaje de parte del servidor. Es altamente recomendable aprender Python antes de iniciar a crear aplicaciones con Frappe. | |||
Para escribir código de calidad del lado del servidor, también debes incluir pruebas automatizadas. | |||
Recursos: | |||
1. [Tutorial sobre Python de Codecademy](https://www.codecademy.com/learn/python) | |||
1. [Tutorial Oficial de Python](https://docs.python.org/2.7/tutorial/index.html) | |||
1. [Tutorial básico de Test-driven development](http://code.tutsplus.com/tutorials/beginning-test-driven-development-in-python--net-30137) | |||
--- | |||
#### 2. MariaDB / MySQL | |||
Para crear aplicaciones con frappe, debes entender los conceptops básicos del manejo de base de datos, como instalarlas, acceder, crear nueva base de datos, y hacer consultas básicas con SQL. | |||
Recursos: | |||
1. [Tutorial sobre SQL de Codecademy](https://www.codecademy.com/learn/learn-sql) | |||
1. [Tutorial Básico de MySQL de DigitalOcean](https://www.digitalocean.com/community/tutorials/a-basic-mysql-tutorial) | |||
1. [Introducción a MariaDB](https://mariadb.com/kb/en/mariadb/documentation/getting-started/) | |||
--- | |||
#### 3. HTML / CSS | |||
Si quieres construir interfaces de usuario usando Frappe, necesitas aprender los conceptops básicos de HTML / CSS y el framework de CSS Bootstrap. | |||
Recursos: | |||
1. [Tutorial sobre HTML/CSS de Codecademy](https://www.codecademy.com/learn/learn-html-css) | |||
1. [Introducción a Bootstrap](https://getbootstrap.com/getting-started/) | |||
--- | |||
#### 4. JavaScript and jQuery | |||
Para modificar formularios y crear interfaces de usuarios interactivas, deberías aprender JavaScript y la librería JQuery. | |||
Recursos: | |||
1. [Tutorial sobre JavaScript de Codecademy](https://www.codecademy.com/learn/learn-javascript) | |||
1. [Tutorial sobre jQuery de Codecademy](https://www.codecademy.com/learn/jquery) | |||
--- | |||
#### 5. Manejar de plantillas Jinja | |||
Si estas modificando plantillas de Impresión o Páginas Web, tienes que aprender a utilizar el manejar de plantillas Jinja. Es una forma facíl de crear páginas web dinámicas. | |||
Recursos: | |||
1. [Primer on Jinja Templating](https://realpython.com/blog/python/primer-on-jinja-templating/) | |||
1. [Documentación oficial](http://jinja.pocoo.org/) | |||
--- | |||
#### 6. Git and GitHub | |||
Aprende como contribuir en un proyecto de código abierto usando Git y GitHub, dos increíbles herramientes que te ayudan a gestionar tu código y compartirlo con otros. | |||
Recursos: | |||
1. [Tutorial Básico de Git](https://try.github.io) | |||
2. [Cómo contribuir al Código Abierto](https://opensource.guide/how-to-contribute/) | |||
--- | |||
Cuando estes listo, puedes intentar [crear una aplicación simple]({{ docs_base_url }}/user/es/tutorial/app) usando Frappe. |
@@ -0,0 +1,11 @@ | |||
# Instalando el Frappe Bench | |||
La forma más facíl de configurar frappe en un computador usando sitemas basados en Unix es usando frappe-bench. Lee las instrucciones detalladas acerca de como instalarlo usando Frappe Bench. | |||
> [https://github.com/frappe/bench](https://github.com/frappe/bench) | |||
Con Frappe Bench vas a poder configurar y hostear multiples aplicaciones y sitios, también va a configurar un entorno virtual de Python por lo que vas a tener un entorno apartado para correr sus aplicaciones (y no va a tener conflictos de versiones con otros entornos de desarrollo). | |||
El comando `bench` va a ser instalado en su sistema para ayudarlo en la fase de desarrollo y el manejo de la aplicación. | |||
{next} |
@@ -0,0 +1,5 @@ | |||
# Conclusión | |||
Esperamos que esto te haya dado una idea de como son desarrolladas las aplicaicones en Frappe. El objetivo era de que de manera breve se tocaran varios de los aspectos del desarrollo de aplicaciones. Para obtener ayuda en inconvenientes o temas específicos, favor revisar el API. | |||
Para ayuda, únete a la comunidad en el [canal de chat en Gitter](https://gitter.im/frappe/erpnext) o el [foro de desarrollo](https://discuss.erpnext.com) |
@@ -0,0 +1,59 @@ | |||
# Controladores (Controllers) | |||
El siguiente paso va a ser agregar métodos y eventos a los modelos. En la aplicación, debemos asegurar que si una Library Transaction es creada, el Article que se solicita debe estar en disponibilidad y el miembro que lo solicita debe tener una membresía (membership) válida. | |||
Para esto, podemos escribir una validación que se verifique justo en el momento que una Library Transaction es guardada. Para lograrlo, abre el archivo `library_management/doctype/library_transaction/library_transaction.py`. | |||
Este archivo es el controlador para el objeto Library Transaction. En este archivo puedes escribir métodos para: | |||
1. `before_insert` | |||
1. `validate` (Antes de insertar o actualizar) | |||
1. `on_update` (Despues de guardar) | |||
1. `on_submit` (Cuando el documento es presentado como sometido o presentado) | |||
1. `on_cancel` | |||
1. `on_trash` (antes de ser eliminado) | |||
Puedes escribir métodos para estos eventos y estos van a ser llamados por el framework automóticamente cuando el documento pase por uno de esos estados. | |||
Aquí les dejo el controlador completo: | |||
from __future__ import unicode_literals | |||
import frappe | |||
from frappe import _ | |||
from frappe.model.document import Document | |||
class LibraryTransaction(Document): | |||
def validate(self): | |||
last_transaction = frappe.get_list("Library Transaction", | |||
fields=["transaction_type", "transaction_date"], | |||
filters = { | |||
"article": self.article, | |||
"transaction_date": ("<=", self.transaction_date), | |||
"name": ("!=", self.name) | |||
}) | |||
if self.transaction_type=="Issue": | |||
msg = _("Article {0} {1} no ha sido marcado como retornado desde {2}") | |||
if last_transaction and last_transaction[0].transaction_type=="Issue": | |||
frappe.throw(msg.format(self.article, self.article_name, | |||
last_transaction[0].transaction_date)) | |||
else: | |||
if not last_transaction or last_transaction[0].transaction_type!="Issue": | |||
frappe.throw(_("No puedes retornar un Article que no ha sido prestado.")) | |||
En este script: | |||
1. Obtenemos la última transacción antes de la fecha de la transacción actual usando la funcion `frappe.get_list` | |||
1. Si la última transacción es algo que no nos gusta, lanzamos una excepción usando `frappe.throw` | |||
1. Usamos el método `_("texto")` para identificar las cadenas que pueden ser traducidas. | |||
Verifica si sus validaciones funcionan creando nuevos registros. | |||
<img class="screenshot" alt="Transaction" src="{{docs_base_url}}/assets/img/lib_trans.png"> | |||
#### Depurando | |||
Para depurar, siempre mantener abierta su consola JS. Verifíca rastros de Javascript y del Servidor. | |||
Siempre verifica su terminal para las excepciones. Cualquier **500 Internal Server Errors** va a ser mostrado en la terminal en la que está corriendo el servidor. | |||
{next} |
@@ -0,0 +1,31 @@ | |||
# Estructura de directorios de un DocType | |||
Despues de guardar los DocTypes, revisa que los archivos `.json` y `.py` fuueron creados en módulo `apps/library_management/library_management`. La estructura de directorios despues de crear los modelos debería ser similar a la siguiente: | |||
. | |||
├── MANIFEST.in | |||
├── README.md | |||
├── library_management | |||
.. | |||
│ ├── library_management | |||
│ │ ├── __init__.py | |||
│ │ └── doctype | |||
│ │ ├── __init__.py | |||
│ │ ├── article | |||
│ │ │ ├── __init__.py | |||
│ │ │ ├── article.json | |||
│ │ │ └── article.py | |||
│ │ ├── library_member | |||
│ │ │ ├── __init__.py | |||
│ │ │ ├── library_member.json | |||
│ │ │ └── library_member.py | |||
│ │ ├── library_membership | |||
│ │ │ ├── __init__.py | |||
│ │ │ ├── library_membership.json | |||
│ │ │ └── library_membership.py | |||
│ │ └── library_transaction | |||
│ │ ├── __init__.py | |||
│ │ ├── library_transaction.json | |||
│ │ └── library_transaction.py | |||
{next} |
@@ -0,0 +1,96 @@ | |||
# DocType | |||
Despues de crear los Roles, vamos a crear los **DocTypes** | |||
Para crear un nuevo **DocType**, ir a: | |||
> Developer > Documents > Doctype > New | |||
<img class="screenshot" alt="New Doctype" src="{{docs_base_url}}/assets/img/doctype_new.png"> | |||
En el DocType, primero el módulo, lo que en nuestro caso es **Library Management** | |||
#### Agregando Campos | |||
En la tabla de campos, puedes agregar los campos (propiedades) de el DocType (Article). | |||
Los campos son mucho más que solo columnas en la base de datos, pueden ser: | |||
Fields are much more than database columns, they can be: | |||
1. Columnas en la base de datos | |||
1. Ayudantes de diseño (definidores de secciones / columnas) | |||
1. Tablas hijas (Tipo de dato Table) | |||
1. HTML | |||
1. Acciones (botones) | |||
1. Adjuntos o Imagenes | |||
Vamos a agregar los campos de el Article. | |||
<img class="screenshot" alt="Adding Fields" src="{{docs_base_url}}/assets/img/doctype_adding_field.png"> | |||
Cuando agredas los campos, necesitas llenar el campo **Type**. **Label** es opcional para los Section Break y Column Break. **Name** (`fieldname`) es el nombre de la columna en la tabla de la base de datos y tambien el nombre de la propiedad para el controlador. Esto tiene que ser *code friendly*, i.e. Necesitas poner _ en lugar de " ". Si dejas en blanco este campo, se va a llenar automáticamente al momento de guardar. | |||
Puedes establecer otras propiedades al campo como si es obligatorio o no, si es de solo lectura, etc. | |||
Podemos agregar los siguientes campos: | |||
1. Article Name (Data) | |||
2. Author (Data) | |||
3. Description | |||
4. ISBN | |||
5. Status (Select): Para los campos de tipo Select, vas a escribir las opciones. Escribe **Issued** y **Available** cada una en una linea diferente en la caja de texto de Options. Ver el diagrama más abajo. | |||
6. Publisher (Data) | |||
7. Language (Data) | |||
8. Image (Adjuntar Imagen) | |||
#### Agregar permisos | |||
Despues de agregar los campos, dar click en hecho y agrega una nueva fila en la sección de Permission Roles. Por ahora, vamos a darle accesos Lectura, Escritura, Creación y Reportes al Role **Librarian**. Frappe cuenta con un sistema basados en el modelo de Roles finamente granulado. Puedes cambiar los permisos más adealante usando el **Role Permissions Manager** desde **Setup**. | |||
<img class="screenshot" alt="Adding Permissions" src="{{docs_base_url}}/assets/img/doctype_adding_permission.png"> | |||
#### Guardando | |||
Dar click en el botón de **Guardar**. Cuando el botón es clickeado, una ventana emergente le va a preguntar por el nombre. Vamos a darle el nombre de **Article** y guarda el DocType. | |||
Ahora accede a mysql y verifica que en la base de datos que se ha creado una nueva tabla llamada tabArticle. | |||
$ bench mysql | |||
Welcome to the MariaDB monitor. Commands end with ; or \g. | |||
Your MariaDB connection id is 3931 | |||
Server version: 5.5.36-MariaDB-log Homebrew | |||
Copyright (c) 2000, 2014, Oracle, Monty Program Ab and others. | |||
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. | |||
MariaDB [library]> DESC tabArticle; | |||
+--------------+--------------+------+-----+---------+-------+ | |||
| Field | Type | Null | Key | Default | Extra | | |||
+--------------+--------------+------+-----+---------+-------+ | |||
| name | varchar(255) | NO | PRI | NULL | | | |||
| creation | datetime(6) | YES | | NULL | | | |||
| modified | datetime(6) | YES | | NULL | | | |||
| modified_by | varchar(40) | YES | | NULL | | | |||
| owner | varchar(60) | YES | | NULL | | | |||
| docstatus | int(1) | YES | | 0 | | | |||
| parent | varchar(255) | YES | MUL | NULL | | | |||
| parentfield | varchar(255) | YES | | NULL | | | |||
| parenttype | varchar(255) | YES | | NULL | | | |||
| idx | int(8) | YES | | NULL | | | |||
| article_name | varchar(255) | YES | | NULL | | | |||
| status | varchar(255) | YES | | NULL | | | |||
| description | text | YES | | NULL | | | |||
| image | varchar(255) | YES | | NULL | | | |||
| publisher | varchar(255) | YES | | NULL | | | |||
| isbn | varchar(255) | YES | | NULL | | | |||
| language | varchar(255) | YES | | NULL | | | |||
| author | varchar(255) | YES | | NULL | | | |||
+--------------+--------------+------+-----+---------+-------+ | |||
18 rows in set (0.00 sec) | |||
Como puedes ver, junto con los DocFields, algunas columnas fueron agregadas a la tabla. Las importantes a notar son, la clave primaria, `name`, `owner`(El usuario que creo el registro), | |||
`creation` y `modified` (timestamps para la creación y última modificación). | |||
{next} |
@@ -0,0 +1,39 @@ | |||
## Añadir Scripts a nuestros formularios | |||
Ya que tenemos creado el sistema básico que funciona sin problemas sin escribir una linea de código. Vamos a escribir algunos scripts | |||
para hablar la aplicación más interactiva y agregar validaciones para que el usuario no pueda introducir información erronea. | |||
### Scripts del lado del Cliente | |||
En el DocType **Library Transaction**, solo tenemos campo para el Nombre del miembro. No hemos creado dos campos. Esto podría ser dos campos (y probablemente debería), pero para los motivos del ejemplo, vamos a considerar que tenemos que implementarlo así. Para hacerlo vamos a tener que escribir un manejador de eventos para el evento que ocurre cuando el usuario selecciona el campo `library_member` y luego accede a la información del miembro desde el servidor usando el REST API y cambia los valores en el formulario. | |||
Para empezar el script, en el directorio `library_management/doctype/library_transaction`, crea un nuevo archivo `library_transaction.js`. | |||
Este archivo va a ser ejecutado automáticamente cuando la primer Library Transaction es abierta por el usuario. En este archivo, podemos establecer eventos y escribir otras funciones. | |||
#### library_transaction.js | |||
frappe.ui.form.on("Library Transaction", "library_member", | |||
function(frm) { | |||
frappe.call({ | |||
"method": "frappe.client.get", | |||
args: { | |||
doctype: "Library Member", | |||
name: frm.doc.library_member | |||
}, | |||
callback: function (data) { | |||
frappe.model.set_value(frm.doctype, | |||
frm.docname, "member_name", | |||
data.message.first_name | |||
+ (data.message.last_name ? | |||
(" " + data.message.last_name) : "")) | |||
} | |||
}) | |||
}); | |||
1. **frappe.ui.form.on(*doctype*, *fieldname*, *handler*)** es usada para establecer un manejador de eventos cuando la propiedad library_member es seleccionada. | |||
1. En el manejador, vamos a disparar una llamada AJAX a `frappe.client.get`. En respuesta obtenemos el objeto consultado en formato JSON. [Aprende más acerca del API](/frappe/user/en/guides/integration/rest_api). | |||
1. Usando **frappe.model.set_value(*doctype*, *name*, *fieldname*, *value*)** cambiamos el valor en el formulario. | |||
**Nota:** Para verificar si su script funciona, recuerda Recargar/Reload la página antes de probar el script. Los cambios realizados a los script del lado del Cliente no son automáticamente cargados nuevamente cuando estas en modo desarrollador. | |||
{next} |
@@ -0,0 +1,34 @@ | |||
# Tutorial sobre Frappe | |||
En esta guía, vamos a mostrarte como crear una aplicación desde cero usando **Frappe**. Usando el ejemplo de un Sistema de Gestión de Librería. Vamos a cubrir: | |||
1. Instalación | |||
1. Creando una nueva App | |||
1. Creando Modelos | |||
1. Creando Usuarios y Registros | |||
1. Creando Controladores | |||
1. Creando Vistas Web | |||
1. Configurando Hooks y Tareas | |||
## Para Quién es este tutorial? | |||
Esta guía esta orientada para desarrolladores de software que estan familiarizados con el proceso de como son creadas y servidas las aplicaciones web. El Framework Frappe está escrito en Python y usa MariaDB como base de datos y para la creación de las vistas web usa HTML/CSS/Javascript. Por lo que sería excelente si estas familiarizado con estas tecnologías. | |||
Por lo menos, si nunca haz usado Python antes, deberías tomar un tutorial rápido antes de iniciar con este tutorial. | |||
Frappe usa el sistema de gestión de versiones en GitHub. También, es importante estar familiarizado con los conceptos básicos de git y tener una cuenta en GitHub para manejar sus aplicaciones. | |||
## Ejemplo | |||
Para esta guía, vamos a crear una aplicación simple llamada **Library Management**. En esta aplicación vamos a tener los siguientes modelos (Permanecerán en inglés para que coincidan con las imagenes): | |||
1. Article (Libro o cualquier otro artículo que pueda ser prestado) | |||
1. Library Member | |||
1. Library Transaction (Entrega o Retorno de un artículo) | |||
1. Library Membership (Un período en el que un miembro esta permitido hacer una trasacción) | |||
1. Library Management Setting (Configuraciones generales, como el tiempo que dura el prestamo de un artículo) | |||
La interfaz de usuario (UI) para la aplicación va a ser el **Frappe Desk**, un entorno para UI basado en el navegador y viene integrado en Frappe donde los formularios son generados automáticamente desde los modelos y los roles y permisos son aplicados. | |||
También, vamos a crear vistas webs para la librería donde los usuarios pueden buscar los artículos desde una página web. | |||
{index} |
@@ -0,0 +1,19 @@ | |||
before | |||
app | |||
bench | |||
new-app | |||
setting-up-the-site | |||
start | |||
models | |||
roles | |||
doctypes | |||
naming-and-linking | |||
doctype-directory-structure | |||
users-and-records | |||
form-client-scripting | |||
controllers | |||
reports | |||
web-views | |||
single-doctypes | |||
task-runner | |||
conclusion |
@@ -0,0 +1,19 @@ | |||
# Creando Modelos | |||
El siguiente paso es crear los modelos que discutimos en la introducción. En Frappe, los modelos son llamados **DocTypes**. Puedes crear nuevos DocTypes desde el UI Escritorio de Frappe. **DocTypes** son creados de campos llamados **DocField** y los permisos basados en roles son integrados dentro de los modelos, estos son llamados **DocPerms**. | |||
Cuando un DocType es guardado, se crea una nueva tabla en la base de datos. Esta tabla se nombra `tab[doctype]`. | |||
Cuando creas un **DocType** una nueva carpeta es creada en el **Module** y un archivo JSON y una platilla de un controlador en Python son creados automáticamente. Cuando modificas un DocType, el archivo JSON es modificado y cada vez que se ejecuta `bench migrate`, sincroniza el archivo JSON con la tabla en la base de datos. Esto hace que sea más facíl reflejar los cambios hechos al esquema y migrarlo. | |||
### Modo desarrollador | |||
Para crear modelos, debes setear `developer_mode` a 1 en el archivo `site_config.json` ubicados en /sites/library y ejecuta el comando `bench clear-cache` o usa el menú de usuario en el Escritorio y da click en "Recargar/Reload" para que los cambios tomen efecto. Deberías poder ver la aplicación llamada "Developer" en su escritorio. | |||
{ | |||
"db_name": "bcad64afbf", | |||
"db_password": "v3qHDeVKvWVi7s97", | |||
"developer_mode": 1 | |||
} | |||
{next} |
@@ -0,0 +1,71 @@ | |||
# Nombrando y Asociando DocType | |||
Vamos a crear otro DocType y guardarlo: | |||
1. Library Member (First Name, Last Name, Email Address, Phone, Address) | |||
<img class="screenshot" alt="Doctype Saved" src="{{docs_base_url}}/assets/img/naming_doctype.png"> | |||
#### Nombrando DocTypes | |||
Los DocTypes pueden ser nombrados en diferentes maneras: | |||
1. Basados en un campo | |||
1. Basados en una serie | |||
1. A traves del controlador (vía código) | |||
1. Con un promt | |||
Esto puede ser seteado a traves del campo **Autoname**. Para el controlador, dejar en blanco. | |||
> **Search Fields**: Un DocType puede ser nombrado por serie pero seguir teniendo la necesidad de ser buscado por nombre. En nuestro caso, el Article va ser buscado por el título o el nombre del autor. Por lo que vamos a poner esos campos en el campo de search. | |||
<img class="screenshot" alt="Autonaming and Search Field" src="{{docs_base_url}}/assets/img/autoname_and_search_field.png"> | |||
#### Campo de Enlace y Campo Select | |||
Las claves foraneas son específicadas en Frappe como campos **Link** (Enlace). El DocType debe ser mencionado en el area de texto de Options. | |||
En nuestro ejemplo, en el DocType de Library Transaction,tenemos que enlazar los dos DocTypes de Library Member and the Article. | |||
**Nota:** Recuerda que los campos de Enlace no son automáticamente establecidos como claves foraneas en la base de datos MariaDB, porque esto crearía un indice en la columna. Las validaciones de claves foraneas son realizadas por el Framework. | |||
<img class="screenshot" alt="Link Field" src="{{docs_base_url}}/assets/img/link_field.png"> | |||
Por campos de tipo Select, como mencionamos antes, agrega varias opciones en la caja de texto **Options**, cada una en una nueva linea. | |||
<img class="screenshot" alt="Select Field" src="{{docs_base_url}}/assets/img/select_field.png"> | |||
De manera similar continua haciendo los otros modelos. | |||
#### Valores enlazados | |||
Un patrón estandar es que cuando seleccionas un ID, dice **Library Member** en **Library Membership**, entonces el nombre y apellido del miembro deberian ser copiados en campos relevantes de el Doctype Library Membership Transaction. | |||
Para hacer esto, podemos usar campos de solo lectura y en opciones, podemos especificar el nombre del link (enlace) y el campo o propiedad que deseas obtener. Para este ejempo en **Member First Name** podemos especificar `library_member.first_name`. | |||
<img class="screenshot" alt="Fetch values" src="{{docs_base_url}}/assets/img/fetch.png"> | |||
### Completar los modelos | |||
En la misma forma, puedes completar todos los modelos, todos los campos deben verse de esta manera | |||
#### Article | |||
<img class="screenshot" alt="Article" src="{{docs_base_url}}/assets/img/doctype_article.png"> | |||
#### Library Member | |||
<img class="screenshot" alt="Library Member" src="{{docs_base_url}}/assets/img/doctype_lib_member.png"> | |||
#### Library Membership | |||
<img class="screenshot" alt="Library Membership" src="{{docs_base_url}}/assets/img/doctype_lib_membership.png"> | |||
#### Library Transaction | |||
<img class="screenshot" alt="Library Transaction" src="{{docs_base_url}}/assets/img/doctype_lib_trans.png"> | |||
> Asegurate de dar permiso a **Librarian** en cada DocType | |||
{next} |
@@ -0,0 +1,54 @@ | |||
# Creando una nueva aplicación | |||
Una vez el bench esté instalado, vas a ver dos directorios principales, `apps` and `sites`. Todas las aplicaciones van a ser instaladas en apps. | |||
Para crear una nueva aplicación, debes posicionarte en el directorio del bench y ejecutar `bench new-app {app_name}` y llenar los detalles de la aplicación. Esto a va crear los directorios y archivos necesarios para una aplicación. | |||
$ bench new-app library_management | |||
App Title (defaut: Lib Mgt): Library Management | |||
App Description: App for managing Articles, Members, Memberships and Transactions for Libraries | |||
App Publisher: Frappe | |||
App Email: info@frappe.io | |||
App Icon (default 'octicon octicon-file-directory'): octicon octicon-book | |||
App Color (default 'grey'): #589494 | |||
App License (default 'MIT'): GNU General Public License | |||
### Estructura de una aplicación | |||
La aplicación va a ser creada en el directorio llamado `library_management` y va a tener la siguiente estructura: | |||
. | |||
├── MANIFEST.in | |||
├── README.md | |||
├── library_management | |||
│ ├── __init__.py | |||
│ ├── config | |||
│ │ ├── __init__.py | |||
│ │ └── desktop.py | |||
│ ├── hooks.py | |||
│ ├── library_management | |||
│ │ └── __init__.py | |||
│ ├── modules.txt | |||
│ ├── patches.txt | |||
│ └── templates | |||
│ ├── __init__.py | |||
│ ├── generators | |||
│ │ └── __init__.py | |||
│ ├── pages | |||
│ │ └── __init__.py | |||
│ └── statics | |||
├── license.txt | |||
├── requirements.txt | |||
└── setup.py | |||
1. `config` contiene la información de configuración de la aplicación. | |||
1. `desktop.py` es donde los íconos del escritorio pueden ser agregados al mismo. | |||
1. `hooks.py` es donde se configuran las integraciones con el entorno y otras aplicaciones. | |||
1. `library_management` (dentro) es un **módulo** que está contenido. En Frappe, un **módulo** es donde los modelos y controladores se almacenan. | |||
1. `modules.txt` contiene la lista de **módulos** en la aplicación. Cuando creas un nuevo módulo, es obligatorio que lo agregues a este archivo. | |||
1. `patches.txt` es donde los patches para migraciones son establecidos. Son módulos de Python referenciados usando la nomenclatura de punto. | |||
1. `templates` es el directorio donde son mantenidos las plantillas de vistas web. Plantillas para **Login** y otras páginas estandar estan contenidas en Frappe. | |||
1. `generators` son donde las plantillas para los modelos son almacenadas, donde cada instancia de modelo tiene una ruta web separada, por ejemplo un **Blog Post** donde cada post tiene una url única. En Frappe, el manejador de plantillas utilizado es Jinja2. | |||
1. `pages` es donde las rutas simples son almacenadas. Por ejemplo para un tipo de página "/blog". | |||
{next} |
@@ -0,0 +1,7 @@ | |||
# Reportes | |||
Puedes dar click en el texto que dice Reportes en el panel lateral izquierdo para ver los registros de manera tabulada. | |||
<img class="screenshot" alt="Report" src="{{docs_base_url}}/assets/img/report.png"> | |||
{next} |
@@ -0,0 +1,14 @@ | |||
# Creando Roles | |||
Antes de crear los Modelos, debemos crear los Roles que van a establecer los permisos en el Modelo. En nuestro ejemplo vamos a tener dos Roles: | |||
1. Librarian | |||
1. Library Member | |||
Para crear un nuevo Role, ir a: | |||
> Setup > Users > Role > New | |||
<img class="screenshot" alt="Adding Roles" src="{{docs_base_url}}/assets/img/roles_creation.png"> | |||
{next} |
@@ -0,0 +1,68 @@ | |||
# Configurando el Site | |||
Vamos a crear un nuevo Site llamado `library`. | |||
*Nota: Antes de crear cualquier Site, necesitas hacer unos cambios en su instalación de MariaDB.* | |||
*Copia la siguiente configuración por defecto de ERPNext en su archivo `my.cnf`.* | |||
[mysqld] | |||
innodb-file-format=barracuda | |||
innodb-file-per-table=1 | |||
innodb-large-prefix=1 | |||
character-set-client-handshake = FALSE | |||
character-set-server = utf8mb4 | |||
collation-server = utf8mb4_unicode_ci | |||
[mysql] | |||
default-character-set = utf8mb4 | |||
Ahora puedes instalar un nuevo site, ejecutando el comando `bench new-site library`. | |||
La ejecución del comando anterior va a generar una nueva base de datos, un directorio en la carpeta sites y va a instalar `frappe` (el cual también es una aplicación!) en el nuevo site. | |||
La aplicación `frappe` tiene dos módulos integrados que son **Core** y **Website**. El módulo Core contiene los modelos básicos para la aplicación. Frappe es un Framework con muchas funcionalidades incluidas y viene con muchos modelos integrados. Estos modelos son llamados **DocTypes**. Vamos a ver más de esto en lo adelante. | |||
$ bench new-site library | |||
MySQL root password: | |||
Installing frappe... | |||
Updating frappe : [========================================] | |||
Updating country info : [========================================] | |||
Set Administrator password: | |||
Re-enter Administrator password: | |||
Installing fixtures... | |||
*** Scheduler is disabled *** | |||
### Estructura de un Site | |||
Un nuevo directorio ha sido creado dentro de la carpeta `sites` llamado `library`. La estructura siguiente es la que trae por defecto un site. | |||
. | |||
├── locks | |||
├── private | |||
│ └── backups | |||
├── public | |||
│ └── files | |||
└── site_config.json | |||
1. `public/files` es donde se almacenan los archivos subidos por los usuarios. | |||
1. `private/backups` es donde se almacenan los backups o copias de respaldo. | |||
1. `site_config.json` es donde todas las configuraciones a nivel de sites son almacenadas. | |||
### Configurando un Site por defecto | |||
En caso que tengas varios sites en tu Bench, debes usar `bench use [nombre_site]` para especificar el site por defecto. | |||
Ejemplo: | |||
$ bench use library | |||
### Instalar Aplicaciones | |||
Ahora vamos a instalar nuestra aplicación `library_management` en nuestro site `library`. | |||
1. Instalar la aplicación library_management en el site library se logra ejecutando el siguiente comando: `bench --site [nombre_site] install-app [nombre_app]` | |||
Ejemplo: | |||
$ bench --site library install-app library_management | |||
{next} |
@@ -0,0 +1,9 @@ | |||
# DocTypes Simples | |||
Una aplicación normalmente va a tener una página de configuración. En nuestra aplicación, podemos definir una página donde específiquemos el período de prestamos. Necesitaremos almacenar esta propiedad. En Frappe, esto puede lograrse usando los DocType de tipo **Simple**. Un DocType Simple es como el patrón Singleton en Java. Es un objeto con tan solo una instancia. Vamos a llamarlo **Library Managment Settings**. | |||
Para crear un Single DocType, marca el checkbox **Is Single**. | |||
<img class="screenshot" alt="Single Doctypes" src="{{docs_base_url}}/assets/img/tab_single.png"> | |||
{next} |
@@ -0,0 +1,31 @@ | |||
# Iniciando el Bench | |||
Ahora podemos acceder y verificar que todo esta funcionando de forma correcta. | |||
Para iniciar el servidor de desarrollo, ejecuta `bench start`. | |||
$ bench start | |||
13:58:51 web.1 | started with pid 22135 | |||
13:58:51 worker.1 | started with pid 22136 | |||
13:58:51 workerbeat.1 | started with pid 22137 | |||
13:58:52 web.1 | * Running on http://0.0.0.0:8000/ | |||
13:58:52 web.1 | * Restarting with reloader | |||
13:58:52 workerbeat.1 | [2014-09-17 13:58:52,343: INFO/MainProcess] beat: Starting... | |||
Ahora abre tu navegador y ve a la dirección `http://localhost:8000`. Deberías ver la páagina de inicio de sesión si todo salió bien.: | |||
<img class="screenshot" alt="Login Screen" src="{{docs_base_url}}/assets/img/login.png"> | |||
Ahora accede con : | |||
Login ID: **Administrator** | |||
Password : **Usa la contraseña que creaste durante la instalación** | |||
Cuando accedas, deberías poder ver la página de inicio (Desk). | |||
<img class="screenshot" alt="Desk" src="{{docs_base_url}}/assets/img/desk.png"> | |||
Como puedes ver, el sistema básico de Frappe viene con algunas aplicaciones preinstaladas como To Do, File Manager etc. Estas aplicaciones pueden integrarse en el flujo de trabajo de su aplicació a medida que avancemos. | |||
{next} |
@@ -0,0 +1,77 @@ | |||
# Tareas Programadas | |||
Finalmente, una aplicación también tiene que mandar notificaciones de email y hacer otros tipos de tareas programadas. En Frappe, si tienes el bench configurado, el programador de tareas es configurado vía Celery usando Redis Queue. | |||
Para agregar un nuevo manejador(Handler) de tareas, ir a `hooks.py` y agrega un nuevo manejador. Los manejadores (Handlers) por defecto son `all`, `daily`, `weekly`, `monthly`. El manejador `all` es llamado cada 3 minutos por defecto. | |||
# Tareas Programadas | |||
# --------------- | |||
scheduler_events = { | |||
"daily": [ | |||
"library_management.tasks.daily" | |||
], | |||
} | |||
Aquí hacemos referencia a una función Python que va a ser ejecutada diariamente. Vamos a ver como se ve esta función: | |||
# Copyright (c) 2013, Frappe | |||
# For license information, please see license.txt | |||
from __future__ import unicode_literals | |||
import frappe | |||
from frappe.utils import datediff, nowdate, format_date, add_days | |||
def daily(): | |||
loan_period = frappe.db.get_value("Library Management Settings", | |||
None, "loan_period") | |||
overdue = get_overdue(loan_period) | |||
for member, items in overdue.iteritems(): | |||
content = """<h2>Following Items are Overdue</h2> | |||
<p>Please return them as soon as possible</p><ol>""" | |||
for i in items: | |||
content += "<li>{0} ({1}) due on {2}</li>".format(i.article_name, | |||
i.article, | |||
format_date(add_days(i.transaction_date, loan_period))) | |||
content += "</ol>" | |||
recipient = frappe.db.get_value("Library Member", member, "email_id") | |||
frappe.sendmail(recipients=[recipient], | |||
sender="test@example.com", | |||
subject="Library Articles Overdue", content=content, bulk=True) | |||
def get_overdue(loan_period): | |||
# check for overdue articles | |||
today = nowdate() | |||
overdue_by_member = {} | |||
articles_transacted = [] | |||
for d in frappe.db.sql("""select name, article, article_name, | |||
library_member, member_name | |||
from `tabLibrary Transaction` | |||
order by transaction_date desc, modified desc""", as_dict=1): | |||
if d.article in articles_transacted: | |||
continue | |||
if d.transaction_type=="Issue" and \ | |||
datediff(today, d.transaction_date) > loan_period: | |||
overdue_by_member.setdefault(d.library_member, []) | |||
overdue_by_member[d.library_member].append(d) | |||
articles_transacted.append(d.article) | |||
Podemos pegar el código anterior en cualquier módulo de Python que sea accesible. La ruta es definida en `hooks.py`, por lo que para nuestro propósito vamos a poner el código en el archivo `library_management/tasks.py`. | |||
Nota: | |||
1. Obtenemos el período de prestamo desde **Library Management Settings** usando la función `frappe.db.get_value`. | |||
1. Ejecutamos una consulta en la base de datos usando la función `frappe.db.sql` | |||
1. Los Email son enviados usando `frappe.sendmail` | |||
{next} |
@@ -0,0 +1,55 @@ | |||
# Creando Usuarios y Registros | |||
Teniendo los modelos creados, podemos empezar a crear registros usando la interfaz gráfica de usuario de Frappe. No necesitas crear vistas! Las vistas en Frappe son automáticamente creadas basadas en las propiedades del DocType. | |||
### 4.1 Creando Usuarios | |||
Para crear registros, primero vamos a crear un Usuario. Para crear un usuario, ir a: | |||
> Setup > Users > User > New | |||
Crea un nuevo Usuario y llena los campos de nombre, primer nombre y nueva contraseña. | |||
Luego dale los Roles de Librarian y de Library Member a este usuario. | |||
<img class="screenshot" alt="Add User Roles" src="{{docs_base_url}}/assets/img/add_user_roles.png"> | |||
Ahora cierra sesión y accede usando las credenciales del nuevo usuario. | |||
### 4.2 Creando Registros | |||
Debes ver un ícono del módulo de Library Management. Dar click en el ícono para entrar a la página del módulo: | |||
<img class="screenshot" alt="Library Management Module" src="{{docs_base_url}}/assets/img/lib_management_module.png"> | |||
Aquí puedes ver los DocTypes que fueron creados para la aplicación. Vamos a comenzar a crear nuevos registros. | |||
Primero vamos a crear un nuevo Article: | |||
<img class="screenshot" alt="New Article" src="{{docs_base_url}}/assets/img/new_article_blank.png"> | |||
Aquí vas a ver que los DocTypes que haz creado han sido renderizados como un formulario. Las validaciones y las otras restricciones también están aplicadas según se diseñaron. Vamos a llenar los datos de un Article. | |||
<img class="screenshot" alt="New Article" src="{{docs_base_url}}/assets/img/new_article.png"> | |||
Puedes agregar una imagen si deseas. | |||
<img class="screenshot" alt="Attach Image" src="{{docs_base_url}}/assets/img/attach_image.gif"> | |||
Ahora vamos a crear un nuevo miembro: | |||
<img class="screenshot" alt="New Library Member" src="{{docs_base_url}}/assets/img/new_member.png"> | |||
Despues de esto, crearemos una nueva membresía (membership) para el miembro. | |||
Si recuerdas, aquí hemos específicado los valores del nombre y apellido del miembro directamente desde el registro del miembro tan pronto selecciones el miembro id, los nombres serán actualizados. | |||
<img class="screenshot" alt="New Library Membership" src="{{docs_base_url}}/assets/img/new_lib_membership.png"> | |||
Como puedes ver la fecha tiene un formato de año-mes-día lo cual es una fecha del sistema. Para seleccionar o cambiar la fecha, tiempo y formatos de números, ir a: | |||
> Setup > Settings > System Settings | |||
<img class="screenshot" alt="System Settings" src="{{docs_base_url}}/assets/img/system_settings.png"> | |||
{next} |
@@ -0,0 +1,64 @@ | |||
# Vistas Web (Web Views) | |||
Frappe tiene dos entornos principales, El escritorio y la Web. El escritorio es una interfaz de usuario controlada con una excelente aplicación AJAX y la web es mas plantillas de HTML tradicionales dispuestas para consumo público. Vistas Web pueden también ser generadas para crear vistas controladas para los usuarios que puedes acceder al sistema pero aún así no tener acceso al escritorio. | |||
En Frappe, Las vistas web son manejadas por plantillas que estan usualmente en el directorio `templates`. Hay dos tipos principales de plantillas. | |||
1. Pages: Estos son plantillas Jinja donde una vista existe solo para una ruta. ejemplo. `/blog`. | |||
2. Generators: Estas son plantiallas donde cada instancia de un DocType tiene una ruta diferente `/blog/a-blog`, `blog/b-blog` etc. | |||
3. Lists and Views: Estos son listas y vistan estandares con la ruta `[doctype]/[name]` y son renderizadas basándose en los permisos. | |||
### Vista Web Estandar | |||
> Esta funcionalidad sigue bajo desarrollo. | |||
Vamos a ver las Vistas web estandar: | |||
Si estas logueado como el usuario de prueba, ve a `/article` y deberías ver la lista de artículos. | |||
<img class="screenshot" alt="web list" src="{{docs_base_url}}/assets/img/web-list.png"> | |||
Da click en uno de los artículos y vas a ver una vista web por defecto | |||
<img class="screenshot" alt="web view" src="{{docs_base_url}}/assets/img/web-view.png"> | |||
Si deseas hacer una mejor vista para la lista de artículos, crea un archivo llamado `row_template.html` en el directorio `library_management/templates/includes/list/`. | |||
Aquí hay un archivo de ejemplo: | |||
{% raw %}<div class="row"> | |||
<div class="col-sm-4"> | |||
<a href="/Article/{{ doc.name }}"> | |||
<img src="{{ doc.image }}" | |||
class="img-responsive" style="max-height: 200px"> | |||
</a> | |||
</div> | |||
<div class="col-sm-4"> | |||
<a href="/Article/{{ doc.name }}"><h4>{{ doc.article_name }}</h4></a> | |||
<p>{{ doc.author }}</p> | |||
<p>{{ (doc.description[:200] + "...") | |||
if doc.description|len > 200 else doc.description }}</p> | |||
<p class="text-muted">Publisher: {{ doc.publisher }}</p> | |||
</div> | |||
</div>{% endraw %} | |||
Aquí, vas a tener todas las propiedades de un artículo en el objeto `doc`. | |||
La lista actualizada debe lucir de esta manera! | |||
<img class="screenshot" alt="new web list" src="{{docs_base_url}}/assets/img/web-list-new.png"> | |||
#### Página de Inicio | |||
Frappe también tiene vistas para el registro de usuarios que incluye opciones de registro usando Google, Facebook y GitHub. Cuando un usuario se registra vía la web, no tiene acceso a la interfaz del Escritorio por defecto. | |||
> Para permitirles a los usuarios acceso al Escritorio, debes especificar que el usuario es de tipo "System User" en Setup > User | |||
Para usuario que no son de tipo System User, podemos especificar una página de inicio por defecto a traves de `hooks.py` basándonos en Role. | |||
Cuando miembros acceden al sistema, deben ser redireccionados a la página `article`, para configurar esto modifica el archivo `library_management/hooks.py` y agrega lo siguiente: | |||
role_home_page = { | |||
"Library Member": "article" | |||
} | |||
{next} |
@@ -0,0 +1,9 @@ | |||
# Videos Tutoriales acerca del Framework Frappe | |||
Este video tutorial de 10 videos va a enseñarte como crear aplicaciones complejas en Frappe. | |||
Prerrequisitos: <a href="{{ docs_base_url }}/user/es/tutorial/before.html" target="_blank">Debes tener conocimientos básicos de Python, Javascript y MySQl antes de empezar este tutorial.</a> | |||
--- | |||
<iframe width="670" height="376" src="https://www.youtube.com/embed/videoseries?list=PL3lFfCEoMxvzHtsZHFJ4T3n5yMM3nGJ1W" frameborder="0" allowfullscreen></iframe> |
@@ -5,3 +5,4 @@ Select your language | |||
1. [English]({{docs_base_url}}/user/en) | |||
1. [Français]({{docs_base_url}}/user/fr) | |||
1. [Português]({{docs_base_url}}/user/pt) | |||
1. [Español]({{docs_base_url}}/user/es) |
@@ -1,3 +1,4 @@ | |||
en | |||
fr | |||
pt | |||
es |
@@ -8,6 +8,7 @@ from frappe import _ | |||
from frappe.model.document import Document | |||
from datetime import timedelta | |||
import frappe.utils | |||
from frappe.utils import now, global_date_format, format_time | |||
from frappe.utils.xlsxutils import make_xlsx | |||
from frappe.utils.csvutils import to_csv | |||
@@ -76,16 +77,28 @@ class AutoEmailReport(Document): | |||
return xlsx_file.getvalue() | |||
elif self.format == 'CSV': | |||
spreadsheet_data = self.get_spreadsheet_data(columns, data) | |||
spreadsheet_data = self.get_spreadsheet_data(columns, data) | |||
return to_csv(spreadsheet_data) | |||
else: | |||
frappe.throw(_('Invalid Output Format')) | |||
def get_html_table(self, columns, data): | |||
return frappe.render_template('frappe/templates/includes/print_table.html', { | |||
def get_html_table(self, columns=None, data=None): | |||
date_time = global_date_format(now()) + ' ' + format_time(now()) | |||
report_doctype = frappe.db.get_value('Report', self.report, 'ref_doctype') | |||
return frappe.render_template('frappe/templates/emails/auto_email_report.html', { | |||
'title': self.name, | |||
'description': self.description, | |||
'date_time': date_time, | |||
'columns': columns, | |||
'data': data | |||
'data': data, | |||
'report_url': frappe.utils.get_url_to_report(self.report, | |||
self.report_type, report_doctype), | |||
'report_name': self.report, | |||
'edit_report_settings': frappe.utils.get_link_to_form('Auto Email Report', | |||
self.name) | |||
}) | |||
@staticmethod | |||
@@ -111,29 +124,17 @@ class AutoEmailReport(Document): | |||
return | |||
attachments = None | |||
message = '<p>{0}</p>'.format(_('{0} generated on {1}')\ | |||
.format(frappe.bold(self.name), | |||
frappe.utils.format_datetime(frappe.utils.now_datetime()))) | |||
if self.description: | |||
message += '<hr style="margin: 15px 0px;">' + self.description | |||
if self.format=='HTML': | |||
message += '<hr>' + data | |||
if self.format == "HTML": | |||
message = data | |||
else: | |||
message = self.get_html_table() | |||
if not self.format=='HTML': | |||
attachments = [{ | |||
'fname': self.get_file_name(), | |||
'fcontent': data | |||
}] | |||
report_doctype = frappe.db.get_value('Report', self.report, 'ref_doctype') | |||
report_footer = frappe.render_template(self.get_report_footer(), | |||
dict(report_url = frappe.utils.get_url_to_report(self.report, self.report_type, report_doctype), | |||
report_name = self.report, | |||
edit_report_settings = frappe.utils.get_link_to_form('Auto Email Report', self.name))) | |||
message += report_footer | |||
frappe.sendmail( | |||
recipients = self.email_to.split(), | |||
subject = self.name, | |||
@@ -141,14 +142,6 @@ class AutoEmailReport(Document): | |||
attachments = attachments | |||
) | |||
def get_report_footer(self): | |||
return """<hr style="margin: 30px 0px 15px 0px;"> | |||
<p style="font-size: 9px;"> | |||
View report in your browser: | |||
<a href= {{report_url}} target="_blank">{{report_name}}</a><br><br> | |||
Edit Auto Email Report Settings: {{edit_report_settings}} | |||
</p>""" | |||
@frappe.whitelist() | |||
def download(name): | |||
'''Download report locally''' | |||
@@ -69,15 +69,15 @@ def add_subscribers(name, email_list): | |||
count = 0 | |||
for email in email_list: | |||
email = email.strip() | |||
valid = validate_email_add(email, False) | |||
parsed_email = validate_email_add(email, False) | |||
if valid: | |||
if parsed_email: | |||
if not frappe.db.get_value("Email Group Member", | |||
{"email_group": name, "email": email}): | |||
{"email_group": name, "email": parsed_email}): | |||
frappe.get_doc({ | |||
"doctype": "Email Group Member", | |||
"email_group": name, | |||
"email": email | |||
"email": parsed_email | |||
}).insert(ignore_permissions = frappe.flags.ignore_permissions) | |||
count += 1 | |||
@@ -15,7 +15,7 @@ from email.mime.multipart import MIMEMultipart | |||
def get_email(recipients, sender='', msg='', subject='[No Subject]', | |||
text_content = None, footer=None, print_html=None, formatted=None, attachments=None, | |||
content=None, reply_to=None, cc=[], email_account=None, expose_recipients=None, | |||
inline_images=[], header=False): | |||
inline_images=[], header=None): | |||
""" Prepare an email with the following format: | |||
- multipart/mixed | |||
- multipart/alternative | |||
@@ -76,7 +76,7 @@ class EMail: | |||
self.email_account = email_account or get_outgoing_email_account() | |||
def set_html(self, message, text_content = None, footer=None, print_html=None, | |||
formatted=None, inline_images=None, header=False): | |||
formatted=None, inline_images=None, header=None): | |||
"""Attach message in the html portion of multipart/alternative""" | |||
if not formatted: | |||
formatted = get_formatted_html(self.subject, message, footer, print_html, | |||
@@ -233,12 +233,12 @@ class EMail: | |||
self.make() | |||
return self.msg_root.as_string() | |||
def get_formatted_html(subject, message, footer=None, print_html=None, email_account=None, header=False): | |||
def get_formatted_html(subject, message, footer=None, print_html=None, email_account=None, header=None): | |||
if not email_account: | |||
email_account = get_outgoing_email_account(False) | |||
rendered_email = frappe.get_template("templates/emails/standard.html").render({ | |||
"header": get_header() if header else None, | |||
"header": get_header(header), | |||
"content": message, | |||
"signature": get_signature(email_account), | |||
"footer": get_footer(email_account, footer), | |||
@@ -247,7 +247,27 @@ def get_formatted_html(subject, message, footer=None, print_html=None, email_acc | |||
"subject": subject | |||
}) | |||
return scrub_urls(rendered_email) | |||
sanitized_html = scrub_urls(rendered_email) | |||
transformed_html = inline_style_in_html(sanitized_html) | |||
return transformed_html | |||
def inline_style_in_html(html): | |||
''' Convert email.css and html to inline-styled html | |||
''' | |||
from premailer import Premailer | |||
apps = frappe.get_installed_apps() | |||
css_files = [] | |||
for app in apps: | |||
path = 'assets/{0}/css/email.css'.format(app) | |||
if os.path.exists(os.path.abspath(path)): | |||
css_files.append(path) | |||
p = Premailer(html=html, external_styles=css_files, strip_important=False) | |||
return p.transform() | |||
def add_attachment(fname, fcontent, content_type=None, | |||
parent=None, content_id=None, inline=False): | |||
@@ -396,23 +416,27 @@ def get_filecontent_from_path(path): | |||
return None | |||
def get_header(): | |||
def get_header(header=None): | |||
""" Build header from template """ | |||
from frappe.utils.jinja import get_email_from_template | |||
default_brand_image = 'assets/frappe/images/favicon.png' # svg doesn't work in email | |||
email_brand_image = frappe.get_hooks('email_brand_image') | |||
if len(email_brand_image): | |||
email_brand_image = email_brand_image[-1] | |||
else: | |||
email_brand_image = default_brand_image | |||
if not header: return None | |||
if isinstance(header, basestring): | |||
# header = 'My Title' | |||
header = [header, None] | |||
if len(header) == 1: | |||
# header = ['My Title'] | |||
header.append(None) | |||
# header = ['My Title', 'orange'] | |||
title, indicator = header | |||
email_brand_image = default_brand_image | |||
brand_text = frappe.get_hooks('app_title')[-1] | |||
if not title: | |||
title = frappe.get_hooks('app_title')[-1] | |||
email_header, text = get_email_from_template('email_header', { | |||
'brand_image': email_brand_image, | |||
'brand_text': brand_text | |||
'header_title': title, | |||
'indicator': indicator | |||
}) | |||
return email_header |
@@ -23,7 +23,7 @@ def send(recipients=None, sender=None, subject=None, message=None, text_content= | |||
attachments=None, reply_to=None, cc=[], message_id=None, in_reply_to=None, send_after=None, | |||
expose_recipients=None, send_priority=1, communication=None, now=False, read_receipt=None, | |||
queue_separately=False, is_notification=False, add_unsubscribe_link=1, inline_images=None, | |||
header=False): | |||
header=None): | |||
"""Add email to sending queue (Email Queue) | |||
:param recipients: List of recipients. | |||
@@ -495,6 +495,7 @@ def prepare_message(email, recipient, recipients_list): | |||
'fcontent': fcontent, | |||
'parent': msg_obj | |||
}) | |||
attachment.pop("fid", None) | |||
add_attachment(**attachment) | |||
return msg_obj.as_string() | |||
@@ -3,7 +3,8 @@ | |||
from __future__ import unicode_literals | |||
import frappe, unittest, os, base64 | |||
from frappe.email.email_body import replace_filename_with_cid, get_email | |||
from frappe.email.email_body import (replace_filename_with_cid, | |||
get_email, inline_style_in_html, get_header) | |||
class TestEmailBody(unittest.TestCase): | |||
def setUp(self): | |||
@@ -95,6 +96,46 @@ w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> | |||
'''.format(inline_images[0].get('content_id')) | |||
self.assertEquals(message, processed_message) | |||
def test_inline_styling(self): | |||
html = ''' | |||
<h3>Hi John</h3> | |||
<p>This is a test email</p> | |||
''' | |||
transformed_html = ''' | |||
<h3>Hi John</h3> | |||
<p style="margin:1em 0 !important">This is a test email</p> | |||
''' | |||
self.assertTrue(transformed_html in inline_style_in_html(html)) | |||
def test_email_header(self): | |||
email_html = ''' | |||
<h3>Hey John Doe!</h3> | |||
<p>This is embedded image you asked for</p> | |||
''' | |||
email_string = get_email( | |||
recipients=['test@example.com'], | |||
sender='me@example.com', | |||
subject='Test Subject', | |||
content=email_html, | |||
header=['Email Title', 'green'] | |||
).as_string() | |||
self.assertTrue('''<span class=3D"indicator indicator-green" style=3D"background-color:#98= | |||
d85b; border-radius:8px; display:inline-block; height:8px; margin-right:5px= | |||
; width:8px" bgcolor=3D"#98d85b" height=3D"8" width=3D"8"></span>''' in email_string) | |||
self.assertTrue('<span>Email Title</span>' in email_string) | |||
def test_get_email_header(self): | |||
html = get_header(['This is test', 'orange']) | |||
self.assertTrue('<span class="indicator indicator-orange"></span>' in html) | |||
self.assertTrue('<span>This is test</span>' in html) | |||
html = get_header(['This is another test']) | |||
self.assertTrue('<span>This is another test</span>' in html) | |||
html = get_header('This is string') | |||
self.assertTrue('<span>This is string</span>' in html) | |||
def fixed_column_width(string, chunk_size): | |||
parts = [string[0+i:chunk_size+i] for i in range(0, len(string), chunk_size)] |
@@ -125,7 +125,7 @@ def install_app(name, verbose=False, set_as_patched=True): | |||
frappe.msgprint(_("App {0} already installed").format(name)) | |||
return | |||
print("Installing {0}...".format(name)) | |||
print("\nInstalling {0}...".format(name)) | |||
if name != "frappe": | |||
frappe.only_for("System Manager") | |||
@@ -1,5 +1,6 @@ | |||
from __future__ import print_function | |||
import frappe, urllib | |||
import pytz | |||
from frappe import _ | |||
from urlparse import parse_qs, urlparse | |||
@@ -227,8 +228,10 @@ class OAuthWebRequestValidator(RequestValidator): | |||
def validate_bearer_token(self, token, scopes, request): | |||
# Remember to check expiration and scope membership | |||
otoken = frappe.get_doc("OAuth Bearer Token", token) #{"access_token": str(token)}) | |||
is_token_valid = (frappe.utils.datetime.datetime.now() < otoken.expiration_time) \ | |||
otoken = frappe.get_doc("OAuth Bearer Token", token) | |||
token_expiration_local = otoken.expiration_time.replace(tzinfo=pytz.timezone(frappe.utils.get_time_zone())) | |||
token_expiration_utc = token_expiration_local.astimezone(pytz.utc) | |||
is_token_valid = (frappe.utils.datetime.datetime.utcnow().replace(tzinfo=pytz.utc) < token_expiration_utc) \ | |||
and otoken.status != "Revoked" | |||
client_scopes = frappe.db.get_value("OAuth Client", otoken.client, 'scopes').split(get_url_delimiter()) | |||
are_scopes_valid = True | |||
@@ -187,4 +187,5 @@ frappe.patches.v8_0.update_gender_and_salutation | |||
execute:frappe.db.sql('update tabReport set module="Desk" where name="ToDo"') | |||
frappe.patches.v8_1.enable_allow_error_traceback_in_system_settings | |||
frappe.patches.v8_1.update_format_options_in_auto_email_report | |||
frappe.patches.v8_1.delete_custom_docperm_if_doctype_not_exists | |||
frappe.patches.v8_1.delete_custom_docperm_if_doctype_not_exists | |||
frappe.patches.v8_5.delete_email_group_member_with_invalid_emails |
@@ -0,0 +1,20 @@ | |||
# Copyright (c) 2017, Frappe and Contributors | |||
# License: GNU General Public License v3. See license.txt | |||
from __future__ import unicode_literals | |||
import frappe | |||
from frappe.utils import validate_email_add | |||
def execute(): | |||
''' update/delete the email group member with the wrong email ''' | |||
email_group_members = frappe.get_all("Email Group Member", fields=["name", "email"]) | |||
for member in email_group_members: | |||
validated_email = validate_email_add(member.email) | |||
if (validated_email==member.email): | |||
pass | |||
else: | |||
try: | |||
frappe.db.set_value("Email Group Member", member.name, "email", validated_email) | |||
except Exception: | |||
frappe.delete_doc(doctype="Email Group Member", name=member.name, force=1, ignore_permissions=True) |
@@ -125,6 +125,7 @@ | |||
"public/js/frappe/misc/common.js", | |||
"public/js/frappe/misc/pretty_date.js", | |||
"public/js/frappe/misc/utils.js", | |||
"public/js/frappe/misc/test_utils.js", | |||
"public/js/frappe/misc/tools.js", | |||
"public/js/frappe/misc/datetime.js", | |||
"public/js/frappe/misc/number_format.js", | |||
@@ -161,6 +162,7 @@ | |||
"public/js/frappe/query_string.js", | |||
"public/js/frappe/ui/charts.js", | |||
"public/js/frappe/ui/graph.js", | |||
"public/js/frappe/misc/rating_icons.html", | |||
"public/js/frappe/feedback.js" | |||
@@ -89,6 +89,7 @@ kbd { | |||
} | |||
.dropdown-menu > li > a { | |||
padding: 14px; | |||
white-space: normal; | |||
} | |||
.dropdown-menu { | |||
min-width: 200px; | |||
@@ -89,6 +89,7 @@ kbd { | |||
} | |||
.dropdown-menu > li > a { | |||
padding: 14px; | |||
white-space: normal; | |||
} | |||
.dropdown-menu { | |||
min-width: 200px; | |||
@@ -508,6 +509,17 @@ fieldset[disabled] .form-control { | |||
cursor: pointer; | |||
margin-right: 10px; | |||
} | |||
a.progress-small .progress-chart { | |||
width: 60px; | |||
margin-top: 4px; | |||
float: right; | |||
} | |||
a.progress-small .progress { | |||
margin-bottom: 0; | |||
} | |||
a.progress-small .progress-bar { | |||
background-color: #98d85b; | |||
} | |||
/* on small screens, show only icons on top */ | |||
@media (max-width: 767px) { | |||
.module-view-layout .nav-stacked > li { | |||
@@ -0,0 +1,151 @@ | |||
/* csslint ignore:start */ | |||
body { | |||
line-height: 1.5; | |||
color: #36414C; | |||
} | |||
p { | |||
margin: 1em 0 !important; | |||
} | |||
hr { | |||
border-top: 1px solid #d1d8dd; | |||
} | |||
.body-table { | |||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; | |||
} | |||
.body-table td { | |||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; | |||
} | |||
.email-header, | |||
.email-body, | |||
.email-footer { | |||
width: 100% !important; | |||
min-width: 100% !important; | |||
} | |||
.email-body { | |||
font-size: 14px; | |||
} | |||
.email-footer { | |||
border-top: 1px solid #d1d8dd; | |||
font-size: 12px; | |||
} | |||
.email-header { | |||
border: 1px solid #d1d8dd; | |||
border-radius: 4px 4px 0 0; | |||
} | |||
.email-header .brand-image { | |||
width: 24px; | |||
height: 24px; | |||
display: block; | |||
} | |||
.email-header-title { | |||
font-weight: bold; | |||
} | |||
.body-table.has-header .email-body { | |||
border: 1px solid #d1d8dd; | |||
border-radius: 0 0 4px 4px; | |||
border-top: none; | |||
} | |||
.body-table.has-header .email-footer { | |||
border-top: none; | |||
} | |||
.btn { | |||
text-decoration: none; | |||
padding: 7px 10px; | |||
font-size: 12px; | |||
border: 1px solid; | |||
border-radius: 3px; | |||
} | |||
.btn.btn-default { | |||
color: #fff; | |||
background-color: #f0f4f7; | |||
border-color: transparent; | |||
} | |||
.btn.btn-primary { | |||
color: #fff; | |||
background-color: #5E64FF; | |||
border-color: #444bff; | |||
} | |||
.table { | |||
width: 100%; | |||
border-collapse: collapse; | |||
} | |||
.table td, | |||
.table th { | |||
padding: 8px; | |||
line-height: 1.42857143; | |||
vertical-align: top; | |||
border-top: 1px solid #d1d8dd; | |||
text-align: left; | |||
} | |||
.table th { | |||
font-weight: bold; | |||
} | |||
.table > thead > tr > th { | |||
vertical-align: middle; | |||
border-bottom: 2px solid #d1d8dd; | |||
} | |||
.table > thead:first-child > tr:first-child > th { | |||
border-top: none; | |||
} | |||
.table.table-bordered { | |||
border: 1px solid #d1d8dd; | |||
} | |||
.table.table-bordered td, | |||
.table.table-bordered th { | |||
border: 1px solid #d1d8dd; | |||
} | |||
.more-info { | |||
font-size: 80% !important; | |||
color: #8D99A6 !important; | |||
border-top: 1px solid #EBEFF2; | |||
padding-top: 10px; | |||
} | |||
.text-right { | |||
text-align: right !important; | |||
} | |||
.text-center { | |||
text-align: center !important; | |||
} | |||
.text-muted { | |||
color: #8D99A6 !important; | |||
} | |||
.text-extra-muted { | |||
color: #d1d8dd !important; | |||
} | |||
.text-regular { | |||
font-size: 14px; | |||
} | |||
.text-medium { | |||
font-size: 12px; | |||
} | |||
.text-small { | |||
font-size: 10px; | |||
} | |||
.indicator { | |||
width: 8px; | |||
height: 8px; | |||
border-radius: 8px; | |||
background-color: #b8c2cc; | |||
display: inline-block; | |||
margin-right: 5px; | |||
} | |||
.indicator.indicator-blue { | |||
background-color: #5e64ff; | |||
} | |||
.indicator.indicator-green { | |||
background-color: #98d85b; | |||
} | |||
.indicator.indicator-orange { | |||
background-color: #ffa00a; | |||
} | |||
.indicator.indicator-red { | |||
background-color: #ff5858; | |||
} | |||
.indicator.indicator-yellow { | |||
background-color: #FEEF72; | |||
} | |||
/* auto email report */ | |||
.report-title { | |||
margin-bottom: 20px; | |||
} | |||
/* csslint ignore:end */ |
@@ -642,6 +642,92 @@ select.form-control { | |||
box-shadow: none; | |||
} | |||
} | |||
/* goals */ | |||
.goals-page-container { | |||
background-color: #fafbfc; | |||
padding-top: 1px; | |||
} | |||
.goals-page-container .goal-container { | |||
background-color: #fff; | |||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); | |||
border-radius: 2px; | |||
padding: 10px; | |||
margin: 10px; | |||
} | |||
.graph-container .graphics { | |||
margin-top: 10px; | |||
padding: 10px 0px; | |||
} | |||
.graph-container .stats-group { | |||
display: flex; | |||
justify-content: space-around; | |||
flex: 1; | |||
} | |||
.graph-container .stats-container { | |||
display: flex; | |||
justify-content: space-around; | |||
} | |||
.graph-container .stats-container .stats { | |||
padding-bottom: 15px; | |||
} | |||
.graph-container .stats-container .stats-title { | |||
color: #8D99A6; | |||
} | |||
.graph-container .stats-container .stats-value { | |||
font-size: 20px; | |||
font-weight: 300; | |||
} | |||
.graph-container .stats-container .stats-description { | |||
font-size: 12px; | |||
color: #8D99A6; | |||
} | |||
.graph-container .stats-container .graph-data .stats-value { | |||
color: #98d85b; | |||
} | |||
.bar-graph .axis, | |||
.line-graph .axis { | |||
font-size: 10px; | |||
fill: #6a737d; | |||
} | |||
.bar-graph .axis line, | |||
.line-graph .axis line { | |||
stroke: rgba(27, 31, 35, 0.1); | |||
} | |||
.data-points circle { | |||
fill: #28a745; | |||
stroke: #fff; | |||
stroke-width: 2; | |||
} | |||
.data-points g.mini { | |||
fill: #98d85b; | |||
} | |||
.data-points path { | |||
fill: none; | |||
stroke: #28a745; | |||
stroke-opacity: 1; | |||
stroke-width: 2px; | |||
} | |||
.line-graph .path { | |||
fill: none; | |||
stroke: #28a745; | |||
stroke-opacity: 1; | |||
stroke-width: 2px; | |||
} | |||
line.dashed { | |||
stroke-dasharray: 5,3; | |||
} | |||
.tick.x-axis-label { | |||
display: block; | |||
} | |||
.tick .specific-value { | |||
text-anchor: start; | |||
} | |||
.tick .y-value-text { | |||
text-anchor: end; | |||
} | |||
.tick .x-value-text { | |||
text-anchor: middle; | |||
} | |||
body[data-route^="Form/Communication"] textarea[data-fieldname="subject"] { | |||
height: 80px !important; | |||
} |
@@ -232,6 +232,7 @@ | |||
padding: 2px 4px; | |||
font-weight: normal; | |||
background-color: #F0F4F7; | |||
white-space: normal; | |||
} | |||
.taggle_list .taggle:hover { | |||
padding: 2px 15px 2px 4px; | |||
@@ -448,6 +449,7 @@ | |||
.list-item__content--activity { | |||
justify-content: flex-end; | |||
margin-right: 5px; | |||
min-width: 110px; | |||
} | |||
.list-item__content--activity .list-row-modified, | |||
.list-item__content--activity .avatar-small { | |||
@@ -54,3 +54,6 @@ | |||
.slick-row.odd .slick-cell { | |||
background-color: #fafbfc; | |||
} | |||
.frappe-rtl .slick-wrapper { | |||
direction: ltr; | |||
} |
@@ -89,6 +89,7 @@ kbd { | |||
} | |||
.dropdown-menu > li > a { | |||
padding: 14px; | |||
white-space: normal; | |||
} | |||
.dropdown-menu { | |||
min-width: 200px; | |||
@@ -219,13 +219,6 @@ frappe.get_modal = function(title, content) { | |||
return $(frappe.render_template("modal", {title:title, content:content})).appendTo(document.body); | |||
}; | |||
frappe._in = function(source, target) { | |||
// returns true if source is in target and both are not empty / falsy | |||
if(!source) return false; | |||
if(!target) return false; | |||
return (target.indexOf(source) !== -1); | |||
}; | |||
// add <option> list to <select> | |||
(function($) { | |||
$.fn.add_options = function(options_list) { | |||
@@ -306,4 +299,4 @@ frappe._in = function(source, target) { | |||
} | |||
return this; | |||
} | |||
})(jQuery); | |||
})(jQuery); |