Merge pull request #1 from frappe/develop

Merge from frappe/frappe:develop
This commit is contained in:
Maxwell 2015-07-24 16:32:36 -03:00
commit adcd29cd4f
260 changed files with 5613 additions and 12992 deletions

View file

@ -6,10 +6,15 @@ python:
services:
- mysql
before_install:
- "export DISPLAY=:99.0"
- "sh -e /etc/init.d/xvfb start"
install:
- sudo apt-get purge -y mysql-common
- wget https://raw.githubusercontent.com/frappe/bench/master/install_scripts/setup_frappe.sh
- sudo bash setup_frappe.sh --skip-setup-bench --mysql-root-password travis
- sudo pip install --upgrade pip
- sudo service redis-server start
- rm $TRAVIS_BUILD_DIR/.git/shallow
- cd ~/ && bench init frappe-bench --frappe-path $TRAVIS_BUILD_DIR
@ -23,7 +28,7 @@ script:
- bench build-website
- bench serve &
- sleep 10
- bench --verbose run-tests
- bench --verbose run-tests --driver Firefox
before_script:
- mysql -e 'create database test_frappe'

View file

@ -116,6 +116,7 @@ def init(site, sites_path=None):
local.module_app = None
local.app_modules = None
local.system_settings = None
local.user = None
local.user_obj = None
@ -425,11 +426,19 @@ def has_website_permission(doctype, ptype="read", doc=None, user=None, verbose=F
if not user:
user = session.user
for method in (get_hooks("has_website_permission") or {}).get(doctype, []):
if not call(get_attr(method), doc=doc, ptype=ptype, user=user, verbose=verbose):
return False
hooks = (get_hooks("has_website_permission") or {}).get(doctype, [])
if hooks:
for method in hooks:
result = call(get_attr(method), doc=doc, ptype=ptype, user=user, verbose=verbose)
# if even a single permission check is Falsy
if not result:
return False
return True
# else it is Truthy
return True
else:
return False
def is_table(doctype):
"""Returns True if `istable` property (indicating child Table) is set for given DocType."""
@ -529,9 +538,9 @@ def delete_doc_if_exists(doctype, name):
if db.exists(doctype, name):
delete_doc(doctype, name)
def reload_doctype(doctype):
def reload_doctype(doctype, force=False):
"""Reload DocType from model (`[module]/[doctype]/[name]/[name].json`) files."""
reload_doc(scrub(db.get_value("DocType", doctype, "module")), "doctype", scrub(doctype))
reload_doc(scrub(db.get_value("DocType", doctype, "module")), "doctype", scrub(doctype), force=force)
def reload_doc(module, dt=None, dn=None, force=False):
"""Reload Document from model (`[module]/[doctype]/[name]/[name].json`) files.

View file

@ -1,2 +1,2 @@
from __future__ import unicode_literals
__version__ = "5.0.9"
__version__ = "5.1.3"

View file

@ -70,6 +70,7 @@ def application(request):
except Exception, e:
http_status_code = getattr(e, "http_status_code", 500)
#print frappe.get_traceback()
if (http_status_code==500
and isinstance(e, MySQLdb.OperationalError)

View file

@ -57,14 +57,6 @@ class HTTPRequest:
# run login triggers
if frappe.form_dict.get('cmd')=='login':
frappe.local.login_manager.run_trigger('on_session_creation')
self.clear_active_sessions()
def clear_active_sessions(self):
if not frappe.conf.get("deny_multiple_sessions"):
return
if frappe.session.user != "Guest":
clear_sessions(frappe.session.user, keep_current=True)
def set_lang(self, lang_codes):
@ -90,7 +82,13 @@ class LoginManager:
if frappe.local.form_dict.get('cmd')=='login' or frappe.local.request.path=="/api/method/login":
self.login()
else:
self.make_session(resume=True)
try:
self.make_session(resume=True)
self.set_user_info(resume=True)
except AttributeError:
self.user = "Guest"
self.make_session()
self.set_user_info()
def login(self):
# clear cache
@ -99,29 +97,34 @@ class LoginManager:
self.post_login()
def post_login(self):
self.info = frappe.db.get_value("User", self.user,
["user_type", "first_name", "last_name", "user_image"], as_dict=1)
self.full_name = " ".join(filter(None, [self.info.first_name, self.info.last_name]))
self.user_type = self.info.user_type
self.run_trigger('on_login')
self.validate_ip_address()
self.validate_hour()
self.make_session()
self.set_user_info()
def set_user_info(self):
def set_user_info(self, resume=False):
# set sid again
frappe.local.cookie_manager.init_cookies()
self.info = frappe.db.get_value("User", self.user,
["user_type", "first_name", "last_name", "user_image"], as_dict=1)
self.full_name = " ".join(filter(None, [self.info.first_name,
self.info.last_name]))
self.user_type = self.info.user_type
if self.info.user_type=="Website User":
frappe.local.cookie_manager.set_cookie("system_user", "no")
frappe.local.response["message"] = "No App"
if not resume:
frappe.local.response["message"] = "No App"
else:
frappe.local.cookie_manager.set_cookie("system_user", "yes")
frappe.local.response['message'] = 'Logged In'
if not resume:
frappe.local.response['message'] = 'Logged In'
if not resume:
frappe.response["full_name"] = self.full_name
frappe.response["full_name"] = self.full_name
frappe.local.cookie_manager.set_cookie("full_name", self.full_name)
frappe.local.cookie_manager.set_cookie("user_id", self.user)
frappe.local.cookie_manager.set_cookie("user_image", self.info.user_image or "")
@ -134,6 +137,14 @@ class LoginManager:
# reset user if changed to Guest
self.user = frappe.local.session_obj.user
frappe.local.session = frappe.local.session_obj.data
self.clear_active_sessions()
def clear_active_sessions(self):
if not frappe.conf.get("deny_multiple_sessions"):
return
if frappe.session.user != "Guest":
clear_sessions(frappe.session.user, keep_current=True)
def authenticate(self, user=None, pwd=None):
if not (user and pwd):
@ -192,7 +203,7 @@ class LoginManager:
return
from frappe.utils import now_datetime
current_hour = int(now_datetime(user=frappe.form_dict.get('usr')).strftime('%H'))
current_hour = int(now_datetime().strftime('%H'))
if login_before and current_hour > login_before:
frappe.throw(_("Login not allowed at this time"), frappe.AuthenticationError)

View file

@ -70,6 +70,7 @@ def get_bootinfo():
bootinfo.error_report_email = frappe.get_hooks("error_report_email")
bootinfo.default_background_image = get_url("/assets/frappe/images/ui/into-the-dawn.jpg")
bootinfo.calendars = sorted(frappe.get_hooks("calendars"))
return bootinfo
@ -104,9 +105,16 @@ def get_allowed_pages():
def load_translations(bootinfo):
if frappe.local.lang != 'en':
bootinfo["__messages"] = frappe.get_lang_dict("boot")
messages = frappe.get_lang_dict("boot")
bootinfo["lang"] = frappe.lang
# load translated report names
for name in bootinfo.user.all_reports:
messages[name] = frappe._(name)
bootinfo["__messages"] = messages
def get_fullnames():
"""map of user fullnames"""
ret = frappe.db.sql("""select name,
@ -142,14 +150,10 @@ def add_home_page(bootinfo, docs):
docs.append(page)
def add_timezone_info(bootinfo):
user = bootinfo.user.get("time_zone")
system = bootinfo.sysdefaults.get("time_zone")
if user and user != system:
import frappe.utils.momentjs
bootinfo.timezone_info = {"zones":{}, "rules":{}, "links":{}}
frappe.utils.momentjs.update(user, bootinfo.timezone_info)
frappe.utils.momentjs.update(system, bootinfo.timezone_info)
import frappe.utils.momentjs
bootinfo.timezone_info = {"zones":{}, "rules":{}, "links":{}}
frappe.utils.momentjs.update(system, bootinfo.timezone_info)
def load_print(bootinfo, doclist):
print_settings = frappe.db.get_singles_dict("Print Settings")
@ -158,4 +162,4 @@ def load_print(bootinfo, doclist):
load_print_css(bootinfo, print_settings)
def load_print_css(bootinfo, print_settings):
bootinfo.print_css = frappe.get_attr("frappe.templates.pages.print.get_print_style")(print_settings.print_style or "Modern")
bootinfo.print_css = frappe.get_attr("frappe.templates.pages.print.get_print_style")(print_settings.print_style or "Modern", for_legacy=True)

View file

@ -153,13 +153,13 @@ def pack(target, sources, no_compress, verbose):
def html_to_js_template(path, content):
# remove whitespace to a single space
content = re.sub("\s+", " ", content).replace("'", "\'")
content = re.sub("\s+", " ", content)
# strip comments
content = re.sub("(<!--.*?-->)", "", content)
return """frappe.templates["{key}"] = '{content}';\n""".format(\
key=path.rsplit("/", 1)[-1][:-5], content=content)
key=path.rsplit("/", 1)[-1][:-5], content=content.replace("'", "\'"))
def files_dirty():
for target, sources in get_build_maps().iteritems():

View file

@ -0,0 +1 @@
- Ability to set permissions based on owner by checking **If Owner** in Role Permissions Manager

View file

@ -0,0 +1,3 @@
Leave change log files in this folder for user release notes.
(this file is just a place holder, don't delete it)

View file

@ -0,0 +1,6 @@
#### Updates to Web Forms
- Web Forms list now is a standard portal list and includes paging and other extensions
- Section, Column Breaks in Web Forms
- Consistent User Interface
- Cleanup of Portal Pages

View file

@ -0,0 +1 @@
- Ability to send yourself a copy of the outgoing email added back.

View file

@ -0,0 +1,5 @@
- Reports are now searchable from awesome bar
- Show currect label for title in list views
- Datepicker now sets default value as Today
- Map child table as per meta, if not mentioned in table_map via mapper
- Re-enable save button on error

View file

@ -0,0 +1,3 @@
- Change print font from Setup > Print Settings or set it for each Print Format. Font options are "Default", "Arial", "Helvetica", "Verdana", "Monospace".
- Print and full-page print preview in user's language
- Fixed inconsistent visibility of a logged-in user's image in website

View file

@ -0,0 +1 @@
- Ability to **Share with Everyone** (except Guest) using **Share With**

View file

@ -4,7 +4,6 @@
from __future__ import unicode_literals, absolute_import
import sys
import os
import subprocess
import json
import click
import hashlib
@ -140,7 +139,7 @@ def reinstall(context):
frappe.clear_cache()
installed = frappe.get_installed_apps()
frappe.clear_cache()
except Exception, e:
except Exception:
installed = []
finally:
if frappe.db:
@ -191,8 +190,6 @@ def migrate(context, rebuild_website=False):
import frappe.translate
from frappe.desk.notifications import clear_notifications
verbose = context.verbose
for site in context.sites:
print 'Migrating', site
frappe.init(site=site)
@ -312,15 +309,16 @@ def destroy_all_sessions(context):
frappe.destroy()
@click.command('sync-www')
@click.option('--force', help='Rebuild all pages', is_flag=True, default=False)
@pass_context
def sync_www(context):
def sync_www(context, force=False):
"Sync files from static pages from www directory to Web Pages"
from frappe.website import statics
for site in context.sites:
try:
frappe.init(site=site)
frappe.connect()
statics.sync_statics(rebuild=context.force)
statics.sync_statics(rebuild=force)
frappe.db.commit()
finally:
frappe.destroy()
@ -341,32 +339,17 @@ def build_website(context):
frappe.destroy()
@click.command('setup-docs')
@click.argument('app')
@click.argument('docs-app')
@click.argument('path')
@pass_context
def setup_docs(context,app, docs_app, path):
def setup_docs(context):
"Setup docs in target folder of target app"
from frappe.utils.setup_docs import setup_docs
from frappe.website import statics
for site in context.sites:
try:
frappe.init(site=site)
frappe.connect()
setup_docs(app, docs_app, path)
finally:
frappe.destroy()
@click.command('build-docs')
@click.argument('app')
@pass_context
def build_docs(context, app):
"Build docs from /src to /www folder in app"
from frappe.utils.autodoc import build
frappe.destroy()
for site in context.sites:
try:
frappe.init(site=site)
build(app)
setup_docs()
statics.sync_statics(rebuild=True)
finally:
frappe.destroy()
@ -483,7 +466,7 @@ def export_json(context, doctype, name, path):
try:
frappe.init(site=site)
frappe.connect()
data_import_tool.export_json(doctype, name, path)
data_import_tool.export_json(doctype, path, name=name)
finally:
frappe.destroy()
@ -613,6 +596,7 @@ def console(context):
site = get_single_site(context)
frappe.init(site=site)
frappe.connect()
frappe.local.lang = frappe.db.get_default("lang")
import IPython
IPython.embed()
@ -825,7 +809,6 @@ commands = [
sync_www,
build_website,
setup_docs,
build_docs,
reset_perms,
execute,
celery,

View file

@ -208,15 +208,6 @@ def get_data():
"description": _("Install Applications."),
"icon": "icon-download"
},
{
"type": "doctype",
"name": "Backup Manager",
"label": _("Download Backup"),
"onclick": "frappe.ui.toolbar.download_backup",
"icon": "icon-download-alt",
"description": _("Send download link of a recent backup to System Managers"),
"hide_count": True
},
{
"type": "doctype",
"name": "Backup Manager",

View file

@ -103,12 +103,31 @@
"fieldtype": "Check",
"label": "Unsubscribed",
"permlevel": 0
},
{
"fieldname": "reference_doctype",
"fieldtype": "Link",
"label": "Reference DocType",
"options": "DocType",
"permlevel": 0,
"precision": "",
"read_only": 1
},
{
"description": "Reference DocType and Reference Name are used to render a comment as a link (href) to a Doc.",
"fieldname": "reference_name",
"fieldtype": "Dynamic Link",
"label": "Reference Name",
"options": "reference_doctype",
"permlevel": 0,
"precision": "",
"read_only": 1
}
],
"icon": "icon-comments",
"idx": 1,
"issingle": 0,
"modified": "2015-02-11 15:32:45.807458",
"modified": "2015-06-08 12:31:15.122312",
"modified_by": "Administrator",
"module": "Core",
"name": "Comment",

View file

@ -7,10 +7,10 @@ from frappe import _
from frappe.website.render import clear_cache
from frappe.model.document import Document
from frappe.model.db_schema import add_column
from frappe.utils import get_fullname
class Comment(Document):
"""Comments are added to Documents via forms or views like blogs etc."""
__doclink__ = "https://frappe.io/docs/models/core/comment"
no_feed_on_delete = True
def get_feed(self):
@ -38,6 +38,9 @@ class Comment(Document):
and comment_docname=%s""", (self.doctype, self.name))[0][0] >= 50:
frappe.throw(_("Cannot add more than 50 comments"))
if not self.comment_by_fullname and self.comment_by:
self.comment_by_fullname = get_fullname(self.comment_by)
def on_update(self):
"""Updates `_comments` property in parent Document."""
self.update_comment_in_doc()
@ -97,7 +100,7 @@ class Comment(Document):
"""Updates `_comments` property in parent Document with given dict.
:param _comments: Dict of comments."""
if frappe.db.get_value("DocType", self.comment_doctype, "issingle"):
if not self.comment_doctype or frappe.db.get_value("DocType", self.comment_doctype, "issingle"):
return
# use sql, so that we do not mess with the timestamp
@ -130,4 +133,3 @@ def on_doctype_update():
frappe.db.commit()
frappe.db.sql("""alter table `tabComment`
add index comment_doctype_docname_index(comment_doctype, comment_docname)""")

View file

@ -5,7 +5,7 @@ from __future__ import unicode_literals, absolute_import
import frappe
import json
from email.utils import formataddr, parseaddr
from frappe.utils import get_url, get_formatted_email
from frappe.utils import get_url, get_formatted_email, cstr, cint
from frappe.utils.file_manager import get_file
import frappe.email.smtp
from frappe import _
@ -43,13 +43,15 @@ class Communication(Document):
if to_status in status_field.options.splitlines():
frappe.db.set_value(parent.doctype, parent.name, "status", to_status)
def send(self, print_html=None, print_format=None, attachments=None):
def send(self, print_html=None, print_format=None, attachments=None,
send_me_a_copy=False, recipients=None):
"""Send communication via Email.
:param print_html: Send given value as HTML attachment.
:param print_format: Attach print format of parent document."""
self.notify(print_html, print_format, attachments)
self.send_me_a_copy = send_me_a_copy
self.notify(print_html, print_format, attachments, recipients)
def set_incoming_outgoing_accounts(self):
self.incoming_email_account = self.outgoing_email_account = None
@ -59,17 +61,20 @@ class Communication(Document):
{"append_to": self.reference_doctype, "enable_incoming": 1}, "email_id")
self.outgoing_email_account = frappe.db.get_value("Email Account",
{"append_to": self.reference_doctype, "enable_outgoing": 1}, "email_id")
{"append_to": self.reference_doctype, "enable_outgoing": 1},
["email_id", "always_use_account_email_id_as_sender"], as_dict=True)
if not self.incoming_email_account:
self.incoming_email_account = frappe.db.get_value("Email Account", {"default_incoming": 1}, "email_id")
if not self.outgoing_email_account:
self.outgoing_email_account = frappe.db.get_value("Email Account", {"default_outgoing": 1}, "email_id")
self.outgoing_email_account = frappe.db.get_value("Email Account", {"default_outgoing": 1},
["email_id", "always_use_account_email_id_as_sender"], as_dict=True) or frappe._dict()
def notify(self, print_html=None, print_format=None, attachments=None, except_recipient=False):
def notify(self, print_html=None, print_format=None, attachments=None, recipients=None, except_recipient=False):
self.prepare_to_notify(print_html, print_format, attachments)
recipients = self.get_recipients(except_recipient=except_recipient)
if not recipients:
recipients = self.get_recipients(except_recipient=except_recipient)
frappe.sendmail(
recipients=recipients,
@ -96,8 +101,8 @@ class Communication(Document):
self.set_incoming_outgoing_accounts()
if not self.sender:
self.sender = formataddr([frappe.session.data.full_name or "Notification", self.outgoing_email_account])
if not self.sender or cint(self.outgoing_email_account.always_use_account_email_id_as_sender):
self.sender = formataddr([frappe.session.data.full_name or "Notification", self.outgoing_email_account.email_id])
self.attachments = []
@ -122,7 +127,8 @@ class Communication(Document):
def get_recipients(self, except_recipient=False):
"""Build a list of users to which this email should go to"""
original_recipients = [s.strip() for s in self.recipients.split(",")]
# [EDGE CASE] self.recipients can be None when an email is sent as BCC
original_recipients = [s.strip() for s in cstr(self.recipients).split(",")]
recipients = original_recipients[:]
if self.reference_doctype and self.reference_name:
@ -153,6 +159,9 @@ class Communication(Document):
if e not in filtered and email_id not in filtered:
filtered.append(e)
if getattr(self, "send_me_a_copy", False):
filtered.append(self.sender)
return filtered
def get_starrers(self):
@ -198,7 +207,8 @@ def on_doctype_update():
@frappe.whitelist()
def make(doctype=None, name=None, content=None, subject=None, sent_or_received = "Sent",
sender=None, recipients=None, communication_medium="Email", send_email=False,
print_html=None, print_format=None, attachments='[]', ignore_doctype_permissions=False):
print_html=None, print_format=None, attachments='[]', ignore_doctype_permissions=False,
send_me_a_copy=False):
"""Make a new communication.
:param doctype: Reference DocType.
@ -212,7 +222,9 @@ def make(doctype=None, name=None, content=None, subject=None, sent_or_received =
:param send_mail: Send via email (default **False**).
:param print_html: HTML Print format to be sent as attachment.
:param print_format: Print Format name of parent document to be sent as attachment.
:param attachments: List of attachments as list of files or JSON string."""
:param attachments: List of attachments as list of files or JSON string.
:param send_me_a_copy: Send a copy to the sender (default **False**).
"""
is_error_report = (doctype=="User" and name==frappe.session.user and subject=="Error Report")
@ -235,11 +247,17 @@ def make(doctype=None, name=None, content=None, subject=None, sent_or_received =
"reference_name": name
})
comm.insert(ignore_permissions=True)
recipients = None
if send_email:
comm.send(print_html, print_format, attachments)
comm.send_me_a_copy = send_me_a_copy
recipients = comm.get_recipients()
comm.send(print_html, print_format, attachments, send_me_a_copy=send_me_a_copy, recipients=recipients)
return comm.name
return {
"name": comm.name,
"recipients": ", ".join(recipients) if recipients else None
}
@frappe.whitelist()
def get_convert_to():

View file

@ -6,5 +6,4 @@ from __future__ import unicode_literals
from frappe.model.document import Document
class DocField(Document):
__doclink__ = "https://frappe.io/docs/models/core/docfield"
pass

View file

@ -33,6 +33,14 @@
"label": "Apply User Permissions",
"permlevel": 0
},
{
"description": "Apply this rule if the User is the Owner",
"fieldname": "if_owner",
"fieldtype": "Check",
"label": "If user is the owner",
"permlevel": 0,
"precision": ""
},
{
"fieldname": "column_break_2",
"fieldtype": "Column Break",
@ -232,7 +240,7 @@
"idx": 1,
"issingle": 0,
"istable": 1,
"modified": "2015-03-18 06:09:58.928014",
"modified": "2015-07-22 07:39:40.471092",
"modified_by": "Administrator",
"module": "Core",
"name": "DocPerm",

View file

@ -7,5 +7,4 @@ import frappe
from frappe.model.document import Document
class DocPerm(Document):
__doclink__ = "https://frappe.io/docs/models/v5.x/core/docperm"
pass

View file

@ -26,7 +26,7 @@
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 1,
"reqd": 0,
"search_index": 1,
"set_only_once": 0
},
@ -129,6 +129,13 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0
},
{
"fieldname": "everyone",
"fieldtype": "Check",
"label": "Everyone",
"permlevel": 0,
"precision": ""
}
],
"hide_heading": 0,
@ -138,7 +145,7 @@
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"modified": "2015-02-12 11:30:52.968078",
"modified": "2015-07-17 07:02:10.632582",
"modified_by": "Administrator",
"module": "Core",
"name": "DocShare",

View file

@ -11,6 +11,7 @@ class DocShare(Document):
no_feed_on_delete = True
def validate(self):
self.validate_user()
self.check_share_permission()
self.cascade_permissions_downwards()
self.get_doc().run_method("validate_share", self)
@ -26,6 +27,12 @@ class DocShare(Document):
self._doc = frappe.get_doc(self.share_doctype, self.share_name)
return self._doc
def validate_user(self):
if self.everyone:
self.user = None
elif not self.user:
frappe.throw(_("User is mandatory for Share"), frappe.MandatoryError)
def check_share_permission(self):
if (not self.flags.ignore_share_permission and
not frappe.has_permission(self.share_doctype, "share", self.get_doc())):

View file

@ -78,3 +78,15 @@ class TestDocShare(unittest.TestCase):
frappe.set_user(self.user)
self.assertFalse(self.event.has_permission("share"))
def test_share_with_everyone(self):
self.assertTrue(self.event.name not in frappe.share.get_shared("Event", self.user))
frappe.share.set_permission("Event", self.event.name, None, "read", everyone=1)
self.assertTrue(self.event.name in frappe.share.get_shared("Event", self.user))
self.assertTrue(self.event.name in frappe.share.get_shared("Event", "test1@example.com"))
self.assertTrue(self.event.name not in frappe.share.get_shared("Event", "Guest"))
frappe.share.set_permission("Event", self.event.name, None, "read", value=0, everyone=1)
self.assertTrue(self.event.name not in frappe.share.get_shared("Event", self.user))
self.assertTrue(self.event.name not in frappe.share.get_shared("Event", "test1@example.com"))
self.assertTrue(self.event.name not in frappe.share.get_shared("Event", "Guest"))

View file

@ -18,7 +18,6 @@ form_grid_templates = {
}
class DocType(Document):
__doclink__ = "https://frappe.io/docs/models/core/doctype"
def get_feed(self):
return self.name
@ -315,6 +314,7 @@ def validate_fields(meta):
if not d.permlevel: d.permlevel = 0
if not d.fieldname:
frappe.throw(_("Fieldname is required in row {0}").format(d.idx))
d.fieldname = d.fieldname.lower()
check_illegal_characters(d.fieldname)
check_unique_fieldname(d.fieldname)
check_illegal_mandatory(d)
@ -362,14 +362,21 @@ def validate_permissions(doctype, for_remove=False):
def check_double(d):
has_similar = False
similar_because_of = ""
for p in permissions:
if (p.role==d.role and p.permlevel==d.permlevel
and p.apply_user_permissions==d.apply_user_permissions and p!=d):
has_similar = True
break
if p.role==d.role and p.permlevel==d.permlevel and p!=d:
if p.apply_user_permissions==d.apply_user_permissions:
has_similar = True
similar_because_of = _("Apply User Permissions")
break
elif p.if_owner==d.if_owner:
similar_because_of = _("If Owner")
has_similar = True
break
if has_similar:
frappe.throw(_("{0}: Only one rule allowed with the same Role, Level and Apply User Permissions").format(get_txt(d)))
frappe.throw(_("{0}: Only one rule allowed with the same Role, Level and {1}")\
.format(get_txt(d), similar_because_of))
def check_level_zero_is_set(d):
if cint(d.permlevel) > 0 and d.role != 'All':
@ -382,8 +389,8 @@ def validate_permissions(doctype, for_remove=False):
if not has_zero_perm:
frappe.throw(_("{0}: Permission at level 0 must be set before higher levels are set").format(get_txt(d)))
if d.create or d.submit or d.cancel or d.amend:
frappe.throw(_("{0}: Create, Submit, Cancel and Amend only valid at level 0").format(get_txt(d)))
for invalid in ("create", "submit", "cancel", "amend"):
if d.get(invalid): d.set(invalid, 0)
def check_permission_dependency(d):
if d.cancel and not d.submit:
@ -466,4 +473,3 @@ def init_list(doctype):
doc = frappe.get_meta(doctype)
make_boilerplate("controller_list.js", doc)
make_boilerplate("controller_list.html", doc)

View file

@ -7,7 +7,6 @@ import frappe, os
from frappe.model.document import Document
class ModuleDef(Document):
__doclink__ = "https://frappe.io/docs/models/core/module_def"
def on_update(self):
"""If in `developer_mode`, create folder for module and
add in `modules.txt` of app if missing."""
@ -39,7 +38,3 @@ class ModuleDef(Document):
frappe.clear_cache()
frappe.setup_module_map()

View file

@ -87,7 +87,7 @@
"idx": 1,
"issingle": 0,
"istable": 0,
"modified": "2015-02-05 05:11:41.982758",
"modified": "2015-07-13 04:45:55.942795",
"modified_by": "Administrator",
"module": "Core",
"name": "Page",
@ -122,14 +122,6 @@
"share": 1,
"submit": 0,
"write": 1
},
{
"apply_user_permissions": 1,
"email": 1,
"permlevel": 0,
"print": 1,
"read": 1,
"role": "All"
}
],
"read_only": 0

View file

@ -5,6 +5,7 @@ from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
from frappe.build import html_to_js_template
from frappe import conf
class Page(Document):
def autoname(self):
@ -26,13 +27,16 @@ class Page(Document):
cnt = 1
self.name += '-' + str(cnt)
def validate(self):
if not getattr(conf,'developer_mode', 0):
frappe.throw(_("Not in Developer Mode"))
# export
def on_update(self):
"""
Writes the .txt for this page and if write_content is checked,
it will write out a .html file
"""
from frappe import conf
from frappe.core.doctype.doctype.doctype import make_module_and_roles
make_module_and_roles(self, "roles")
@ -62,6 +66,21 @@ class Page(Document):
d[key] = self.get(key)
return d
def is_permitted(self):
"""Returns true if Page Role is not set or the user is allowed."""
from frappe.utils import has_common
allowed = [d.role for d in frappe.get_all("Page Role", fields=["role"],
filters={"parent": self.name})]
if not allowed:
return True
roles = frappe.get_roles()
if has_common(roles, allowed):
return True
def load_assets(self):
from frappe.modules import get_module_path, scrub
import os

View file

@ -6,6 +6,15 @@
"doctype": "DocType",
"document_type": "System",
"fields": [
{
"default": "0",
"fieldname": "seen",
"fieldtype": "Check",
"hidden": 1,
"label": "Seen",
"permlevel": 0,
"precision": ""
},
{
"fieldname": "method",
"fieldtype": "Data",
@ -22,7 +31,7 @@
],
"icon": "icon-warning-sign",
"idx": 1,
"modified": "2015-02-05 05:11:46.339879",
"modified": "2015-05-28 02:49:12.819934",
"modified_by": "Administrator",
"module": "Core",
"name": "Scheduler Log",

View file

@ -9,4 +9,11 @@ import frappe
from frappe.model.document import Document
class SchedulerLog(Document):
pass
def onload(self):
if not self.seen:
self.seen = 1
self.save()
def set_old_logs_as_seen():
frappe.db.sql("""update `tabScheduler Log` set seen=1
where ifnull(seen, 0)=0 and datediff(curdate(), creation) > 7""")

View file

@ -0,0 +1,11 @@
frappe.listview_settings['Scheduler Log'] = {
add_fields: ["seen"],
get_indicator: function(doc) {
if(cint(doc.seen)) {
return [__("Seen"), "green", "seen,=,1"];
} else {
return [__("Not Seen"), "red", "seen,=,0"];
}
},
order_by: "seen asc, modified desc",
};

View file

@ -0,0 +1,12 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
import frappe
import unittest
# test_records = frappe.get_test_records('Scheduler Log')
class TestSchedulerLog(unittest.TestCase):
pass

View file

@ -7,7 +7,7 @@
{
"fieldname": "localization",
"fieldtype": "Section Break",
"label": "Localization",
"label": "",
"permlevel": 0
},
{
@ -20,6 +20,12 @@
"reqd": 1,
"search_index": 0
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break",
"permlevel": 0,
"precision": ""
},
{
"fieldname": "time_zone",
"fieldtype": "Select",
@ -30,7 +36,7 @@
{
"fieldname": "date_and_number_format",
"fieldtype": "Section Break",
"label": "Date and Number Format",
"label": "",
"permlevel": 0
},
{
@ -41,6 +47,12 @@
"permlevel": 0,
"reqd": 1
},
{
"fieldname": "column_break_7",
"fieldtype": "Column Break",
"permlevel": 0,
"precision": ""
},
{
"fieldname": "number_format",
"fieldtype": "Select",
@ -80,6 +92,12 @@
"permlevel": 0,
"precision": ""
},
{
"fieldname": "column_break_13",
"fieldtype": "Column Break",
"permlevel": 0,
"precision": ""
},
{
"description": "Run scheduled jobs only if checked",
"fieldname": "enable_scheduler",
@ -96,11 +114,39 @@
"permlevel": 0,
"precision": "",
"report_hide": 1
},
{
"fieldname": "email",
"fieldtype": "Section Break",
"label": "EMail",
"permlevel": 0,
"precision": ""
},
{
"description": "Your organization name and address for the email footer.",
"fieldname": "email_footer_address",
"fieldtype": "Small Text",
"label": "Email Footer Address",
"permlevel": 0,
"precision": ""
},
{
"fieldname": "column_break_18",
"fieldtype": "Column Break",
"permlevel": 0,
"precision": ""
},
{
"fieldname": "disable_standard_email_footer",
"fieldtype": "Check",
"label": "Disable Standard Email Footer",
"permlevel": 0,
"precision": ""
}
],
"icon": "icon-cog",
"issingle": 1,
"modified": "2015-05-18 05:11:38.759688",
"modified": "2015-05-21 07:15:55.682132",
"modified_by": "Administrator",
"module": "Core",
"name": "System Settings",

View file

@ -5,6 +5,7 @@ from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.model import no_value_fields
from frappe.translate import get_lang_dict, set_default_language
from frappe.utils import cint
from frappe.utils.momentjs import get_all_timezones
@ -19,7 +20,7 @@ class SystemSettings(Document):
def on_update(self):
for df in self.meta.get("fields"):
if df.fieldtype in ("Select", "Data", "Check"):
if df.fieldtype not in no_value_fields:
frappe.db.set_default(df.fieldname, self.get(df.fieldname))
if self.language:

View file

@ -46,9 +46,7 @@ cur_frm.cscript.refresh = function(doc) {
window.location.reload();
}
cur_frm.toggle_display('change_password', !doc.__islocal);
cur_frm.toggle_display(['sb1', 'sb3'], false);
cur_frm.toggle_display(['sb1', 'sb3', 'modules_access'], false);
if(!doc.__islocal){
cur_frm.add_custom_button(__("Set User Permissions"), function() {
@ -59,7 +57,7 @@ cur_frm.cscript.refresh = function(doc) {
}, null, "btn-default")
if(has_common(user_roles, ["Administrator", "System Manager"])) {
cur_frm.toggle_display(['sb1', 'sb3'], true);
cur_frm.toggle_display(['sb1', 'sb3', 'modules_access'], true);
}
cur_frm.cscript.enabled(doc);
@ -77,7 +75,7 @@ cur_frm.cscript.refresh = function(doc) {
cur_frm.cscript.enabled = function(doc) {
if(!doc.__islocal && has_common(user_roles, ["Administrator", "System Manager"])) {
cur_frm.toggle_display(['sb1', 'sb3'], doc.enabled);
cur_frm.toggle_display(['sb1', 'sb3', 'modules_access'], doc.enabled);
cur_frm.toggle_enable('*', doc.enabled);
cur_frm.set_df_property('enabled', 'read_only', 0);
}

View file

@ -115,6 +115,7 @@
"permlevel": 0
},
{
"depends_on": "eval:!doc.__islocal",
"fieldname": "change_password",
"fieldtype": "Section Break",
"label": "",
@ -127,6 +128,14 @@
"no_copy": 1,
"permlevel": 0
},
{
"depends_on": "",
"fieldname": "send_password_update_notification",
"fieldtype": "Check",
"label": "Send Password Update Notification",
"permlevel": 0,
"precision": ""
},
{
"fieldname": "reset_password_key",
"fieldtype": "Data",
@ -473,7 +482,7 @@
"issingle": 0,
"istable": 0,
"max_attachments": 5,
"modified": "2015-04-24 14:37:26.430454",
"modified": "2015-06-01 01:00:32.901851",
"modified_by": "Administrator",
"module": "Core",
"name": "User",

View file

@ -76,8 +76,9 @@ class User(Document):
if new_password and not self.in_insert:
_update_password(self.name, new_password)
self.password_update_mail(new_password)
frappe.msgprint(_("New password emailed"))
if self.send_password_update_notification:
self.password_update_mail(new_password)
frappe.msgprint(_("New password emailed"))
def on_update(self):
# clear new password
@ -324,12 +325,16 @@ def get_perm_info(arg=None):
and docstatus<2 order by parent, permlevel""", (frappe.form_dict['role'],), as_dict=1)
@frappe.whitelist(allow_guest=True)
def update_password(new_password, key=None):
def update_password(new_password, key=None, old_password=None):
# verify old password
if key:
user = frappe.db.get_value("User", {"reset_password_key":key})
if not user:
return _("Cannot Update: Incorrect / Expired Link.")
elif old_password:
# verify old password
frappe.local.login_manager.check_password(frappe.session.user, old_password)
user = frappe.session.user
_update_password(user, new_password)
@ -342,6 +347,10 @@ def update_password(new_password, key=None):
else:
return "/"
@frappe.whitelist()
def verify_password(password):
frappe.local.login_manager.check_password(frappe.session.user, password)
@frappe.whitelist(allow_guest=True)
def sign_up(email, full_name):
user = frappe.db.get("User", {"email": email})

View file

@ -15,3 +15,5 @@ frappe.listview_settings['User'] = {
}
}
};
frappe.help.youtube_id["User"] = "fnBoRhBrwR4";

View file

@ -6,6 +6,9 @@ import frappe
def get_notification_config():
return {
"for_doctype": {
"Scheduler Log": {"seen": 0},
},
"for_module_doctypes": {
"ToDo": "To Do",
"Event": "Calendar",

View file

@ -7,16 +7,25 @@ frappe.DataImportTool = Class.extend({
title: __("Data Import Tool"),
single_column: true
});
this.page.add_inner_button(__("Help"), function() {
frappe.help.show_video("6wiriRKPhmg");
});
this.make();
this.make_upload();
},
set_route_options: function() {
if(frappe.route_options
&& frappe.route_options.doctype
&& in_list(frappe.boot.user.can_import, frappe.route_options.doctype)) {
this.select.val(frappe.route_options.doctype).change();
frappe.route_options = null;
var doctype = null;
if(frappe.get_route()[1]) {
doctype = frappe.get_route()[1];
} else if(frappe.route_options && frappe.route_options.doctype) {
doctype = frappe.route_options.doctype;
}
if(in_list(frappe.boot.user.can_import, doctype)) {
this.select.val(doctype).change();
}
frappe.route_options = null;
},
make: function() {
var me = this;

View file

@ -3,7 +3,7 @@
from __future__ import unicode_literals
import frappe, json, os
import frappe, os
from frappe import _
import frappe.modules.import_file
@ -30,12 +30,13 @@ def get_doctype_options():
doctype = frappe.form_dict['doctype']
return [doctype] + [d.options for d in frappe.get_meta(doctype).get_table_fields()]
def import_file_by_path(path, ignore_links=False, overwrite=False, submit=False):
def import_file_by_path(path, ignore_links=False, overwrite=False, submit=False, pre_process=None):
from frappe.utils.csvutils import read_csv_content
from frappe.core.page.data_import_tool.importer import upload
print "Importing " + path
with open(path, "r") as infile:
upload(rows = read_csv_content(infile.read()), ignore_links=ignore_links, overwrite=overwrite, submit_after_import=submit)
upload(rows = read_csv_content(infile.read()), ignore_links=ignore_links, overwrite=overwrite,
submit_after_import=submit, pre_process=pre_process)
def export_csv(doctype, path):
from frappe.core.page.data_import_tool.exporter import get_template
@ -43,7 +44,7 @@ def export_csv(doctype, path):
get_template(doctype=doctype, all_doctypes="Yes", with_data="Yes")
csvfile.write(frappe.response.result.encode("utf-8"))
def export_json(doctype, path, filters=None):
def export_json(doctype, path, filters=None, name=None):
def post_process(out):
del_keys = ('parent', 'parentfield', 'parenttype', 'modified_by', 'creation', 'owner', 'idx')
for doc in out:
@ -57,9 +58,10 @@ def export_json(doctype, path, filters=None):
if key in child:
del child[key]
from frappe.utils.response import json_handler
out = []
if frappe.db.get_value("DocType", doctype, "issingle"):
if name:
out.append(frappe.get_doc(doctype, name).as_dict())
elif frappe.db.get_value("DocType", doctype, "issingle"):
out.append(frappe.get_doc(doctype).as_dict())
else:
for doc in frappe.get_all(doctype, fields=["name"], filters=filters, limit_page_length=0, order_by="creation asc"):
@ -79,18 +81,18 @@ def export_fixture(doctype, app):
export_json(doctype, frappe.get_app_path(app, "fixtures", frappe.scrub(doctype) + ".json"))
def import_doc(path, overwrite=False, ignore_links=False, ignore_insert=False, insert=False, submit=False):
def import_doc(path, overwrite=False, ignore_links=False, ignore_insert=False,
insert=False, submit=False, pre_process=None):
if os.path.isdir(path):
files = [os.path.join(path, f) for f in os.listdir(path)]
else:
files = [path]
for f in files:
if f.endswith(".json"):
frappe.flags.mute_emails = True
frappe.modules.import_file.import_file_by_path(f, data_import=True)
frappe.modules.import_file.import_file_by_path(f, data_import=True, force=True, pre_process=pre_process)
frappe.flags.mute_emails = False
elif f.endswith(".csv"):
import_file_by_path(f, ignore_links=ignore_links, overwrite=overwrite, submit=submit)
import_file_by_path(f, ignore_links=ignore_links, overwrite=overwrite, submit=submit, pre_process=pre_process)
frappe.db.commit()

View file

@ -8,7 +8,7 @@ from frappe import _
import frappe.permissions
import re
from frappe.utils.csvutils import UnicodeWriter
from frappe.utils import cstr, cint, flt
from frappe.utils import cstr, cint, flt, formatdate, format_datetime
from frappe.core.page.data_import_tool.data_import_tool import get_data_keys
reflags = {
@ -31,7 +31,7 @@ def get_template(doctype=None, parent_doctype=None, all_doctypes="No", with_data
if len(doctype) > 1:
docs_to_export = doctype[1]
doctype = doctype[0]
if not parent_doctype:
parent_doctype = doctype
@ -46,7 +46,7 @@ def get_template(doctype=None, parent_doctype=None, all_doctypes="No", with_data
def get_data_keys_definition():
return get_data_keys()
def add_main_header():
w.writerow([_('Data Import Template')])
w.writerow([get_data_keys_definition().main_table, doctype])
@ -155,6 +155,7 @@ def get_template(doctype=None, parent_doctype=None, all_doctypes="No", with_data
def add_data():
def add_data_row(row_group, dt, doc, rowidx):
d = doc.copy()
meta = frappe.get_meta(dt)
if all_doctypes:
d.name = '"'+ d.name+'"'
@ -162,7 +163,16 @@ def get_template(doctype=None, parent_doctype=None, all_doctypes="No", with_data
row_group.append([""] * (len(columns) + 1))
row = row_group[rowidx]
for i, c in enumerate(columns[column_start_end[dt].start:column_start_end[dt].end]):
row[column_start_end[dt].start + i + 1] = d.get(c, "")
df = meta.get_field(c)
fieldtype = df.fieldtype if df else "Data"
value = d.get(c, "")
if value:
if fieldtype == "Date":
value = formatdate(value)
elif fieldtype == "Datetime":
value = format_datetime(value)
row[column_start_end[dt].start + i + 1] = value
if with_data=='Yes':
frappe.permissions.can_export(parent_doctype, raise_exception=True)
@ -172,7 +182,7 @@ def get_template(doctype=None, parent_doctype=None, all_doctypes="No", with_data
for doc in data:
op = docs_to_export.get("op")
names = docs_to_export.get("name")
if names and op:
if op == '=' and doc.name not in names:
continue
@ -184,7 +194,7 @@ def get_template(doctype=None, parent_doctype=None, all_doctypes="No", with_data
flags = 0
for a in re.split('\W+',sflags):
flags = flags | reflags.get(a,0)
c = re.compile(names, flags)
m = c.match(doc.name)
if not m:

View file

@ -15,7 +15,8 @@ from frappe.utils import cint, cstr, flt
from frappe.core.page.data_import_tool.data_import_tool import get_data_keys
@frappe.whitelist()
def upload(rows = None, submit_after_import=None, ignore_encoding_errors=False, overwrite=None, ignore_links=False):
def upload(rows = None, submit_after_import=None, ignore_encoding_errors=False, overwrite=None,
ignore_links=False, pre_process=None):
"""upload data"""
frappe.flags.mute_emails = True
# extra input params
@ -200,6 +201,9 @@ def upload(rows = None, submit_after_import=None, ignore_encoding_errors=False,
doc = None
doc = get_doc(row_idx)
if pre_process:
pre_process(doc)
try:
frappe.local.message_log = []
if parentfield:

View file

@ -191,6 +191,7 @@ frappe.PermissionEngine = Class.extend({
if (d.permlevel===0) {
me.setup_user_permissions(d, role_cell);
me.setup_if_owner(d, role_cell);
}
var cell = me.add_cell(row, d, "permlevel");
@ -269,6 +270,12 @@ frappe.PermissionEngine = Class.extend({
d.help = "";
},
setup_if_owner: function(d, role_cell) {
var checkbox = this.add_check(role_cell, d, "if_owner")
.removeClass("col-md-4")
.css({"margin-top": "15px"});
},
rights: ["read", "write", "create", "delete", "submit", "cancel", "amend",
"print", "email", "report", "import", "export", "set_user_permissions"],

View file

@ -19,7 +19,7 @@ def get_roles_and_doctypes():
name not in ('DocType') and
exists(select * from `tabDocField` where parent=dt.name)""")],
"roles": [d[0] for d in frappe.db.sql("""select name from tabRole where name not in
('Guest', 'Administrator')""")]
('Administrator')""")]
}
@frappe.whitelist()

View file

@ -243,7 +243,7 @@ frappe.UserPermissions = Class.extend({
+__("These restrictions will apply for Document Types where 'Apply User Permissions' is checked for the permission rule and a field with this value is present.")
+'</p>').appendTo(this.body);
$.each([[__("Allow User If"), 150], [__("Document Type"), 150], [__("Is"),150], ["", 50]],
$.each([[__("Allow User"), 150], [__("If Document Type"), 150], [__("Is"),150], ["", 50]],
function(i, col) {
$("<th>").html(col[0]).css("width", col[1]+"px")
.appendTo(me.table.find("thead tr"));
@ -300,9 +300,9 @@ frappe.UserPermissions = Class.extend({
var d = new frappe.ui.Dialog({
title: __("Add A New Restriction"),
fields: [
{fieldtype:"Select", label:__("Allow User If"),
{fieldtype:"Select", label:__("Allow User"),
options:me.options.users, reqd:1, fieldname:"user"},
{fieldtype:"Select", label: __("Select Document Type"), fieldname:"defkey",
{fieldtype:"Select", label: __("If Document Type"), fieldname:"defkey",
options:me.get_link_names(), reqd:1},
{fieldtype:"Link", label:__("Is"), fieldname:"defvalue",
options:'[Select]', reqd:1},

View file

@ -102,7 +102,7 @@ class CustomizeForm(Document):
meta = frappe.get_meta(self.doc_type)
# doctype property setters
for property in self.doctype_properties:
if property != "idx" and self.get(property) != meta.get(property):
if self.get(property) != meta.get(property):
self.make_property_setter(property=property, value=self.get(property),
property_type=self.doctype_properties[property])
@ -117,7 +117,7 @@ class CustomizeForm(Document):
continue
for property in self.docfield_properties:
if df.get(property) != meta_df[0].get(property):
if property != "idx" and df.get(property) != meta_df[0].get(property):
if property == "fieldtype":
self.validate_fieldtype_change(df, meta_df[0].get(property), df.get(property))
@ -260,4 +260,3 @@ class CustomizeForm(Document):
and ifnull(field_name, '')!='naming_series'""", self.doc_type)
frappe.clear_cache(doctype=self.doc_type)
self.fetch_to_customize()

View file

@ -1,52 +0,0 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
# DEPRECATED only for reference
from __future__ import unicode_literals
# app configuration
# database config
db_name = '%(db_name)s'
db_password = '%(db_password)s'
# user attachments stored in
files_path = 'public/files'
public_path = 'public'
# max file attachment size (default 1MB)
max_file_size = 1000000
# max email size in bytes
max_email_size = 0
# total pop session timeout in seconds
pop_timeout = 0
# generate schema (.txt files)
developer_mode = 0
# clear cache on refresh
auto_cache_clear = 0
# email logs to admin (beta)
admin_email_notification = 0
# user timezone
user_timezone = 'Asia/Calcutta'
# outgoing mail settings
mail_server = None
mail_login = None
mail_password = None
mail_port = None
use_ssl = None
auto_email_id = None
# logging settings
log_file_name = 'logs/error_log.txt'
debug_log_dbs = []
log_level = 'logging.INFO'
log_file_size = 5000
log_file_backup_count = 5

View file

@ -2,7 +2,7 @@ ar العربية
bg bǎlgarski
bs bosanski
ca català
cz česky
cs česky
da dansk
de deutsch
el ελληνικά

View file

@ -511,6 +511,10 @@ class Database:
tabSingles where doctype=%s and field=%s""", (doctype, fieldname))
return val[0][0] if val else None
def get_singles_value(self, *args, **kwargs):
"""Alias for get_single_value"""
return self.get_single_value(*args, **kwargs)
def _get_values_from_table(self, fields, filters, doctype, as_dict, debug, order_by=None, update=None):
fl = []
if isinstance(fields, (list, tuple)):

View file

@ -11,7 +11,7 @@ def get(name):
Return the :term:`doclist` of the `Page` specified by `name`
"""
page = frappe.get_doc('Page', name)
if has_permission(page):
if page.is_permitted():
page.load_assets()
return page
else:

View file

@ -11,6 +11,10 @@ from frappe.utils.user import get_enabled_system_users
weekdays = ["monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"]
class Event(Document):
def get_route(self):
"""for test-case"""
return "/Event/" + self.name
def validate(self):
if self.starts_on and self.ends_on and self.starts_on > self.ends_on:
frappe.msgprint(frappe._("Event end must be after start"), raise_exception=True)

View file

@ -86,7 +86,7 @@
"fieldtype": "Link",
"ignore_user_permissions": 1,
"in_list_view": 0,
"label": "Assigned To",
"label": "Allocated To",
"options": "User",
"permlevel": 0,
"reqd": 1
@ -178,7 +178,7 @@
"in_dialog": 0,
"issingle": 0,
"max_attachments": 0,
"modified": "2015-05-04 07:44:46.567785",
"modified": "2015-06-11 16:06:34.561469",
"modified_by": "Administrator",
"module": "Desk",
"name": "ToDo",

View file

@ -14,10 +14,10 @@ sender_field = "sender"
class ToDo(Document):
def validate(self):
if self.is_new():
self.add_assign_comment(frappe._("Assigned to {0}").format(get_fullname(self.owner)), "Assigned")
self.add_assign_comment(frappe._("Assigned to {0}: {1}").format(get_fullname(self.owner), self.description), "Assigned")
else:
cur_status = frappe.db.get_value("ToDo", self.name, "status")
if cur_status != self.status:
# NOTE the previous value is only available in validate method
if self.get_db_value("status") != self.status:
self.add_assign_comment(frappe._("Assignment closed by {0}".format(get_fullname(frappe.session.user))),
"Assignment Completed")
@ -25,20 +25,29 @@ class ToDo(Document):
self.update_in_reference()
def on_trash(self):
# unlink assignment comment
frappe.db.sql("""update `tabComment` set reference_doctype=null and reference_name=null
where reference_doctype='ToDo' and reference_name=%s""", self.name)
self.update_in_reference()
def add_assign_comment(self, text, comment_type):
if not self.reference_type and self.reference_name:
return
frappe.get_doc({
comment = frappe.get_doc({
"doctype":"Comment",
"comment_by": frappe.session.user,
"comment_type": comment_type,
"comment_doctype": self.reference_type,
"comment_docname": self.reference_name,
"comment": """{text}""".format(text=text)
}).insert(ignore_permissions=True)
"comment": """{text}""".format(text=text),
"reference_doctype": self.doctype,
"reference_name": self.name
})
comment.flags.ignore_permissions = True
comment.flags.ignore_links = True
comment.insert()
def update_in_reference(self):
if not (self.reference_type and self.reference_name):

View file

@ -89,7 +89,7 @@ def get_docinfo(doc=None, doctype=None, name=None):
"assignments": get_assignments(doc.doctype, doc.name),
"permissions": get_doc_permissions(doc),
"shared": frappe.share.get_users(doc.doctype, doc.name,
fields=["user", "read", "write", "share"])
fields=["user", "read", "write", "share", "everyone"])
}
def get_user_permissions(meta):
@ -105,7 +105,8 @@ def get_attachments(dt, dn):
def get_comments(dt, dn, limit=100):
comments = frappe.db.sql("""select name, comment, comment_by, creation,
comment_type, "Comment" as doctype from `tabComment`
reference_doctype, reference_name, comment_type, "Comment" as doctype
from `tabComment`
where comment_doctype=%s and comment_docname=%s
order by creation desc limit %s""" % ('%s','%s', limit),
(dt, dn), as_dict=1)

View file

@ -11,6 +11,7 @@ from frappe.model.workflow import get_workflow_name
from frappe.utils import get_html_format
from frappe.translate import make_dict_from_messages, extract_messages_from_code
from frappe.utils.jinja import render_include
from frappe.build import html_to_js_template
######
@ -70,12 +71,25 @@ class FormMeta(Meta):
self.add_code_via_hook("doctype_js", "__js")
self.add_code_via_hook("doctype_list_js", "__list_js")
self.add_custom_script()
self.add_html_templates(path)
def _add_code(self, path, fieldname):
js = frappe.read_file(path)
if js:
self.set(fieldname, (self.get(fieldname) or "") + "\n\n" + render_include(js))
def add_html_templates(self, path):
if self.custom:
return
js = ""
for fname in os.listdir(path):
if fname.endswith(".html"):
with open(os.path.join(path, fname), 'r') as f:
template = unicode(f.read(), "utf-8")
js += html_to_js_template(fname, template)
self.set("__js", (self.get("__js") or "") + js)
def add_code_via_hook(self, hook, fieldname):
for app_name in frappe.get_installed_apps():
code_hook = frappe.get_hooks(hook, default={}, app_name=app_name)

View file

@ -144,6 +144,7 @@ def get_linked_docs(doctype, name, metadata_loaded=None, no_metadata=False):
filters=[[dt, link.get("fieldname"), '=', name]])
except frappe.PermissionError:
frappe.local.message_log.pop()
continue
if ret:

View file

@ -137,7 +137,8 @@ def apply_permissions(data):
if ((item.type=="doctype" and item.name in user.can_read)
or (item.type=="page" and item.name in allowed_pages)
or (item.type=="report" and item.doctype in user.can_get_report)):
or (item.type=="report" and item.doctype in user.can_get_report)
or item.type=="help"):
new_items.append(item)

View file

@ -11,6 +11,7 @@ from frappe.modules import scrub, get_module_path
from frappe.utils import flt, cint, get_html_format
from frappe.translate import send_translations
import frappe.desk.reportview
from frappe.permissions import get_role_permissions
def get_report_doc(report_name):
doc = frappe.get_doc("Report", report_name)
@ -144,20 +145,35 @@ def get_filtered_data(ref_doctype, columns, data):
linked_doctypes = get_linked_doctypes(columns, data)
match_filters_per_doctype = get_user_match_filters(linked_doctypes, ref_doctype)
shared = frappe.share.get_shared(ref_doctype)
columns_dict = get_columns_dict(columns)
role_permissions = get_role_permissions(frappe.get_meta(ref_doctype))
if_owner = role_permissions.get("if_owner", {}).get("report")
if match_filters_per_doctype:
for row in data:
if shared and row[linked_doctypes[ref_doctype]] in shared:
result.append(row)
elif has_match(row, linked_doctypes, match_filters_per_doctype):
elif has_match(row, linked_doctypes, match_filters_per_doctype, ref_doctype, if_owner, columns_dict):
result.append(row)
else:
result = list(data)
return result
def has_match(row, linked_doctypes, doctype_match_filters):
def has_match(row, linked_doctypes, doctype_match_filters, ref_doctype, if_owner, columns_dict):
"""Returns True if after evaluating permissions for each linked doctype
- There is an owner match for the ref_doctype
- `and` There is a user permission match for all linked doctypes
Returns True if the row is empty
Note:
Each doctype could have multiple conflicting user permission doctypes.
Hence even if one of the sets allows a match, it is true.
This behavior is equivalent to the trickling of user permissions of linked doctypes to the ref doctype.
"""
resultant_match = True
if not row:
@ -167,20 +183,33 @@ def has_match(row, linked_doctypes, doctype_match_filters):
for doctype, filter_list in doctype_match_filters.items():
matched_for_doctype = False
for match_filters in filter_list:
match = True
for dt, idx in linked_doctypes.items():
if dt in match_filters and row[idx] not in match_filters[dt]:
match = False
if doctype==ref_doctype and if_owner:
idx = linked_doctypes.get("User")
if (idx is not None
and row[idx]==frappe.session.user
and columns_dict[idx]==columns_dict.get("owner")):
# owner match is true
matched_for_doctype = True
if not matched_for_doctype:
for match_filters in filter_list:
match = True
for dt, idx in linked_doctypes.items():
# case handled above
if dt=="User" and columns_dict[idx]==columns_dict.get("owner"):
continue
if dt in match_filters and row[idx] not in match_filters[dt]:
match = False
break
# each doctype could have multiple conflicting user permission doctypes, hence using OR
# so that even if one of the sets allows a match, it is true
matched_for_doctype = matched_for_doctype or match
if matched_for_doctype:
break
# each doctype could have multiple conflicting user permission doctypes, hence using OR
# so that even if one of the sets allows a match, it is true
matched_for_doctype = matched_for_doctype or match
if matched_for_doctype:
break
# each doctype's user permissions should match the row! hence using AND
resultant_match = resultant_match and matched_for_doctype
@ -192,16 +221,16 @@ def has_match(row, linked_doctypes, doctype_match_filters):
def get_linked_doctypes(columns, data):
linked_doctypes = {}
for idx, col in enumerate(columns):
if isinstance(col, basestring):
col = col.split(":")
if len(col) > 1 and col[1].startswith("Link"):
link_dt = col[1].split("/")[1]
linked_doctypes[link_dt] = idx
columns_dict = get_columns_dict(columns)
# dict
elif col.get("fieldtype")=="Link" and col.get("options"):
linked_doctypes[col["options"]] = col["fieldname"]
for idx, col in enumerate(columns):
df = columns_dict[idx]
if df.get("fieldtype")=="Link":
if isinstance(col, basestring):
linked_doctypes[df["options"]] = idx
else:
# dict
linked_doctypes[df["options"]] = df["fieldname"]
# remove doctype if column is empty
for doctype, key in linked_doctypes.items():
@ -210,6 +239,35 @@ def get_linked_doctypes(columns, data):
return linked_doctypes
def get_columns_dict(columns):
"""Returns a dict with column docfield values as dict
The keys for the dict are both idx and fieldname,
so either index or fieldname can be used to search for a column's docfield properties
"""
columns_dict = {}
for idx, col in enumerate(columns):
col_dict = {}
# string
if isinstance(col, basestring):
col = col.split(":")
if len(col) > 1:
if "/" in col[1]:
col_dict["fieldtype"], col_dict["options"] = col[1].split("/")
else:
col_dict["fieldtype"] = col[1]
col_dict["fieldname"] = col[0].lower()
# dict
else:
col_dict.update(col)
columns_dict[idx] = col_dict
columns_dict[col_dict["fieldname"]] = col_dict
return columns_dict
def get_user_match_filters(doctypes, ref_doctype):
match_filters = {}

View file

@ -72,13 +72,13 @@ def send(recipients=None, sender=None, subject=None, message=None, reference_doc
unsubscribe_method, unsubscribe_params)
# add to queue
email_content = add_unsubscribe_link(email_content, email, reference_doctype, reference_name,
unsubscribe_url, unsubscribe_message)
email_content = add_unsubscribe_link(email_content, email, reference_doctype,
reference_name, unsubscribe_url, unsubscribe_message)
email_text_context += "\n" + _("Unsubscribe link: {0}").format(unsubscribe_url)
email_text_context += "\n" + _("This email was sent to {0}. To unsubscribe click on this link: {1}").format(email, unsubscribe_url)
add(email, sender, subject, email_content, email_text_context, reference_doctype, reference_name, attachments, reply_to,
cc, message_id, send_after)
add(email, sender, subject, email_content, email_text_context, reference_doctype,
reference_name, attachments, reply_to, cc, message_id, send_after)
def add(email, sender, subject, formatted, text_content=None,
reference_doctype=None, reference_name=None, attachments=None, reply_to=None,
@ -112,7 +112,8 @@ def check_bulk_limit(recipients):
# No limit for own email settings
smtp_server = SMTPServer()
if smtp_server.email_account and not getattr(smtp_server.email_account,
if smtp_server.email_account and getattr(smtp_server.email_account,
"from_site_config", False) or frappe.flags.in_test:
monthly_bulk_mail_limit = frappe.conf.get('monthly_bulk_mail_limit') or 500
@ -121,11 +122,12 @@ def check_bulk_limit(recipients):
BulkLimitCrossedError)
def add_unsubscribe_link(message, email, reference_doctype, reference_name, unsubscribe_url, unsubscribe_message):
unsubscribe_link = """<div style="padding: 7px; margin-top: 7px;">
<a href="{unsubscribe_url}" style="color: #8D99A6; text-decoration: none; font-size: 85%;" target="_blank">
{unsubscribe_message}
unsubscribe_link = """<div style="padding: 7px; text-align: center; color: #8D99A6;">
{email}. <a href="{unsubscribe_url}" style="color: #8D99A6; text-decoration: underline;
target="_blank">{unsubscribe_message}.
</a>
</div>""".format(unsubscribe_url = unsubscribe_url,
email= _("This email was sent to {0}").format(email),
unsubscribe_message = unsubscribe_message or _("Unsubscribe from this list"))
message = message.replace("<!--unsubscribe link here-->", unsubscribe_link)
@ -164,7 +166,7 @@ def unsubscribe(doctype, name, email):
return_unsubscribed_page(email, doctype, name)
def return_unsubscribed_page(email, doctype, name):
frappe.respond_as_web_page(_("Unsubscribed"), _("{0} has has left the conversation in {1} {2}").format(email, _(doctype), name))
frappe.respond_as_web_page(_("Unsubscribed"), _("{0} has left the conversation in {1} {2}").format(email, _(doctype), name))
def flush(from_test=False):
"""flush email queue, every time: called from scheduler"""
@ -176,6 +178,9 @@ def flush(from_test=False):
msgprint(_("Emails are muted"))
from_test = True
frappe.db.sql("""update `tabBulk Email` set status='Expired'
where datediff(curdate(), creation) > 3""", auto_commit=auto_commit)
for i in xrange(500):
email = frappe.db.sql("""select * from `tabBulk Email` where
status='Not Sent' and ifnull(send_after, "2000-01-01 00:00:00") < %s

View file

@ -73,13 +73,14 @@
"icon": "icon-envelope",
"idx": 1,
"in_create": 1,
"modified": "2015-04-01 10:00:20.892939",
"modified": "2015-05-21 07:26:13.627637",
"modified_by": "Administrator",
"module": "Email",
"name": "Bulk Email",
"owner": "Administrator",
"permissions": [
{
"delete": 1,
"email": 1,
"permlevel": 0,
"print": 1,

View file

@ -38,9 +38,15 @@ frappe.ui.form.on("Email Account", {
+ toTitle(frm.doc.email_id.split("@")[0].replace(/[._]/g, " ")));
}
},
enable_incoming: function(frm) {
frm.set_df_property("append_to", "reqd", frm.doc.enable_incoming);
},
onload: function(frm) {
frm.set_df_property("append_to", "only_select", true);
frm.set_query("append_to", "frappe.email.doctype.email_account.email_account.get_append_to");
},
refresh: function(frm) {
frm.set_df_property("append_to", "reqd", frm.doc.enable_incoming);
}
});

View file

@ -44,6 +44,21 @@
"search_index": 0,
"set_only_once": 0
},
{
"fieldname": "login_id_is_different",
"fieldtype": "Check",
"label": "Login Id is Different",
"permlevel": 0,
"precision": ""
},
{
"depends_on": "login_id_is_different",
"fieldname": "login_id",
"fieldtype": "Data",
"label": "Login Id",
"permlevel": 0,
"precision": ""
},
{
"allow_on_submit": 0,
"fieldname": "password",
@ -276,6 +291,15 @@
"permlevel": 0,
"precision": ""
},
{
"depends_on": "enable_outgoing",
"description": "Uses the Email ID mentioned in this Account as the Sender for all emails sent using this Account. ",
"fieldname": "always_use_account_email_id_as_sender",
"fieldtype": "Check",
"label": "Always use Account's Email ID as Sender",
"permlevel": 0,
"precision": ""
},
{
"allow_on_submit": 0,
"fieldname": "signature_section",
@ -364,6 +388,7 @@
{
"allow_on_submit": 0,
"depends_on": "enable_auto_reply",
"description": "ProTip: Add <code>Reference: {{ reference_doctype }} {{ reference_name }}</code> to send document reference",
"fieldname": "auto_reply_message",
"fieldtype": "Text Editor",
"hidden": 0,
@ -404,7 +429,7 @@
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"modified": "2015-04-21 15:31:02.269145",
"modified": "2015-07-16 10:11:06.466258",
"modified_by": "Administrator",
"module": "Email",
"name": "Email Account",

View file

@ -5,7 +5,9 @@ from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import validate_email_add, cint, get_datetime, DATE_FORMAT
from frappe.utils import validate_email_add, cint, get_datetime, DATE_FORMAT, strip, comma_or
from frappe.utils.user import is_system_user
from frappe.utils.jinja import render_template
from frappe.email.smtp import SMTPServer
from frappe.email.receive import POP3Server, Email
from poplib import error_proto
@ -30,9 +32,18 @@ class EmailAccount(Document):
if self.email_id:
validate_email_add(self.email_id, True)
if self.login_id_is_different:
if not self.login_id:
frappe.throw(_("Login Id is required"))
else:
self.login_id = None
if frappe.local.flags.in_patch or frappe.local.flags.in_test:
return
if self.enable_incoming and not self.append_to:
frappe.throw(_("Append To is mandatory for incoming mails"))
if not frappe.local.flags.in_install and not frappe.local.flags.in_patch:
if self.enable_incoming:
self.get_pop3()
@ -44,6 +55,11 @@ class EmailAccount(Document):
for e in self.get_unreplied_notification_emails():
validate_email_add(e, True)
if self.enable_incoming and self.append_to:
valid_doctypes = [d[0] for d in get_append_to()]
if self.append_to not in valid_doctypes:
frappe.throw(_("Append To can be one of {0}").format(comma_or(valid_doctypes)))
def on_update(self):
"""Check there is only one default of each type."""
self.there_must_be_only_one_default()
@ -67,7 +83,8 @@ class EmailAccount(Document):
if not self.smtp_server:
frappe.throw(_("{0} is required").format("SMTP Server"))
server = SMTPServer(login = self.email_id,
server = SMTPServer(login = getattr(self, "login_id", None) \
or self.email_id,
password = self.password,
server = self.smtp_server,
port = cint(self.smtp_port),
@ -80,7 +97,7 @@ class EmailAccount(Document):
args = {
"host": self.pop3_server,
"use_ssl": self.use_ssl,
"username": self.email_id,
"username": getattr(self, "login_id", None) or self.email_id,
"password": self.password
}
@ -171,36 +188,39 @@ class EmailAccount(Document):
sender_field = None
if in_reply_to:
if "@" in in_reply_to:
if "@{0}".format(frappe.local.site) in in_reply_to:
# reply to a communication sent from the system
in_reply_to = in_reply_to.split("@", 1)[0]
in_reply_to, domain = in_reply_to.split("@", 1)
if frappe.db.exists("Communication", in_reply_to):
parent = frappe.get_doc("Communication", in_reply_to)
if parent.reference_name:
if self.append_to:
# parent must reference only if name matches
if parent.reference_doctype==self.append_to:
# parent same as parent of last communication
parent = frappe.get_doc(parent.reference_doctype,
parent.reference_name)
else:
parent = frappe.get_doc(parent.reference_doctype,
parent.reference_name)
parent = frappe.get_doc(parent.reference_doctype,
parent.reference_name)
if not parent and self.append_to and subject_field and sender_field:
# try and match by subject and sender
# if sent by same sender with same subject,
# append it to old coversation
if not parent and self.append_to and sender_field:
if subject_field:
# try and match by subject and sender
# if sent by same sender with same subject,
# append it to old coversation
subject = strip(re.sub("^\s*(Re|RE)[^:]*:\s*", "", email.subject))
subject = re.sub("Re[^:]*:\s*", "", email.subject)
parent = frappe.db.get_all(self.append_to, filters={
sender_field: email.from_email,
subject_field: ("like", "%{0}%".format(subject)),
"creation": (">", (get_datetime() - relativedelta(days=10)).strftime(DATE_FORMAT))
}, fields="name")
parent = frappe.db.get_all(self.append_to, filters={
sender_field: email.from_email,
subject_field: ("like", "%{0}%".format(subject)),
"creation": (">", (get_datetime() - relativedelta(days=10)).strftime(DATE_FORMAT))
}, fields="name")
# match only subject field
# when the from_email is of a user in the system
# and subject is atleast 10 chars long
if not parent and len(subject) > 10 and is_system_user(email.from_email):
parent = frappe.db.get_all(self.append_to, filters={
subject_field: ("like", "%{0}%".format(subject)),
"creation": (">", (get_datetime() - relativedelta(days=10)).strftime(DATE_FORMAT))
}, fields="name")
if parent:
parent = frappe.get_doc(self.append_to, parent[0].name)
@ -218,8 +238,17 @@ class EmailAccount(Document):
parent.flags.ignore_mandatory = True
parent.insert(ignore_permissions=True)
try:
parent.insert(ignore_permissions=True)
except frappe.DuplicateEntryError:
# try and find matching parent
parent_name = frappe.db.get_value(self.append_to, {sender_field: email.from_email})
if parent_name:
parent.name = parent_name
else:
parent = None
# NOTE if parent isn't found and there's no subject match, it is likely that it is a new conversation thread and hence is_first = True
communication.is_first = True
if parent:
@ -228,14 +257,14 @@ class EmailAccount(Document):
def send_auto_reply(self, communication, email):
"""Send auto reply if set."""
if self.auto_reply_message:
if self.enable_auto_reply:
communication.set_incoming_outgoing_accounts()
frappe.sendmail(recipients = [email.from_email],
sender = self.email_id,
reply_to = communication.incoming_email_account,
subject = _("Re: ") + communication.subject,
content = self.auto_reply_message or \
content = render_template(self.auto_reply_message or "", communication.as_dict()) or \
frappe.get_template("templates/emails/auto_reply.html").render(communication.as_dict()),
reference_doctype = communication.reference_doctype,
reference_name = communication.reference_name,
@ -254,7 +283,7 @@ class EmailAccount(Document):
frappe.db.sql("update `tabCommunication` set email_account='' where email_account=%s", self.name)
@frappe.whitelist()
def get_append_to(doctype, txt, searchfield, start, page_len, filters):
def get_append_to(doctype=None, txt=None, searchfield=None, start=None, page_len=None, filters=None):
if not txt: txt = ""
return [[d] for d in frappe.get_hooks("email_append_to") if txt in d]

View file

@ -19,3 +19,5 @@ frappe.listview_settings["Email Account"] = {
}
}
}
frappe.help.youtube_id["Email Account"] = "YFYe0DrB95o";

View file

@ -90,7 +90,7 @@ class TestEmailAccount(unittest.TestCase):
# send
sent_name = make(subject = "Test", content="test content",
recipients="test_receiver@example.com", sender="test@example.com",
send_email=True)
send_email=True)["name"]
sent_mail = email.message_from_string(frappe.get_last_doc("Bulk Email").message)
with open(os.path.join(os.path.dirname(__file__), "test_mails", "reply-1.raw"), "r") as f:

View file

@ -129,7 +129,7 @@
"fieldname": "message_examples",
"fieldtype": "HTML",
"label": "Message Examples",
"options": "<h5>Message Example (Markdown)</h5>\n<pre>Transaction {{ doc.name }} has exceeded Due Date. Please take relevant action\n\n#### Details\n\nCustomer: {{ doc.customer }}\nAmount: {{ doc.total_amount }}</pre>",
"options": "<h5>Message Example</h5>\n\n<pre>\n<h3>Order Overdue</h3>\n\n<p>Transaction {{ doc.name }} has exceeded Due Date. Please take necessary action.</p>\n\n<h4>Details</h4>\n\n<ul>\n<li>Customer: {{ doc.customer }}\n<li>Amount: {{ doc.total_amount }}\n</ul>\n</pre>",
"permlevel": 0
},
{
@ -141,7 +141,7 @@
}
],
"icon": "icon-envelope",
"modified": "2015-03-25 06:20:07.472953",
"modified": "2015-07-09 00:27:00.169741",
"modified_by": "Administrator",
"module": "Email",
"name": "Email Alert",

View file

@ -5,7 +5,7 @@ from __future__ import unicode_literals
import frappe
from frappe.utils.pdf import get_pdf
from frappe.email.smtp import get_outgoing_email_account
from frappe.utils.data import get_url, scrub_urls, strip, expand_relative_urls
from frappe.utils import get_url, scrub_urls, strip, expand_relative_urls, cint
import email.utils
from markdown2 import markdown
@ -211,9 +211,11 @@ def get_formatted_html(subject, message, footer=None, print_html=None):
# imported here to avoid cyclic import
message = scrub_urls(message)
email_account = get_outgoing_email_account(False)
rendered_email = frappe.get_template("templates/emails/standard.html").render({
"content": message,
"footer": get_footer(footer),
"signature": get_signature(email_account),
"footer": get_footer(email_account, footer),
"title": subject,
"print_html": print_html,
"subject": subject
@ -221,21 +223,29 @@ def get_formatted_html(subject, message, footer=None, print_html=None):
return rendered_email
def get_footer(footer=None):
def get_signature(email_account):
if email_account and email_account.add_signature and email_account.signature:
return "<br><br>" + email_account.signature
else:
return ""
def get_footer(email_account, footer=None):
"""append a footer (signature)"""
footer = footer or ""
email_account = get_outgoing_email_account(False)
if email_account and email_account.add_signature and email_account.signature:
footer += email_account.signature
if email_account and email_account.footer:
footer += email_account.footer
else:
for default_mail_footer in frappe.get_hooks("default_mail_footer"):
footer += default_mail_footer
footer += "<!--unsubscribe link here-->"
company_address = frappe.db.get_default("email_footer_address")
if company_address:
footer += '<div style="text-align: center; color: #8d99a6">{0}</div>'\
.format(company_address.replace("\n", "<br>"))
if not cint(frappe.db.get_default("disable_standard_email_footer")):
for default_mail_footer in frappe.get_hooks("default_mail_footer"):
footer += default_mail_footer
return footer

View file

@ -44,28 +44,37 @@ class POP3Server:
self.pop.user(self.settings.username)
self.pop.pass_(self.settings.password)
# connection established!
return True
except _socket.error:
# Invalid mail server -- due to refusing connection
frappe.msgprint(_('Invalid Mail Server. Please rectify and try again.'))
raise
except poplib.error_proto, e:
if not "SYS/TEMP" in str(e):
if self.is_temporary_system_problem(e):
return False
else:
frappe.msgprint(_('Invalid User Name or Support Password. Please rectify and try again.'))
raise
raise
def get_messages(self):
"""Returns new email messages in a list."""
if not self.check_mails():
return # nothing to do
self.latest_messages = []
frappe.db.commit()
self.connect()
if not self.connect():
return []
try:
# track if errors arised
self.errors = False
self.latest_messages = []
pop_list = self.pop.list()[1]
num = num_copy = len(pop_list)
@ -138,6 +147,16 @@ class POP3Server:
def has_login_limit_exceeded(self, e):
return "-ERR Exceeded the login limit" in strip(cstr(e.message))
def is_temporary_system_problem(self, e):
messages = (
"-ERR [SYS/TEMP] Temporary system problem. Please try again later.",
"Connection timed out",
)
for message in messages:
if message in strip(cstr(e.message)):
return True
return False
def validate_pop(self, pop_meta):
# throttle based on email size
if not self.max_email_size:

View file

@ -21,8 +21,8 @@ def send(email, append_to=None):
try:
smtpserver = SMTPServer(append_to=append_to)
if hasattr(smtpserver, "always_use_login_id_as_sender") and \
cint(smtpserver.always_use_login_id_as_sender) and smtpserver.login:
if hasattr(smtpserver, "always_use_account_email_id_as_sender") and \
cint(smtpserver.always_use_account_email_id_as_sender) and smtpserver.login:
if not email.reply_to:
email.reply_to = email.sender
email.sender = smtpserver.login
@ -55,7 +55,8 @@ def get_outgoing_email_account(raise_exception_not_set=True, append_to=None):
email_account = get_default_outgoing_email_account(raise_exception_not_set=raise_exception_not_set)
if not email_account and raise_exception_not_set:
frappe.throw(_("Please setup default Email Account from Setup > Email > Email Account"))
frappe.throw(_("Please setup default Email Account from Setup > Email > Email Account"),
frappe.OutgoingEmailError)
frappe.local.outgoing_email_account[append_to or "default"] = email_account
@ -114,11 +115,13 @@ class SMTPServer:
self.email_account = get_outgoing_email_account(raise_exception_not_set=False, append_to=append_to)
if self.email_account:
self.server = self.email_account.smtp_server
self.login = self.email_account.email_id
self.login = getattr(self.email_account, "login_id", None) \
or self.email_account.email_id
self.password = self.email_account.password
self.port = self.email_account.smtp_port
self.use_ssl = self.email_account.use_tls
self.sender = self.email_account.email_id
self.always_use_account_email_id_as_sender = self.email_account.get("always_use_account_email_id_as_sender")
@property
def sess(self):

View file

@ -58,7 +58,7 @@
"icon": "icon-bitcoin",
"idx": 1,
"in_create": 0,
"modified": "2015-02-05 05:11:36.294972",
"modified": "2015-07-13 05:01:14.014983",
"modified_by": "Administrator",
"module": "Geo",
"name": "Currency",
@ -81,12 +81,38 @@
{
"apply_user_permissions": 1,
"delete": 0,
"email": 1,
"email": 0,
"permlevel": 0,
"print": 1,
"print": 0,
"read": 1,
"report": 1,
"role": "All"
"report": 0,
"role": "Accounts User"
},
{
"create": 0,
"delete": 0,
"email": 0,
"export": 0,
"permlevel": 0,
"print": 0,
"read": 1,
"report": 0,
"role": "Sales User",
"share": 0,
"write": 0
},
{
"create": 0,
"delete": 0,
"email": 0,
"export": 0,
"permlevel": 0,
"print": 0,
"read": 1,
"report": 0,
"role": "Purchase User",
"share": 0,
"write": 0
}
],
"read_only": 0

View file

@ -31,8 +31,7 @@ def logout():
def web_logout():
frappe.local.login_manager.logout()
frappe.db.commit()
frappe.respond_as_web_page("Logged Out", """<p>You have been logged out.</p>
<p><a href='index'>Back to Home</a></p>""")
frappe.respond_as_web_page("Logged Out", """<p><a href="/index" class="text-muted">Back to Home</a></p>""")
@frappe.whitelist(allow_guest=True)
def run_custom_method(doctype, name, custom_method):

View file

@ -2,12 +2,35 @@ from __future__ import unicode_literals
app_name = "frappe"
app_title = "Frappe Framework"
app_publisher = "Frappe Technologies Pvt. Ltd."
app_description = "Full Stack Web Application Framework in Python"
app_icon = "octicon octicon-circuit-board"
app_version = "5.0.9"
app_color = "orange"
app_description = """## Frappe Framework
app_email = "support@frappe.io"
Frappe is a full stack web application framework written in Python,
Javascript, HTML/CSS with MySQL as the backend. It was built for ERPNext
but is pretty generic and can be used to build database driven apps.
The key differece in Frappe compared to other frameworks is that Frappe
is that meta-data is also treated as data and is used to build front-ends
very easily. Frappe comes with a full blown admin UI called the **Desk**
that handles forms, navigation, lists, menus, permissions, file attachment
and much more out of the box.
Frappe also has a plug-in architecture that can be used to build plugins
to ERPNext.
### Links:
- Project Home: [https://frappe.io](https://frappe.io)
- Tutorial: [https://frappe.io/tutorial](https://frappe.io/tutorial)
- GitHub: [https://github.com/frappe/frappe](https://github.com/frappe/frappe)
- Forum: [https://discuss.erpnext.com](https://discuss.erpnext.com)
"""
app_icon = "octicon octicon-circuit-board"
app_version = "5.1.3"
app_color = "orange"
github_link = "https://github.com/frappe/frappe"
app_email = "info@frappe.io"
before_install = "frappe.utils.install.before_install"
after_install = "frappe.utils.install.after_install"
@ -35,8 +58,7 @@ web_include_js = [
bootstrap = "assets/frappe/css/bootstrap.css"
web_include_css = [
"assets/css/frappe-web.css",
"website_theme.css"
"assets/css/frappe-web.css"
]
website_route_rules = [
{"from_route": "/blog", "to_route": "Blog Post"},
@ -59,6 +81,8 @@ website_generators = ["Web Page", "Blog Post", "Blog Category", "Web Form"]
email_append_to = ["Event", "ToDo", "Communication"]
calendars = ["Event"]
# login
on_session_creation = [
@ -119,6 +143,7 @@ scheduler_events = {
"daily": [
"frappe.email.bulk.clear_outbox",
"frappe.desk.notifications.clear_notifications",
"frappe.core.doctype.scheduler_log.scheduler_log.set_old_logs_as_seen",
"frappe.desk.doctype.event.event.send_event_digest",
"frappe.sessions.clear_expired_sessions",
"frappe.email.doctype.email_alert.email_alert.trigger_daily_alerts",

View file

@ -231,11 +231,12 @@ def add_module_defs(app):
def remove_missing_apps():
apps = ('frappe_subscription', 'shopping_cart')
installed_apps = frappe.get_installed_apps()
installed_apps = json.loads(frappe.db.get_global("installed_apps") or "[]")
for app in apps:
if app in installed_apps:
try:
importlib.import_module(app)
except ImportError:
installed_apps.remove(app)
frappe.db.set_global("installed_apps", json.dumps(installed_apps))

View file

@ -72,111 +72,3 @@ def delete_fields(args_dict, delete=0):
", ".join(["DROP COLUMN `%s`" % f for f in fields if f in existing_fields])
frappe.db.commit()
frappe.db.sql(query)
def rename_field(doctype, old_fieldname, new_fieldname):
"""This functions assumes that doctype is already synced"""
meta = frappe.get_meta(doctype, cached=False)
new_field = meta.get_field(new_fieldname)
if not new_field:
print "rename_field: " + (new_fieldname) + " not found in " + doctype
return
if new_field.fieldtype == "Table":
# change parentfield of table mentioned in options
frappe.db.sql("""update `tab%s` set parentfield=%s
where parentfield=%s""" % (new_field.options.split("\n")[0], "%s", "%s"),
(new_fieldname, old_fieldname))
elif new_field.fieldtype not in no_value_fields:
if meta.issingle:
frappe.db.sql("""update `tabSingles` set field=%s
where doctype=%s and field=%s""",
(new_fieldname, doctype, old_fieldname))
else:
# copy field value
frappe.db.sql("""update `tab%s` set `%s`=`%s`""" % \
(doctype, new_fieldname, old_fieldname))
update_reports(doctype, old_fieldname, new_fieldname)
update_users_report_view_settings(doctype, old_fieldname, new_fieldname)
# update in property setter
frappe.db.sql("""update `tabProperty Setter` set field_name = %s
where doc_type=%s and field_name=%s""", (new_fieldname, doctype, old_fieldname))
def update_reports(doctype, old_fieldname, new_fieldname):
def _get_new_sort_by(report_dict, report, key):
sort_by = report_dict.get(key) or ""
if sort_by:
sort_by = sort_by.split(".")
if len(sort_by) > 1:
if sort_by[0]==doctype and sort_by[1]==old_fieldname:
sort_by = doctype + "." + new_fieldname
report_dict["updated"] = True
elif report.ref_doctype == doctype and sort_by[0]==old_fieldname:
sort_by = doctype + "." + new_fieldname
report_dict["updated"] = True
if isinstance(sort_by, list):
sort_by = '.'.join(sort_by)
return sort_by
reports = frappe.db.sql("""select name, ref_doctype, json from tabReport
where report_type = 'Report Builder' and ifnull(is_standard, 'No') = 'No'
and json like %s and json like %s""",
('%%%s%%' % old_fieldname , '%%%s%%' % doctype), as_dict=True)
for r in reports:
report_dict = json.loads(r.json)
# update filters
new_filters = []
for f in report_dict.get("filters"):
if f[0] == doctype and f[1] == old_fieldname:
new_filters.append([doctype, new_fieldname, f[2], f[3]])
report_dict["updated"] = True
else:
new_filters.append(f)
# update columns
new_columns = []
for c in report_dict.get("columns"):
if c[0] == old_fieldname and c[1] == doctype:
new_columns.append([new_fieldname, doctype])
report_dict["updated"] = True
else:
new_columns.append(c)
# update sort by
new_sort_by = _get_new_sort_by(report_dict, r, "sort_by")
new_sort_by_next = _get_new_sort_by(report_dict, r, "sort_by_next")
if report_dict.get("updated"):
new_val = json.dumps({
"filters": new_filters,
"columns": new_columns,
"sort_by": new_sort_by,
"sort_order": report_dict.get("sort_order"),
"sort_by_next": new_sort_by_next,
"sort_order_next": report_dict.get("sort_order_next")
})
frappe.db.sql("""update `tabReport` set `json`=%s where name=%s""", (new_val, r.name))
def update_users_report_view_settings(doctype, ref_fieldname, new_fieldname):
user_report_cols = frappe.db.sql("""select defkey, defvalue from `tabDefaultValue` where
defkey like '_list_settings:%'""")
for key, value in user_report_cols:
new_columns = []
columns_modified = False
for field, field_doctype in json.loads(value):
if field == ref_fieldname and field_doctype == doctype:
new_columns.append([new_fieldname, field_doctype])
columns_modified=True
else:
new_columns.append([field, field_doctype])
if columns_modified:
frappe.db.sql("""update `tabDefaultValue` set defvalue=%s
where defkey=%s""" % ('%s', '%s'), (json.dumps(new_columns), key))

View file

@ -261,7 +261,7 @@ class BaseDocument(object):
return
type, value, traceback = sys.exc_info()
frappe.msgprint(_("Duplicate name {0} {1}").format(self.doctype, self.name))
raise frappe.NameError, (self.doctype, self.name, e), traceback
raise frappe.DuplicateEntryError, (self.doctype, self.name, e), traceback
else:
raise
@ -453,7 +453,7 @@ class BaseDocument(object):
return self._precision[cache_key][fieldname]
def get_formatted(self, fieldname, doc=None, currency=None):
def get_formatted(self, fieldname, doc=None, currency=None, absolute_value=False):
from frappe.utils.formatters import format_value
df = self.meta.get_field(fieldname)
@ -461,7 +461,10 @@ class BaseDocument(object):
from frappe.model.meta import get_default_df
df = get_default_df(fieldname)
return format_value(self.get(fieldname), df=df, doc=doc or self, currency=currency)
val = self.get(fieldname)
if absolute_value and isinstance(val, (int, float)):
val = abs(self.get(fieldname))
return format_value(val, df=df, doc=doc or self, currency=currency)
def is_print_hide(self, fieldname, df=None, for_print=True):
"""Returns true if fieldname is to be hidden for print.
@ -524,7 +527,7 @@ class BaseDocument(object):
def cast(self, val, df):
if df.fieldtype in ("Currency", "Float", "Percent"):
val = flt(val, self.precision(df.fieldname))
val = flt(val)
elif df.fieldtype in ("Int", "Check"):
val = cint(val)

View file

@ -270,7 +270,6 @@ class DatabaseQuery(object):
"""add match conditions if applicable"""
self.match_filters = []
self.match_conditions = []
only_if_shared = False
if not self.tables: self.extract_tables()
@ -283,7 +282,7 @@ class DatabaseQuery(object):
if not meta.istable and not role_permissions.get("read") and not self.flags.ignore_permissions:
only_if_shared = True
if not self.shared:
frappe.throw(_("No permission to read {0}").format(self.doctype))
frappe.throw(_("No permission to read {0}").format(self.doctype), frappe.PermissionError)
else:
self.conditions.append(self.get_share_condition())
@ -295,9 +294,9 @@ class DatabaseQuery(object):
self.add_user_permissions(user_permissions,
user_permission_doctypes=role_permissions.get("user_permission_doctypes").get("read"))
# share is an OR condition, if there is a role permission
if not only_if_shared and self.shared:
self.or_conditions.append(self.get_share_condition())
if role_permissions.get("if_owner", {}).get("read"):
self.match_conditions.append("`tab{0}`.owner = '{1}'".format(self.doctype,
frappe.db.escape(frappe.session.user)))
if as_condition:
conditions = ""
@ -309,6 +308,11 @@ class DatabaseQuery(object):
if doctype_conditions:
conditions += (' and ' + doctype_conditions) if conditions else doctype_conditions
# share is an OR condition, if there is a role permission
if not only_if_shared and self.shared and conditions:
conditions = "({conditions}) or ({shared_condition})".format(
conditions=conditions, shared_condition=self.get_share_condition())
return conditions
else:

View file

@ -234,7 +234,14 @@ class DbTable:
query.append('alter column `{}` set default {}'.format(col.fieldname, col_default))
if query:
frappe.db.sql("alter table `{}` {}".format(self.name, ", ".join(query)))
try:
frappe.db.sql("alter table `{}` {}".format(self.name, ", ".join(query)))
except Exception, e:
# sanitize
if e.args[0]==1060:
frappe.throw(str(e))
else:
raise e
class DbColumn:
def __init__(self, table, fieldname, fieldtype, length, default,
@ -302,17 +309,34 @@ class DbColumn:
def default_changed(self, current_def):
if "decimal" in current_def['type']:
try:
if current_def['default'] in ("", None) and self.default in ("", None):
# both none, empty
return False
else:
return float(current_def['default'])!=float(self.default)
except TypeError:
return True
return self.default_changed_for_decimal(current_def)
else:
return current_def['default'] != self.default
def default_changed_for_decimal(self, current_def):
try:
if current_def['default'] in ("", None) and self.default in ("", None):
# both none, empty
return False
elif current_def['default'] in ("", None):
try:
# check if new default value is valid
float(self.default)
return True
except ValueError:
return False
elif self.default in ("", None):
# new default value is empty
return True
else:
# NOTE float() raise ValueError when "" or None is passed
return float(current_def['default'])!=float(self.default)
except TypeError:
return True
class DbManager:
"""
Basically, a wrapper for oft-used mysql commands. like show tables,databases, variables etc...
@ -452,4 +476,3 @@ def add_column(doctype, column_name, fieldtype, precision=None):
frappe.db.commit()
frappe.db.sql("alter table `tab%s` add column %s %s" % (doctype,
column_name, get_definition(fieldtype, precision)))

View file

@ -147,7 +147,9 @@ class Document(BaseDocument):
def raise_no_permission_to(self, perm_type):
"""Raise `frappe.PermissionError`."""
raise frappe.PermissionError("No permission to {} {} {}".format(perm_type, self.doctype, self.name or ""))
msg = _("No permission to {0} {1} {2}".format(perm_type, self.doctype, self.name or ""))
frappe.msgprint(msg)
raise frappe.PermissionError(msg)
def insert(self, ignore_permissions=None):
"""Insert the document in the database (as a new document).
@ -560,11 +562,13 @@ class Document(BaseDocument):
elif self._action=="submit":
self.run_method("on_update")
self.run_method("on_submit")
self.add_comment("Submitted")
if not self.flags.ignore_submit_comment:
self.add_comment("Submitted")
elif self._action=="cancel":
self.run_method("on_cancel")
self.check_no_back_links_exist()
self.add_comment("Cancelled")
if not self.flags.ignore_submit_comment:
self.add_comment("Cancelled")
elif self._action=="update_after_submit":
self.run_method("on_update_after_submit")

View file

@ -33,6 +33,17 @@ def get_mapped_doc(from_doctype, from_docname, table_maps, target_doc=None,
for df in source_doc.meta.get_table_fields():
source_child_doctype = df.options
table_map = table_maps.get(source_child_doctype)
# if table_map isn't explicitly specified check if both source and target have the same fieldname and same table options and both of them don't have no_copy
if not table_map:
target_df = target_doc.meta.get_field(df.fieldname)
if target_df:
target_child_doctype = target_df.options
if target_df and target_child_doctype==source_child_doctype and not df.no_copy and not target_df.no_copy:
table_map = {
"doctype": target_child_doctype
}
if table_map:
for source_d in source_doc.get(df.fieldname):
if "condition" in table_map:

View file

@ -114,6 +114,9 @@ class Meta(Document):
list_fields.append(self.title_field)
return list_fields
def get_title_field(self):
return self.title_field or "name"
def process(self):
# don't process for special doctypes
# prevent's circular dependency

View file

@ -177,7 +177,7 @@ def append_number_if_name_exists(doc):
if frappe.db.exists(doc.doctype, doc.name):
last = frappe.db.sql("""select name from `tab{}`
where name regexp '{}-[[:digit:]]+'
order by name desc limit 1""".format(doc.doctype, doc.name))
order by length(name) desc, name desc limit 1""".format(doc.doctype, doc.name))
if last:
count = str(cint(last[0][0].rsplit("-", 1)[1]) + 1)

View file

@ -0,0 +1,116 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
from __future__ import unicode_literals
import frappe
import json
from frappe.model import no_value_fields
def rename_field(doctype, old_fieldname, new_fieldname):
"""This functions assumes that doctype is already synced"""
meta = frappe.get_meta(doctype, cached=False)
new_field = meta.get_field(new_fieldname)
if not new_field:
print "rename_field: " + (new_fieldname) + " not found in " + doctype
return
if new_field.fieldtype == "Table":
# change parentfield of table mentioned in options
frappe.db.sql("""update `tab%s` set parentfield=%s
where parentfield=%s""" % (new_field.options.split("\n")[0], "%s", "%s"),
(new_fieldname, old_fieldname))
elif new_field.fieldtype not in no_value_fields:
if meta.issingle:
frappe.db.sql("""update `tabSingles` set field=%s
where doctype=%s and field=%s""",
(new_fieldname, doctype, old_fieldname))
else:
# copy field value
frappe.db.sql("""update `tab%s` set `%s`=`%s`""" % \
(doctype, new_fieldname, old_fieldname))
update_reports(doctype, old_fieldname, new_fieldname)
update_users_report_view_settings(doctype, old_fieldname, new_fieldname)
# update in property setter
frappe.db.sql("""update `tabProperty Setter` set field_name = %s
where doc_type=%s and field_name=%s""", (new_fieldname, doctype, old_fieldname))
def update_reports(doctype, old_fieldname, new_fieldname):
def _get_new_sort_by(report_dict, report, key):
sort_by = report_dict.get(key) or ""
if sort_by:
sort_by = sort_by.split(".")
if len(sort_by) > 1:
if sort_by[0]==doctype and sort_by[1]==old_fieldname:
sort_by = doctype + "." + new_fieldname
report_dict["updated"] = True
elif report.ref_doctype == doctype and sort_by[0]==old_fieldname:
sort_by = doctype + "." + new_fieldname
report_dict["updated"] = True
if isinstance(sort_by, list):
sort_by = '.'.join(sort_by)
return sort_by
reports = frappe.db.sql("""select name, ref_doctype, json from tabReport
where report_type = 'Report Builder' and ifnull(is_standard, 'No') = 'No'
and json like %s and json like %s""",
('%%%s%%' % old_fieldname , '%%%s%%' % doctype), as_dict=True)
for r in reports:
report_dict = json.loads(r.json)
# update filters
new_filters = []
for f in report_dict.get("filters"):
if f and len(f) > 1 and f[0] == doctype and f[1] == old_fieldname:
new_filters.append([doctype, new_fieldname, f[2], f[3]])
report_dict["updated"] = True
else:
new_filters.append(f)
# update columns
new_columns = []
for c in report_dict.get("columns"):
if c and len(c) > 1 and c[0] == old_fieldname and c[1] == doctype:
new_columns.append([new_fieldname, doctype])
report_dict["updated"] = True
else:
new_columns.append(c)
# update sort by
new_sort_by = _get_new_sort_by(report_dict, r, "sort_by")
new_sort_by_next = _get_new_sort_by(report_dict, r, "sort_by_next")
if report_dict.get("updated"):
new_val = json.dumps({
"filters": new_filters,
"columns": new_columns,
"sort_by": new_sort_by,
"sort_order": report_dict.get("sort_order"),
"sort_by_next": new_sort_by_next,
"sort_order_next": report_dict.get("sort_order_next")
})
frappe.db.sql("""update `tabReport` set `json`=%s where name=%s""", (new_val, r.name))
def update_users_report_view_settings(doctype, ref_fieldname, new_fieldname):
user_report_cols = frappe.db.sql("""select defkey, defvalue from `tabDefaultValue` where
defkey like '_list_settings:%'""")
for key, value in user_report_cols:
new_columns = []
columns_modified = False
for field, field_doctype in json.loads(value):
if field == ref_fieldname and field_doctype == doctype:
new_columns.append([new_fieldname, field_doctype])
columns_modified=True
else:
new_columns.append([field, field_doctype])
if columns_modified:
frappe.db.sql("""update `tabDefaultValue` set defvalue=%s
where defkey=%s""" % ('%s', '%s'), (json.dumps(new_columns), key))

View file

@ -7,19 +7,19 @@ import frappe, os, json
from frappe.modules import get_module_path, scrub_dt_dn
from frappe.utils import get_datetime_str
def import_files(module, dt=None, dn=None, force=False):
def import_files(module, dt=None, dn=None, force=False, pre_process=None):
if type(module) is list:
out = []
for m in module:
out.append(import_file(m[0], m[1], m[2], force=force))
out.append(import_file(m[0], m[1], m[2], force=force, pre_process=pre_process))
return out
else:
return import_file(module, dt, dn, force=force)
return import_file(module, dt, dn, force=force, pre_process=pre_process)
def import_file(module, dt, dn, force=False):
def import_file(module, dt, dn, force=False, pre_process=None):
"""Sync a file from txt if modifed, return false if not updated"""
path = get_file_path(module, dt, dn)
ret = import_file_by_path(path, force)
ret = import_file_by_path(path, force, pre_process=pre_process)
return ret
def get_file_path(module, dt, dn):
@ -30,7 +30,7 @@ def get_file_path(module, dt, dn):
return path
def import_file_by_path(path, force=False, data_import=False):
def import_file_by_path(path, force=False, data_import=False, pre_process=None):
frappe.flags.in_import = True
try:
docs = read_doc_from_file(path)
@ -51,7 +51,7 @@ def import_file_by_path(path, force=False, data_import=False):
original_modified = doc.get("modified")
import_doc(doc, force=force, data_import=data_import)
import_doc(doc, force=force, data_import=data_import, pre_process=pre_process)
if original_modified:
# since there is a new timestamp on the file, update timestamp in
@ -87,10 +87,12 @@ ignore_values = {
ignore_doctypes = ["Page Role", "DocPerm"]
def import_doc(docdict, force=False, data_import=False):
def import_doc(docdict, force=False, data_import=False, pre_process=None):
frappe.flags.in_import = True
docdict["__islocal"] = 1
doc = frappe.get_doc(docdict)
if pre_process:
pre_process(doc)
ignore = []

View file

@ -2,7 +2,7 @@ execute:frappe.db.sql("""update `tabPatch Log` set patch=replace(patch, '.4_0.',
frappe.patches.v5_0.convert_to_barracuda_and_utf8mb4
execute:frappe.reload_doc('core', 'doctype', 'doctype', force=True) #2014-01-24
execute:frappe.reload_doc('core', 'doctype', 'docfield', force=True) #2015-05-15
execute:frappe.reload_doc('core', 'doctype', 'docperm') #2014-06-04
execute:frappe.reload_doc('core', 'doctype', 'docperm') #2014-06-24
execute:frappe.reload_doc('core', 'doctype', 'page') #2013-13-26
execute:frappe.reload_doc('core', 'doctype', 'report') #2014-06-03
execute:frappe.reload_doc('core', 'doctype', 'version') #2014-02-21
@ -66,6 +66,7 @@ frappe.patches.v5_0.clear_website_group_and_notifications
execute:frappe.db.sql("""update tabComment set comment = substr(comment, 6, locate(":", comment)-6) where comment_type in ("Assigned", "Assignment Completed")""")
frappe.patches.v5_0.fix_feed
frappe.patches.v5_0.update_shared
execute:frappe.reload_doc("core", "doctype", "docshare") #2015-07-21
frappe.patches.v5_0.bookmarks_to_stars
frappe.patches.v5_0.style_settings_to_website_theme
frappe.patches.v5_0.rename_ref_type_fieldnames
@ -79,3 +80,6 @@ execute:frappe.db.sql("update tabUser set new_password='' where ifnull(new_passw
frappe.patches.v5_0.fix_text_editor_file_urls
execute:frappe.db.sql("update `tabComment` set comment_type='Comment' where comment_doctype='Blog Post' and ifnull(comment_type, '')=''")
frappe.patches.v5_0.modify_session
frappe.patches.v5_0.expire_old_scheduler_logs
execute:frappe.permissions.reset_perms("DocType")
execute:frappe.db.sql("delete from `tabProperty Setter` where `property` = 'idx'")

View file

@ -1,7 +1,7 @@
from __future__ import unicode_literals
import frappe
from frappe.model import rename_field
from frappe.model.utils.rename_field import rename_field
from frappe.model.meta import get_table_columns
def execute():

View file

@ -1,7 +1,7 @@
from __future__ import unicode_literals
import frappe
from frappe.model import rename_field
from frappe.model.utils.rename_field import rename_field
def execute():
tables = frappe.db.sql_list("show tables")

View file

@ -25,4 +25,8 @@ def execute():
continue
if frappe.db.exists(doctype, docname):
if (doctype=="DocType"
or int(frappe.db.get_value("DocType", doctype, "issingle") or 0)
or not frappe.db.table_exists(doctype)):
continue
_toggle_star(doctype, docname, add="Yes", user=username)

View file

@ -0,0 +1,7 @@
import frappe
def execute():
frappe.reload_doctype("Scheduler Log")
from frappe.core.doctype.scheduler_log.scheduler_log import set_old_logs_as_seen
set_old_logs_as_seen()

View file

@ -1,4 +1,5 @@
import frappe
def execute():
frappe.db.sql("alter table tabSessions add column `device` varchar(255) default 'desktop'")
if "device" not in frappe.db.get_table_columns("Sessions"):
frappe.db.sql("alter table tabSessions add column `device` varchar(255) default 'desktop'")

View file

@ -2,7 +2,7 @@
# License: GNU General Public License v3. See license.txt
import frappe
from frappe.model import rename_field
from frappe.model.utils.rename_field import rename_field
from frappe.modules import scrub, get_doctype_module
rename_map = {

View file

@ -17,7 +17,11 @@ def check_admin_or_system_manager(user=None):
frappe.throw(_("Not permitted"), frappe.PermissionError)
def has_permission(doctype, ptype="read", doc=None, verbose=False, user=None):
"""check if user has permission"""
"""Returns True if user has permission `ptype` for given `doctype`.
If `doc` is passed, it also checks user, share and owner permissions.
Note: if Table DocType is passed, it always returns True.
"""
if not user: user = frappe.session.user
if frappe.is_table(doctype):
@ -39,13 +43,17 @@ def has_permission(doctype, ptype="read", doc=None, verbose=False, user=None):
return True
def false_if_not_shared():
if ptype in ("read", "write", "share"):
shared = frappe.share.get_shared(doctype, user, [ptype])
if ptype in ("read", "write", "share", "email", "print"):
if doc:
doc_name = doc if isinstance(doc, basestring) else doc.name
shared = frappe.share.get_shared(doctype, user,
["read" if ptype in ("email", "print") else ptype])
if doc_name in shared:
if verbose: print "Shared"
return True
if ptype in ("read", "write", "share") or meta.permissions[0].get(ptype):
return True
else:
if verbose: print "Has a shared document"
return True
@ -61,6 +69,11 @@ def has_permission(doctype, ptype="read", doc=None, verbose=False, user=None):
if isinstance(doc, basestring):
doc = frappe.get_doc(meta.name, doc)
# if owner match, then return True
if doc.owner == frappe.session.user and role_permissions["if_owner"].get(ptype) and ptype!="create":
return True
# check if user permission
if role_permissions["apply_user_permissions"].get(ptype):
if not user_has_permission(doc, verbose=verbose, user=user,
user_permission_doctypes=role_permissions.get("user_permission_doctypes", {}).get(ptype) or []):
@ -76,6 +89,7 @@ def has_permission(doctype, ptype="read", doc=None, verbose=False, user=None):
return True
def get_doc_permissions(doc, verbose=False, user=None):
"""Returns a dict of evaluated permissions for given `doc` like `{"read":1, "write":1}`"""
if not user: user = frappe.session.user
if frappe.is_table(doc.doctype):
@ -98,23 +112,62 @@ def get_doc_permissions(doc, verbose=False, user=None):
user_permission_doctypes=role_permissions.get("user_permission_doctypes", {}).get(ptype) or []):
role_permissions[ptype] = 0
# update share permissions
role_permissions.update(frappe.db.get_value("DocShare",
{"share_doctype": doc.doctype, "share_name": doc.name, "user": user},
["read", "write", "share"], as_dict=True) or {})
# apply owner permissions on top of existing permissions
if doc.owner == frappe.session.user:
role_permissions.update(role_permissions.if_owner)
update_share_permissions(role_permissions, doc, user)
return role_permissions
def update_share_permissions(role_permissions, doc, user):
"""Updates share permissions on `role_permissions` for given doc, if shared"""
share_ptypes = ("read", "write", "share")
permissions_by_share = frappe.db.get_value("DocShare",
{"share_doctype": doc.doctype, "share_name": doc.name, "user": user},
share_ptypes, as_dict=True)
if permissions_by_share:
for ptype in share_ptypes:
if ptype:
role_permissions[ptype] = 1
def get_role_permissions(meta, user=None, verbose=False):
"""Returns dict of evaluated role permissions like `{"read": True, "write":False}`
If user permissions are applicable, it adds a dict of user permissions like
{
// user permissions will apply on these rights
"apply_user_permissions": {"read": 1, "write": 1},
// doctypes that will be applicable for each right
"user_permission_doctypes": {
"read": [
// AND between "DocType 1" and "DocType 2"
["DocType 1", "DocType 2"],
// OR
["DocType 3"]
]
}
"if_owner": {"read": 1, "write": 1}
}
"""
if not user: user = frappe.session.user
cache_key = (meta.name, user)
if not frappe.local.role_permissions.get(cache_key):
perms = frappe._dict({ "apply_user_permissions": {}, "user_permission_doctypes": {} })
perms = frappe._dict({ "apply_user_permissions": {}, "user_permission_doctypes": {}, "if_owner": {} })
user_roles = frappe.get_roles(user)
for p in meta.permissions:
if cint(p.permlevel)==0 and (p.role in user_roles):
# apply only for level 0
for ptype in rights:
perms[ptype] = perms.get(ptype, 0) or cint(p.get(ptype))
@ -122,6 +175,10 @@ def get_role_permissions(meta, user=None, verbose=False):
perms["apply_user_permissions"][ptype] = (perms["apply_user_permissions"].get(ptype, 1)
and p.get("apply_user_permissions"))
# build if_owner dict if applicable for this right
if p.if_owner and p.get(ptype):
perms["if_owner"][ptype] = 1
if p.apply_user_permissions:
if p.user_permission_doctypes:
# set user_permission_doctypes in perms

View file

@ -19,12 +19,10 @@
"search_index": 0
},
{
"depends_on": "eval:!doc.custom_format",
"fieldname": "edit_format",
"fieldtype": "Button",
"label": "Edit Format",
"permlevel": 0,
"precision": ""
"fieldname": "disabled",
"fieldtype": "Check",
"label": "Disabled",
"permlevel": 0
},
{
"fieldname": "column_break_3",
@ -51,17 +49,6 @@
"reqd": 1,
"search_index": 1
},
{
"fieldname": "disabled",
"fieldtype": "Check",
"label": "Disabled",
"permlevel": 0
},
{
"fieldname": "section_break_6",
"fieldtype": "Section Break",
"permlevel": 0
},
{
"fieldname": "custom_format",
"fieldtype": "Check",
@ -69,6 +56,12 @@
"permlevel": 0,
"precision": ""
},
{
"depends_on": "custom_format",
"fieldname": "section_break_6",
"fieldtype": "Section Break",
"permlevel": 0
},
{
"default": "Server",
"depends_on": "custom_format",
@ -99,6 +92,44 @@
"reqd": 0,
"search_index": 0
},
{
"depends_on": "eval:!doc.custom_format",
"fieldname": "section_break_9",
"fieldtype": "Section Break",
"permlevel": 0,
"precision": ""
},
{
"depends_on": "eval:!doc.custom_format",
"fieldname": "edit_format",
"fieldtype": "Button",
"label": "Edit Format",
"permlevel": 0,
"precision": ""
},
{
"fieldname": "column_break_11",
"fieldtype": "Column Break",
"permlevel": 0,
"precision": ""
},
{
"default": "Default",
"depends_on": "eval:!doc.custom_format",
"fieldname": "font",
"fieldtype": "Select",
"label": "Font",
"options": "Default\nArial\nHelvetica\nVerdana\nMonospace",
"permlevel": 0,
"precision": ""
},
{
"depends_on": "custom_format",
"fieldname": "section_break_13",
"fieldtype": "Section Break",
"permlevel": 0,
"precision": ""
},
{
"depends_on": "custom_format",
"fieldname": "print_format_help",
@ -134,7 +165,7 @@
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2015-02-05 05:11:42.667447",
"modified": "2015-07-15 08:01:06.284031",
"modified_by": "Administrator",
"module": "Print",
"name": "Print Format",

View file

@ -42,9 +42,13 @@
"permlevel": 0
},
{
"fieldname": "column_break_6",
"fieldtype": "Column Break",
"permlevel": 0
"default": "Default",
"fieldname": "font",
"fieldtype": "Select",
"label": "Font",
"options": "Default\nArial\nHelvetica\nVerdana\nMonospace",
"permlevel": 0,
"precision": ""
},
{
"description": "In points. Default is 9.",
@ -53,6 +57,11 @@
"label": "Font Size",
"permlevel": 0
},
{
"fieldname": "column_break_6",
"fieldtype": "Column Break",
"permlevel": 0
},
{
"default": "1",
"description": "Print with Letterhead, unless unchecked in a particular Document",
@ -76,7 +85,7 @@
],
"icon": "icon-cog",
"issingle": 1,
"modified": "2015-03-25 07:10:38.893958",
"modified": "2015-07-15 08:03:23.743143",
"modified_by": "Administrator",
"module": "Print",
"name": "Print Settings",

View file

@ -152,6 +152,7 @@ frappe.PrintFormatBuilder = Class.extend({
me.page.set_primary_action(__("Save"), function() {
me.save_print_format();
});
me.page.clear_menu();
me.page.add_menu_item(__("Start new Format"), function() {
me.print_format = null;
me.refresh();

View file

@ -47,8 +47,8 @@
"public/js/lib/notify.js",
"public/js/lib/bootstrap.min.js",
"public/js/lib/nprogress.js",
"public/js/lib/moment/moment.min.js",
"public/js/lib/moment/moment-timezone.min.js",
"public/js/lib/moment/moment-with-locales.min.js",
"public/js/lib/moment/moment-timezone-with-data.min.js",
"public/js/frappe/provide.js",
"public/js/frappe/class.js",
@ -95,6 +95,7 @@
"public/js/frappe/misc/tools.js",
"public/js/frappe/misc/datetime.js",
"public/js/frappe/misc/number_format.js",
"public/js/frappe/misc/help.js",
"public/js/frappe/ui/upload.html",
"public/js/frappe/upload.js",

View file

@ -12,6 +12,7 @@
}
.avatar-empty {
border: 1px dashed #d1d8dd;
border-radius: 4px;
}
.avatar-small {
margin-right: 5px;

View file

@ -361,6 +361,11 @@ ul.linked-with-list li {
padding: 7px;
font-size: 12px;
}
@media (min-width: 768px) {
.video-modal {
width: 700px;
}
}
/* z-index hack */
@media (min-width: 768px) {
.hidden-xs-inline {
@ -389,6 +394,18 @@ ul.linked-with-list li {
.modal-title {
margin-top: 5px;
}
.form-control {
position: relative;
}
.link-field.ui-front {
z-index: inherit;
}
.modal .hasDatepicker {
z-index: 1140;
}
.modal .link-field .ui-autocomplete {
z-index: 1141;
}
.form-group {
margin-bottom: 7px;
}

Some files were not shown because too many files have changed in this diff Show more