Merge branch 'develop' of https://github.com/frappe/frappe into page-builder-tailwind
This commit is contained in:
commit
caa068932c
274 changed files with 6544 additions and 4062 deletions
|
|
@ -5,7 +5,7 @@
|
|||
"es6": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 8,
|
||||
"ecmaVersion": 9,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"extends": "eslint:recommended",
|
||||
|
|
@ -78,6 +78,8 @@
|
|||
"has_common": true,
|
||||
"has_words": true,
|
||||
"validate_email": true,
|
||||
"validate_name": true,
|
||||
"validate_phone": true,
|
||||
"get_number_format": true,
|
||||
"format_number": true,
|
||||
"format_currency": true,
|
||||
|
|
|
|||
28
.github/frappe_linter/translation.py
vendored
Normal file
28
.github/frappe_linter/translation.py
vendored
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import re
|
||||
import sys
|
||||
|
||||
errors_encounter = 0
|
||||
pattern = re.compile(r"_\(([\"']{,3})(?P<message>((?!\1).)*)\1(\s*,\s*context\s*=\s*([\"'])(?P<py_context>((?!\5).)*)\5)*(\s*,\s*(.)*?\s*(,\s*([\"'])(?P<js_context>((?!\11).)*)\11)*)*\)")
|
||||
start_pattern = re.compile(r"_{1,2}\([\"']{1,3}")
|
||||
|
||||
# skip first argument
|
||||
files = sys.argv[1:]
|
||||
for _file in files:
|
||||
if not _file.endswith(('.py', '.js')):
|
||||
continue
|
||||
with open(_file, 'r') as f:
|
||||
print(f'Checking: {_file}')
|
||||
for num, line in enumerate(f, 1):
|
||||
all_matches = start_pattern.finditer(line)
|
||||
if all_matches:
|
||||
for match in all_matches:
|
||||
verify = pattern.search(line)
|
||||
if not verify:
|
||||
errors_encounter += 1
|
||||
print(f'A syntax error has been discovered at line number: {num}')
|
||||
print(f'Syntax error occurred with: {line}')
|
||||
if errors_encounter > 0:
|
||||
print('You can visit "https://frappe.io/docs/user/en/translations" to resolve this error.')
|
||||
assert 1+1 == 3
|
||||
else:
|
||||
print('Good To Go!')
|
||||
16
.github/workflows/backport.yml
vendored
16
.github/workflows/backport.yml
vendored
|
|
@ -1,16 +0,0 @@
|
|||
name: Backport
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- closed
|
||||
- labeled
|
||||
|
||||
jobs:
|
||||
backport:
|
||||
runs-on: ubuntu-18.04
|
||||
name: Backport
|
||||
steps:
|
||||
- name: Backport
|
||||
uses: tibdex/backport@v1
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
22
.github/workflows/translation_linter.yml
vendored
Normal file
22
.github/workflows/translation_linter.yml
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
name: Frappe Linter
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- develop
|
||||
- version-12-hotfix
|
||||
- version-11-hotfix
|
||||
jobs:
|
||||
check_translation:
|
||||
name: Translation Syntax Check
|
||||
runs-on: ubuntu-18.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Setup python3
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: 3.6
|
||||
- name: Validating Translation Syntax
|
||||
run: |
|
||||
git fetch origin $GITHUB_BASE_REF:$GITHUB_BASE_REF -q
|
||||
files=$(git diff --name-only $GITHUB_BASE_REF)
|
||||
python $GITHUB_WORKSPACE/.github/frappe_linter/translation.py $files
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
pull_request_rules:
|
||||
- name: Automatic merge on CI success and review
|
||||
conditions:
|
||||
- status-success=Codacy/PR Quality Review
|
||||
- status-success=Sider
|
||||
- status-success=Semantic Pull Request
|
||||
- status-success=Travis CI - Pull Request
|
||||
- status-success=security/snyk - package.json (frappe)
|
||||
|
|
@ -14,7 +14,7 @@ pull_request_rules:
|
|||
method: merge
|
||||
- name: Automatic squash on CI success and review
|
||||
conditions:
|
||||
- status-success=Codacy/PR Quality Review
|
||||
- status-success=Sider
|
||||
- status-success=Semantic Pull Request
|
||||
- status-success=Travis CI - Pull Request
|
||||
- status-success=security/snyk - package.json (frappe)
|
||||
|
|
|
|||
18
CODEOWNERS
Normal file
18
CODEOWNERS
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# Each line is a file pattern followed by one or more owners.
|
||||
|
||||
# These owners will be the default owners for everything in
|
||||
# the repo. Unless a later match takes precedence,
|
||||
|
||||
* @frappe/frappe-review-team
|
||||
website/ @scmmishra
|
||||
web_form/ @scmmishra
|
||||
templates/ @scmmishra
|
||||
www/ @scmmishra
|
||||
integrations/ @Mangesh-Khairnar
|
||||
patches/ @sahil28297
|
||||
dashboard/ @prssanna
|
||||
email/ @Thunderbottom
|
||||
event_streaming/ @ruchamahabal
|
||||
data_import* @netchampfaris
|
||||
core/ @surajshetty3416
|
||||
requirements.txt @gavindsouza
|
||||
|
|
@ -34,6 +34,7 @@ Full-stack web application framework that uses Python and MariaDB on the server
|
|||
|
||||
### Table of Contents
|
||||
* [Installation](#installation)
|
||||
* [Documentation](https://frappe.io/docs)
|
||||
* [License](#license)
|
||||
|
||||
### Installation
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
{
|
||||
"baseUrl": "http://test_site_ui:8000",
|
||||
"projectId": "92odwv",
|
||||
"adminPassword": "admin"
|
||||
"adminPassword": "admin",
|
||||
"defaultCommandTimeout": 10000,
|
||||
"pageLoadTimeout": 15000
|
||||
}
|
||||
|
|
|
|||
|
|
@ -587,6 +587,7 @@ def clear_cache(user=None, doctype=None):
|
|||
else: # everything
|
||||
from frappe import translate
|
||||
frappe.cache_manager.clear_user_cache()
|
||||
frappe.cache_manager.clear_domain_cache()
|
||||
translate.clear_cache()
|
||||
reset_metadata_version()
|
||||
local.cache = {}
|
||||
|
|
@ -832,6 +833,8 @@ def rename_doc(*args, **kwargs):
|
|||
"""
|
||||
kwargs.pop('cmd', None)
|
||||
kwargs.pop('ignore_permissions', None)
|
||||
kwargs.pop('cmd', None)
|
||||
|
||||
from frappe.model.rename_doc import rename_doc
|
||||
return rename_doc(*args, **kwargs)
|
||||
|
||||
|
|
|
|||
129
frappe/api.py
129
frappe/api.py
|
|
@ -2,14 +2,17 @@
|
|||
# MIT License. See license.txt
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import json
|
||||
import frappe
|
||||
import frappe.handler
|
||||
import frappe.client
|
||||
from frappe.utils.response import build_response
|
||||
from frappe import _
|
||||
from six.moves.urllib.parse import urlparse, urlencode
|
||||
import base64
|
||||
import binascii
|
||||
import json
|
||||
|
||||
from six.moves.urllib.parse import urlencode, urlparse
|
||||
|
||||
import frappe
|
||||
import frappe.client
|
||||
import frappe.handler
|
||||
from frappe import _
|
||||
from frappe.utils.response import build_response
|
||||
|
||||
def handle():
|
||||
"""
|
||||
|
|
@ -35,8 +38,8 @@ def handle():
|
|||
`/api/resource/{doctype}/{name}?run_method={method}` will run a whitelisted controller method
|
||||
"""
|
||||
|
||||
validate_oauth()
|
||||
validate_auth_via_api_keys()
|
||||
|
||||
validate_auth()
|
||||
|
||||
parts = frappe.request.path[1:].split("/",3)
|
||||
call = doctype = name = None
|
||||
|
|
@ -145,48 +148,82 @@ def get_request_form_data():
|
|||
|
||||
return frappe.parse_json(data)
|
||||
|
||||
def validate_oauth():
|
||||
""" authentication using oauth """
|
||||
def validate_auth():
|
||||
if frappe.get_request_header("Authorization") is None:
|
||||
return
|
||||
|
||||
VALID_AUTH_PREFIX_TYPES = ['basic', 'bearer', 'token']
|
||||
VALID_AUTH_PREFIX_STRING = ", ".join(VALID_AUTH_PREFIX_TYPES).title()
|
||||
|
||||
authorization_header = frappe.get_request_header("Authorization", str()).split(" ")
|
||||
authorization_type = authorization_header[0].lower()
|
||||
|
||||
if len(authorization_header) == 1:
|
||||
frappe.throw(_('Invalid Authorization headers, add a token with a prefix from one of the following: {0}.'.format(VALID_AUTH_PREFIX_STRING)), frappe.InvalidAuthorizationHeader)
|
||||
|
||||
if authorization_type == "bearer":
|
||||
validate_oauth(authorization_header)
|
||||
elif authorization_type in VALID_AUTH_PREFIX_TYPES:
|
||||
validate_auth_via_api_keys(authorization_header)
|
||||
else:
|
||||
frappe.throw(_('Invalid Authorization Type {0}, must be one of {1}.'.format(authorization_type, VALID_AUTH_PREFIX_STRING)), frappe.InvalidAuthorizationPrefix)
|
||||
|
||||
|
||||
def validate_oauth(authorization_header):
|
||||
"""
|
||||
Authenticate request using OAuth and set session user
|
||||
|
||||
Args:
|
||||
authorization_header (list of str): The 'Authorization' header containing the prefix and token
|
||||
"""
|
||||
|
||||
from frappe.oauth import get_url_delimiter
|
||||
from frappe.integrations.oauth2 import get_oauth_server
|
||||
|
||||
form_dict = frappe.local.form_dict
|
||||
authorization_header = frappe.get_request_header("Authorization", "").split(" ")
|
||||
if authorization_header and authorization_header[0].lower() == "bearer":
|
||||
from frappe.integrations.oauth2 import get_oauth_server
|
||||
token = authorization_header[1]
|
||||
req = frappe.request
|
||||
parsed_url = urlparse(req.url)
|
||||
access_token = {"access_token": token}
|
||||
uri = parsed_url.scheme + "://" + parsed_url.netloc + parsed_url.path + "?" + urlencode(access_token)
|
||||
http_method = req.method
|
||||
body = req.get_data()
|
||||
headers = req.headers
|
||||
token = authorization_header[1]
|
||||
req = frappe.request
|
||||
parsed_url = urlparse(req.url)
|
||||
access_token = {"access_token": token}
|
||||
uri = parsed_url.scheme + "://" + parsed_url.netloc + parsed_url.path + "?" + urlencode(access_token)
|
||||
http_method = req.method
|
||||
body = req.get_data()
|
||||
headers = req.headers
|
||||
|
||||
required_scopes = frappe.db.get_value("OAuth Bearer Token", token, "scopes").split(get_url_delimiter())
|
||||
|
||||
valid, _oauthlib_request = get_oauth_server().verify_request(uri, http_method, body, headers, required_scopes)
|
||||
|
||||
if valid:
|
||||
frappe.set_user(frappe.db.get_value("OAuth Bearer Token", token, "user"))
|
||||
frappe.local.form_dict = form_dict
|
||||
|
||||
|
||||
def validate_auth_via_api_keys():
|
||||
"""
|
||||
authentication using api key and api secret
|
||||
|
||||
set user
|
||||
"""
|
||||
try:
|
||||
authorization_header = frappe.get_request_header("Authorization", "").split(" ")
|
||||
required_scopes = frappe.db.get_value("OAuth Bearer Token", token, "scopes").split(get_url_delimiter())
|
||||
except AttributeError:
|
||||
frappe.throw(_("Invalid Bearer token, please provide a valid access token with prefix 'Bearer'."), frappe.InvalidAuthorizationToken)
|
||||
|
||||
valid, oauthlib_request = get_oauth_server().verify_request(uri, http_method, body, headers, required_scopes)
|
||||
|
||||
if valid:
|
||||
frappe.set_user(frappe.db.get_value("OAuth Bearer Token", token, "user"))
|
||||
frappe.local.form_dict = form_dict
|
||||
|
||||
|
||||
def validate_auth_via_api_keys(authorization_header):
|
||||
"""
|
||||
Authenticate request using API keys and set session user
|
||||
|
||||
Args:
|
||||
authorization_header (list of str): The 'Authorization' header containing the prefix and token
|
||||
"""
|
||||
|
||||
try:
|
||||
auth_type, auth_token = authorization_header
|
||||
authorization_source = frappe.get_request_header("Frappe-Authorization-Source")
|
||||
if authorization_header and authorization_header[0] == 'Basic':
|
||||
token = frappe.safe_decode(base64.b64decode(authorization_header[1])).split(":")
|
||||
validate_api_key_secret(token[0], token[1], authorization_source)
|
||||
elif authorization_header and authorization_header[0] == 'token':
|
||||
token = authorization_header[1].split(":")
|
||||
validate_api_key_secret(token[0], token[1], authorization_source)
|
||||
except Exception as e:
|
||||
raise e
|
||||
if auth_type.lower() == 'basic':
|
||||
api_key, api_secret = frappe.safe_decode(base64.b64decode(auth_token)).split(":")
|
||||
validate_api_key_secret(api_key, api_secret, authorization_source)
|
||||
elif auth_type.lower() == 'token':
|
||||
api_key, api_secret = auth_token.split(":")
|
||||
validate_api_key_secret(api_key, api_secret, authorization_source)
|
||||
except binascii.Error:
|
||||
frappe.throw(_("Failed to decode token, please provide a valid base64-encoded token."), frappe.InvalidAuthorizationToken)
|
||||
except (AttributeError, TypeError, ValueError):
|
||||
frappe.throw(_("Invalid token, please provide a valid token with prefix 'Basic' or 'Token'."), frappe.InvalidAuthorizationToken)
|
||||
|
||||
|
||||
|
||||
def validate_api_key_secret(api_key, api_secret, frappe_authorization_source=None):
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import frappe.auth
|
|||
import frappe.api
|
||||
import frappe.utils.response
|
||||
import frappe.website.render
|
||||
from frappe.utils import get_site_name
|
||||
from frappe.utils import get_site_name, sanitize_html
|
||||
from frappe.middlewares import StaticDataMiddleware
|
||||
from frappe.utils.error import make_error_snapshot
|
||||
from frappe.core.doctype.comment.comment import update_comments_in_parent_after_request
|
||||
|
|
@ -172,7 +172,7 @@ def handle_exception(e):
|
|||
return_as_message = True
|
||||
|
||||
else:
|
||||
traceback = "<pre>"+frappe.get_traceback()+"</pre>"
|
||||
traceback = "<pre>" + sanitize_html(frappe.get_traceback()) + "</pre>"
|
||||
if frappe.local.flags.disable_traceback:
|
||||
traceback = ""
|
||||
|
||||
|
|
|
|||
|
|
@ -1,22 +1,24 @@
|
|||
{
|
||||
"cards": [
|
||||
{
|
||||
"icon": "octicon octicon-briefcase",
|
||||
"links": "[\n {\n \"description\": \"Documents assigned to you and by you.\",\n \"label\": \"To Do\",\n \"name\": \"ToDo\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Event and other calendars.\",\n \"label\": \"Calendar\",\n \"link\": \"List/Event/Calendar\",\n \"name\": \"Event\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Private and public Notes.\",\n \"label\": \"Note\",\n \"name\": \"Note\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Files\",\n \"name\": \"File\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Activity log of all users.\",\n \"label\": \"Activity\",\n \"name\": \"activity\",\n \"type\": \"page\"\n }\n]",
|
||||
"title": "Tools"
|
||||
"hidden": 0,
|
||||
"label": "Tools",
|
||||
"links": "[\n {\n \"description\": \"Documents assigned to you and by you.\",\n \"label\": \"To Do\",\n \"name\": \"ToDo\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Event and other calendars.\",\n \"label\": \"Calendar\",\n \"link\": \"List/Event/Calendar\",\n \"name\": \"Event\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Private and public Notes.\",\n \"label\": \"Note\",\n \"name\": \"Note\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Files\",\n \"name\": \"File\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Video\",\n \"name\": \"Video\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Activity log of all users.\",\n \"label\": \"Activity\",\n \"name\": \"activity\",\n \"type\": \"page\"\n }\n]"
|
||||
},
|
||||
{
|
||||
"links": "[\n {\n \"description\": \"Newsletters to contacts, leads.\",\n \"label\": \"Newsletter\",\n \"name\": \"Newsletter\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Email Group List\",\n \"label\": \"Email Group\",\n \"name\": \"Email Group\",\n \"type\": \"doctype\"\n }\n]",
|
||||
"title": "Email"
|
||||
"hidden": 0,
|
||||
"label": "Email",
|
||||
"links": "[\n {\n \"description\": \"Newsletters to contacts, leads.\",\n \"label\": \"Newsletter\",\n \"name\": \"Newsletter\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Email Group List\",\n \"label\": \"Email Group\",\n \"name\": \"Email Group\",\n \"type\": \"doctype\"\n }\n]"
|
||||
},
|
||||
{
|
||||
"icon": "fa fa-cog",
|
||||
"links": "[\n {\n \"type\": \"doctype\",\n \"name\": \"Assignment Rule\",\n \"description\": \"Set up rules for user assignments.\",\n \"label\": \"Assignment Rule\"\n },\n {\n \"type\": \"doctype\",\n \"name\": \"Milestone\",\n \"description\": \"Tracks milestones on the lifecycle of a document if it undergoes multiple stages.\",\n \"label\": \"Milestone\"\n },\n {\n \"type\": \"doctype\",\n \"name\": \"Auto Repeat\",\n \"description\": \"Automatically generates recurring documents.\",\n \"label\": \"Auto Repeat\"\n }\n]",
|
||||
"title": "Automation"
|
||||
"hidden": 0,
|
||||
"label": "Automation",
|
||||
"links": "[\n {\n \"type\": \"doctype\",\n \"name\": \"Assignment Rule\",\n \"description\": \"Set up rules for user assignments.\",\n \"label\": \"Assignment Rule\"\n },\n {\n \"type\": \"doctype\",\n \"name\": \"Milestone\",\n \"description\": \"Tracks milestones on the lifecycle of a document if it undergoes multiple stages.\",\n \"label\": \"Milestone\"\n },\n {\n \"type\": \"doctype\",\n \"name\": \"Auto Repeat\",\n \"description\": \"Automatically generates recurring documents.\",\n \"label\": \"Auto Repeat\"\n }\n]"
|
||||
},
|
||||
{
|
||||
"links": "[\n {\n \"type\": \"doctype\",\n \"name\": \"Event Producer\",\n \"description\": \"The site you want to subscribe to for consuming events.\",\n \"label\": \"Event Producer\"\n },\n {\n \"type\": \"doctype\",\n \"name\": \"Event Consumer\",\n \"description\": \"The site which is consuming your events.\",\n \"label\": \"Event Consumer\"\n },\n {\n \"type\": \"doctype\",\n \"name\": \"Event Update Log\",\n \"description\": \"Maintains a Log of all inserts, updates and deletions on Event Producer site for documents that have consumers.\",\n \"label\": \"Event Update Log\"\n },\n {\n \"type\": \"doctype\",\n \"name\": \"Event Sync Log\",\n \"description\": \"Maintains a log of every event consumed along with the status of the sync and a Resync button in case sync fails.\",\n \"label\": \"Event Sync Log\"\n },\n {\n \"type\": \"doctype\",\n \"name\": \"Document Type Mapping\",\n \"description\": \"The mapping configuration between two doctypes.\",\n \"label\": \"Document Type Mapping\"\n }\n]",
|
||||
"title": "Event Streaming"
|
||||
"hidden": 0,
|
||||
"label": "Event Streaming",
|
||||
"links": "[\n {\n \"type\": \"doctype\",\n \"name\": \"Event Producer\",\n \"description\": \"The site you want to subscribe to for consuming events.\",\n \"label\": \"Event Producer\"\n },\n {\n \"type\": \"doctype\",\n \"name\": \"Event Consumer\",\n \"description\": \"The site which is consuming your events.\",\n \"label\": \"Event Consumer\"\n },\n {\n \"type\": \"doctype\",\n \"name\": \"Event Update Log\",\n \"description\": \"Maintains a Log of all inserts, updates and deletions on Event Producer site for documents that have consumers.\",\n \"label\": \"Event Update Log\"\n },\n {\n \"type\": \"doctype\",\n \"name\": \"Event Sync Log\",\n \"description\": \"Maintains a log of every event consumed along with the status of the sync and a Resync button in case sync fails.\",\n \"label\": \"Event Sync Log\"\n },\n {\n \"type\": \"doctype\",\n \"name\": \"Document Type Mapping\",\n \"description\": \"The mapping configuration between two doctypes.\",\n \"label\": \"Document Type Mapping\"\n }\n]"
|
||||
}
|
||||
],
|
||||
"category": "Administration",
|
||||
|
|
@ -30,7 +32,7 @@
|
|||
"idx": 0,
|
||||
"is_standard": 1,
|
||||
"label": "Tools",
|
||||
"modified": "2020-03-12 16:30:41.841895",
|
||||
"modified": "2020-04-20 18:21:14.152537",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Automation",
|
||||
"name": "Tools",
|
||||
|
|
@ -39,27 +41,27 @@
|
|||
"pin_to_top": 0,
|
||||
"shortcuts": [
|
||||
{
|
||||
"is_query_report": 0,
|
||||
"label": "ToDo",
|
||||
"link_to": "ToDo",
|
||||
"type": "DocType"
|
||||
},
|
||||
{
|
||||
"is_query_report": 0,
|
||||
"label": "Note",
|
||||
"link_to": "Note",
|
||||
"type": "DocType"
|
||||
},
|
||||
{
|
||||
"is_query_report": 0,
|
||||
"label": "File",
|
||||
"link_to": "File",
|
||||
"type": "DocType"
|
||||
},
|
||||
{
|
||||
"is_query_report": 0,
|
||||
"label": "Assignment Rule",
|
||||
"link_to": "Assignment Rule",
|
||||
"type": "DocType"
|
||||
},
|
||||
{
|
||||
"is_query_report": 0,
|
||||
"label": "Auto Repeat",
|
||||
"link_to": "Auto Repeat",
|
||||
"type": "DocType"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ from frappe.utils.change_log import get_versions
|
|||
from frappe.translate import get_lang_dict
|
||||
from frappe.email.inbox import get_email_accounts
|
||||
from frappe.social.doctype.energy_point_settings.energy_point_settings import is_energy_point_enabled
|
||||
from frappe.website.doctype.web_page_view.web_page_view import is_tracking_enabled
|
||||
from frappe.social.doctype.energy_point_log.energy_point_log import get_energy_points
|
||||
from frappe.social.doctype.post.post import frequently_visited_links
|
||||
|
||||
|
|
@ -41,7 +42,7 @@ def get_bootinfo():
|
|||
|
||||
bootinfo.modules = {}
|
||||
bootinfo.module_list = []
|
||||
load_desktop_icons(bootinfo)
|
||||
load_desktop_data(bootinfo)
|
||||
bootinfo.letter_heads = get_letter_heads()
|
||||
bootinfo.active_domains = frappe.get_active_domains()
|
||||
bootinfo.all_domains = [d.get("name") for d in frappe.get_all("Domain")]
|
||||
|
|
@ -79,6 +80,7 @@ def get_bootinfo():
|
|||
bootinfo.success_action = get_success_action()
|
||||
bootinfo.update(get_email_accounts(user=frappe.session.user))
|
||||
bootinfo.energy_points_enabled = is_energy_point_enabled()
|
||||
bootinfo.website_tracking_enabled = is_tracking_enabled()
|
||||
bootinfo.points = get_energy_points(frappe.session.user)
|
||||
bootinfo.frequently_visited_links = frequently_visited_links()
|
||||
bootinfo.link_preview_doctypes = get_link_preview_doctypes()
|
||||
|
|
@ -99,9 +101,11 @@ def load_conf_settings(bootinfo):
|
|||
for key in ('developer_mode', 'socketio_port', 'file_watcher_port'):
|
||||
if key in conf: bootinfo[key] = conf.get(key)
|
||||
|
||||
def load_desktop_icons(bootinfo):
|
||||
def load_desktop_data(bootinfo):
|
||||
from frappe.config import get_modules_from_all_apps_for_user
|
||||
from frappe.desk.desktop import get_desk_sidebar_items
|
||||
bootinfo.allowed_modules = get_modules_from_all_apps_for_user()
|
||||
bootinfo.allowed_workspaces = get_desk_sidebar_items(True)
|
||||
|
||||
def get_allowed_pages():
|
||||
return get_user_pages_or_reports('Page')
|
||||
|
|
@ -266,4 +270,18 @@ def get_success_action():
|
|||
return frappe.get_all("Success Action", fields=["*"])
|
||||
|
||||
def get_link_preview_doctypes():
|
||||
return [d.name for d in frappe.db.get_all('DocType', {'show_preview_popup': 1})]
|
||||
from frappe.utils import cint
|
||||
|
||||
link_preview_doctypes = [d.name for d in frappe.db.get_all('DocType', {'show_preview_popup': 1})]
|
||||
customizations = frappe.get_all("Property Setter",
|
||||
fields=['doc_type', 'value'],
|
||||
filters={'property': 'show_preview_popup'}
|
||||
)
|
||||
|
||||
for custom in customizations:
|
||||
if not cint(custom.value) and custom.doc_type in link_preview_doctypes:
|
||||
link_preview_doctypes.remove(custom.doc_type)
|
||||
else:
|
||||
link_preview_doctypes.append(custom.doc_type)
|
||||
|
||||
return link_preview_doctypes
|
||||
|
|
|
|||
|
|
@ -14,7 +14,9 @@ common_default_keys = ["__default", "__global"]
|
|||
global_cache_keys = ("app_hooks", "installed_apps",
|
||||
"app_modules", "module_app", "system_settings",
|
||||
'scheduler_events', 'time_zone', 'webhooks', 'active_domains',
|
||||
'active_modules', 'assignment_rule', 'server_script_map', 'wkhtmltopdf_version')
|
||||
'active_modules', 'assignment_rule', 'server_script_map', 'wkhtmltopdf_version',
|
||||
'domain_restricted_doctypes', 'domain_restricted_pages', 'information_schema:counts',
|
||||
'sitemap_routes')
|
||||
|
||||
user_cache_keys = ("bootinfo", "user_recent", "roles", "user_doc", "lang",
|
||||
"defaults", "user_permissions", "home_page", "linked_with",
|
||||
|
|
@ -23,10 +25,6 @@ user_cache_keys = ("bootinfo", "user_recent", "roles", "user_doc", "lang",
|
|||
doctype_cache_keys = ("meta", "form_meta", "table_columns", "last_modified",
|
||||
"linked_doctypes", 'notifications', 'workflow' ,'energy_point_rule_map')
|
||||
|
||||
count_cache_blacklist = ["Version", "Tag", "ToDo", "List Filter", "Note Seen By", "Notification Log",
|
||||
"Document Follow", "Communication", "Email Queue", "Deleted Document", "File", "Email Queue Recipient"
|
||||
"Comment", "Has Role", "Attendance", "Route History"]
|
||||
|
||||
|
||||
def clear_user_cache(user=None):
|
||||
cache = frappe.cache()
|
||||
|
|
@ -46,6 +44,11 @@ def clear_user_cache(user=None):
|
|||
clear_defaults_cache()
|
||||
clear_global_cache()
|
||||
|
||||
def clear_domain_cache(user=None):
|
||||
cache = frappe.cache()
|
||||
domain_cache_keys = ('domain_restricted_doctypes', 'domain_restricted_pages')
|
||||
cache.delete_value(domain_cache_keys)
|
||||
|
||||
def clear_global_cache():
|
||||
from frappe.website.render import clear_cache as clear_website_cache
|
||||
|
||||
|
|
@ -121,7 +124,7 @@ def clear_doctype_map(doctype, name):
|
|||
cache_key = frappe.scrub(doctype) + '_map'
|
||||
frappe.cache().hdel(cache_key, name)
|
||||
|
||||
def build_table_count_cache(doc=None, method=None, *args, **kwargs):
|
||||
def build_table_count_cache():
|
||||
if (frappe.flags.in_patch
|
||||
or frappe.flags.in_install
|
||||
or frappe.flags.in_migrate
|
||||
|
|
@ -129,15 +132,6 @@ def build_table_count_cache(doc=None, method=None, *args, **kwargs):
|
|||
or frappe.flags.in_setup_wizard):
|
||||
return
|
||||
|
||||
if doc and isinstance(doc, Document):
|
||||
doctype = doc.doctype
|
||||
|
||||
if doc.meta.istable:
|
||||
return
|
||||
|
||||
if doctype in count_cache_blacklist:
|
||||
return
|
||||
|
||||
_cache = frappe.cache()
|
||||
data = frappe.db.multisql({
|
||||
"mariadb": """
|
||||
|
|
|
|||
|
|
@ -1,13 +1,23 @@
|
|||
from __future__ import unicode_literals, absolute_import, print_function
|
||||
# imports - standard imports
|
||||
import atexit
|
||||
import compileall
|
||||
import hashlib
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
|
||||
# imports - third party imports
|
||||
import click
|
||||
import hashlib, os, sys, compileall, re
|
||||
|
||||
# imports - module imports
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.commands import pass_context, get_site
|
||||
from frappe.commands import get_site, pass_context
|
||||
from frappe.commands.scheduler import _is_scheduler_enabled
|
||||
from frappe.installer import update_site_config
|
||||
from frappe.utils import touch_file, get_site_path
|
||||
from six import text_type
|
||||
from frappe.utils import get_site_path, touch_file
|
||||
|
||||
|
||||
@click.command('new-site')
|
||||
@click.argument('site')
|
||||
|
|
@ -68,32 +78,33 @@ def _new_site(db_name, site, mariadb_root_username=None, mariadb_root_password=N
|
|||
|
||||
make_site_dirs()
|
||||
|
||||
installing = None
|
||||
try:
|
||||
installing = touch_file(get_site_path('locks', 'installing.lock'))
|
||||
installing = touch_file(get_site_path('locks', 'installing.lock'))
|
||||
atexit.register(_new_site_cleanup, site, mariadb_root_username, mariadb_root_password)
|
||||
|
||||
install_db(root_login=mariadb_root_username, root_password=mariadb_root_password,
|
||||
db_name=db_name, admin_password=admin_password, verbose=verbose,
|
||||
source_sql=source_sql, force=force, reinstall=reinstall, db_password=db_password, db_type=db_type, db_host=db_host, db_port=db_port, no_mariadb_socket=no_mariadb_socket)
|
||||
install_db(root_login=mariadb_root_username, root_password=mariadb_root_password, db_name=db_name,
|
||||
admin_password=admin_password, verbose=verbose, source_sql=source_sql, force=force, reinstall=reinstall,
|
||||
db_password=db_password, db_type=db_type, db_host=db_host, db_port=db_port, no_mariadb_socket=no_mariadb_socket)
|
||||
apps_to_install = ['frappe'] + (frappe.conf.get("install_apps") or []) + (list(install_apps) or [])
|
||||
for app in apps_to_install:
|
||||
_install_app(app, verbose=verbose, set_as_patched=not source_sql)
|
||||
|
||||
apps_to_install = ['frappe'] + (frappe.conf.get("install_apps") or []) + (list(install_apps) or [])
|
||||
for app in apps_to_install:
|
||||
_install_app(app, verbose=verbose, set_as_patched=not source_sql)
|
||||
os.remove(installing)
|
||||
|
||||
frappe.utils.scheduler.toggle_scheduler(enable_scheduler)
|
||||
frappe.db.commit()
|
||||
frappe.utils.scheduler.toggle_scheduler(enable_scheduler)
|
||||
frappe.db.commit()
|
||||
|
||||
scheduler_status = "disabled" if frappe.utils.scheduler.is_scheduler_disabled() else "enabled"
|
||||
print("*** Scheduler is", scheduler_status, "***")
|
||||
scheduler_status = "disabled" if frappe.utils.scheduler.is_scheduler_disabled() else "enabled"
|
||||
print("*** Scheduler is", scheduler_status, "***")
|
||||
|
||||
except frappe.exceptions.ImproperDBConfigurationError:
|
||||
_drop_site(site, mariadb_root_username, mariadb_root_password, force=True)
|
||||
def _new_site_cleanup(site, mariadb_root_username, mariadb_root_password):
|
||||
installing = get_site_path('locks', 'installing.lock')
|
||||
|
||||
finally:
|
||||
if installing and os.path.exists(installing):
|
||||
os.remove(installing)
|
||||
if installing and os.path.exists(installing):
|
||||
if mariadb_root_password:
|
||||
_drop_site(site, mariadb_root_username, mariadb_root_password, force=True)
|
||||
shutil.rmtree(site)
|
||||
|
||||
frappe.destroy()
|
||||
frappe.destroy()
|
||||
|
||||
@click.command('restore')
|
||||
@click.argument('sql-file-path')
|
||||
|
|
@ -317,10 +328,18 @@ def backup(context, with_files=False, backup_path_db=None, backup_path_files=Non
|
|||
"Backup"
|
||||
from frappe.utils.backups import scheduled_backup
|
||||
verbose = context.verbose
|
||||
exit_code = 0
|
||||
for site in context.sites:
|
||||
frappe.init(site=site)
|
||||
frappe.connect()
|
||||
odb = scheduled_backup(ignore_files=not with_files, backup_path_db=backup_path_db, backup_path_files=backup_path_files, backup_path_private_files=backup_path_private_files, force=True)
|
||||
try:
|
||||
frappe.init(site=site)
|
||||
frappe.connect()
|
||||
odb = scheduled_backup(ignore_files=not with_files, backup_path_db=backup_path_db, backup_path_files=backup_path_files, backup_path_private_files=backup_path_private_files, force=True)
|
||||
except Exception as e:
|
||||
if verbose:
|
||||
print("Backup failed for {0}. Database or site_config.json may be corrupted".format(site))
|
||||
exit_code = 1
|
||||
continue
|
||||
|
||||
if verbose:
|
||||
from frappe.utils import now
|
||||
print("database backup taken -", odb.backup_path_db, "- on", now())
|
||||
|
|
@ -329,6 +348,7 @@ def backup(context, with_files=False, backup_path_db=None, backup_path_files=Non
|
|||
print("private files backup taken -", odb.backup_path_private_files, "- on", now())
|
||||
|
||||
frappe.destroy()
|
||||
sys.exit(exit_code)
|
||||
|
||||
@click.command('remove-from-installed-apps')
|
||||
@click.argument('app')
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"actions": [],
|
||||
"allow_events_in_timeline": 1,
|
||||
"allow_import": 1,
|
||||
"allow_rename": 1,
|
||||
|
|
@ -115,6 +116,7 @@
|
|||
"label": "Phone",
|
||||
"oldfieldname": "contact_no",
|
||||
"oldfieldtype": "Data",
|
||||
"options": "Phone",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
|
|
@ -200,6 +202,7 @@
|
|||
"fieldname": "mobile_no",
|
||||
"fieldtype": "Data",
|
||||
"label": "Mobile No",
|
||||
"options": "Phone",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
|
|
@ -245,7 +248,8 @@
|
|||
"icon": "fa fa-user",
|
||||
"idx": 1,
|
||||
"image_field": "image",
|
||||
"modified": "2019-10-10 22:04:41.070479",
|
||||
"links": [],
|
||||
"modified": "2020-04-06 18:25:28.223693",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Contacts",
|
||||
"name": "Contact",
|
||||
|
|
|
|||
|
|
@ -69,23 +69,25 @@ class Contact(Document):
|
|||
return True
|
||||
|
||||
def add_email(self, email_id, is_primary=0, autosave=False):
|
||||
self.append("email_ids", {
|
||||
"email_id": email_id,
|
||||
"is_primary": is_primary
|
||||
})
|
||||
if not frappe.db.exists("Contact Email", {"email_id": email_id, "parent": self.name}):
|
||||
self.append("email_ids", {
|
||||
"email_id": email_id,
|
||||
"is_primary": is_primary
|
||||
})
|
||||
|
||||
if autosave:
|
||||
self.save(ignore_permissions=True)
|
||||
if autosave:
|
||||
self.save(ignore_permissions=True)
|
||||
|
||||
def add_phone(self, phone, is_primary_phone=0, is_primary_mobile_no=0, autosave=False):
|
||||
self.append("phone_nos", {
|
||||
"phone": phone,
|
||||
"is_primary_phone": is_primary_phone,
|
||||
"is_primary_mobile_no": is_primary_mobile_no
|
||||
})
|
||||
if not frappe.db.exists("Contact Phone", {"phone": phone, "parent": self.name}):
|
||||
self.append("phone_nos", {
|
||||
"phone": phone,
|
||||
"is_primary_phone": is_primary_phone,
|
||||
"is_primary_mobile_no": is_primary_mobile_no
|
||||
})
|
||||
|
||||
if autosave:
|
||||
self.save(ignore_permissions=True)
|
||||
if autosave:
|
||||
self.save(ignore_permissions=True)
|
||||
|
||||
def set_primary_email(self):
|
||||
if not self.email_ids:
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"actions": [],
|
||||
"creation": "2019-08-02 13:10:37.890214",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
|
|
@ -14,6 +15,7 @@
|
|||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Number",
|
||||
"options": "Phone",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
|
|
@ -34,7 +36,8 @@
|
|||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"modified": "2019-09-24 17:47:50.375326",
|
||||
"links": [],
|
||||
"modified": "2020-04-06 18:28:10.486220",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Contacts",
|
||||
"name": "Contact Phone",
|
||||
|
|
|
|||
|
|
@ -1,37 +1,37 @@
|
|||
{
|
||||
"cards": [
|
||||
{
|
||||
"icon": "fa fa-th",
|
||||
"links": "[\n {\n \"description\": \"Import Data from CSV / Excel files.\",\n \"icon\": \"octicon octicon-cloud-upload\",\n \"label\": \"Import Data\",\n \"name\": \"Data Import\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Export Data in CSV / Excel format.\",\n \"icon\": \"octicon octicon-cloud-upload\",\n \"label\": \"Export Data\",\n \"name\": \"Data Export\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Update many values at one time.\",\n \"hide_count\": true,\n \"label\": \"Bulk Update\",\n \"name\": \"Bulk Update\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"List of backups available for download\",\n \"icon\": \"fa fa-download\",\n \"label\": \"Download Backups\",\n \"name\": \"backups\",\n \"type\": \"page\"\n },\n {\n \"description\": \"Restore or permanently delete a document.\",\n \"label\": \"Deleted Documents\",\n \"name\": \"Deleted Document\",\n \"type\": \"doctype\"\n }\n]",
|
||||
"title": "Data"
|
||||
"hidden": 0,
|
||||
"label": "Data",
|
||||
"links": "[\n {\n \"description\": \"Import Data from CSV / Excel files.\",\n \"icon\": \"octicon octicon-cloud-upload\",\n \"label\": \"Import Data\",\n \"name\": \"Data Import\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Export Data in CSV / Excel format.\",\n \"icon\": \"octicon octicon-cloud-upload\",\n \"label\": \"Export Data\",\n \"name\": \"Data Export\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Update many values at one time.\",\n \"hide_count\": true,\n \"label\": \"Bulk Update\",\n \"name\": \"Bulk Update\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"List of backups available for download\",\n \"icon\": \"fa fa-download\",\n \"label\": \"Download Backups\",\n \"name\": \"backups\",\n \"type\": \"page\"\n },\n {\n \"description\": \"Restore or permanently delete a document.\",\n \"label\": \"Deleted Documents\",\n \"name\": \"Deleted Document\",\n \"type\": \"doctype\"\n }\n]"
|
||||
},
|
||||
{
|
||||
"icon": "fa fa-envelope",
|
||||
"links": "[\n {\n \"description\": \"Add / Manage Email Accounts.\",\n \"label\": \"Email Account\",\n \"name\": \"Email Account\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Add / Manage Email Domains.\",\n \"label\": \"Email Domain\",\n \"name\": \"Email Domain\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Setup Notifications based on various criteria.\",\n \"label\": \"Notification\",\n \"name\": \"Notification\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Email Templates for common queries.\",\n \"label\": \"Email Template\",\n \"name\": \"Email Template\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Setup Reports to be emailed at regular intervals\",\n \"label\": \"Auto Email Report\",\n \"name\": \"Auto Email Report\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Create and manage newsletter\",\n \"label\": \"Newsletter\",\n \"name\": \"Newsletter\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Configure notifications for mentions, assignments, energy points and more.\",\n \"label\": \"Notification Settings\",\n \"name\": \"Notification Settings\",\n \"route\": \"Form/Notification Settings/Administrator\",\n \"type\": \"doctype\"\n }\n]",
|
||||
"title": "Email / Notifications"
|
||||
"hidden": 0,
|
||||
"label": "Email / Notifications",
|
||||
"links": "[\n {\n \"description\": \"Add / Manage Email Accounts.\",\n \"label\": \"Email Account\",\n \"name\": \"Email Account\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Add / Manage Email Domains.\",\n \"label\": \"Email Domain\",\n \"name\": \"Email Domain\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Setup Notifications based on various criteria.\",\n \"label\": \"Notification\",\n \"name\": \"Notification\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Email Templates for common queries.\",\n \"label\": \"Email Template\",\n \"name\": \"Email Template\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Setup Reports to be emailed at regular intervals\",\n \"label\": \"Auto Email Report\",\n \"name\": \"Auto Email Report\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Create and manage newsletter\",\n \"label\": \"Newsletter\",\n \"name\": \"Newsletter\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Configure notifications for mentions, assignments, energy points and more.\",\n \"label\": \"Notification Settings\",\n \"name\": \"Notification Settings\",\n \"route\": \"Form/Notification Settings/Administrator\",\n \"type\": \"doctype\"\n }\n]"
|
||||
},
|
||||
{
|
||||
"icon": "fa fa-globe",
|
||||
"links": "[\n {\n \"description\": \"Setup of top navigation bar, footer and logo.\",\n \"label\": \"Website Settings\",\n \"name\": \"Website Settings\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"List of themes for Website.\",\n \"label\": \"Website Theme\",\n \"name\": \"Website Theme\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Javascript to append to the head section of the page.\",\n \"label\": \"Website Script\",\n \"name\": \"Website Script\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Settings for About Us Page.\",\n \"label\": \"About Us Settings\",\n \"name\": \"About Us Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Settings for Contact Us Page.\",\n \"label\": \"Contact Us Settings\",\n \"name\": \"Contact Us Settings\",\n \"type\": \"doctype\"\n }\n]",
|
||||
"title": "Website"
|
||||
"hidden": 0,
|
||||
"label": "Website",
|
||||
"links": "[\n {\n \"description\": \"Setup of top navigation bar, footer and logo.\",\n \"label\": \"Website Settings\",\n \"name\": \"Website Settings\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"List of themes for Website.\",\n \"label\": \"Website Theme\",\n \"name\": \"Website Theme\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Javascript to append to the head section of the page.\",\n \"label\": \"Website Script\",\n \"name\": \"Website Script\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Settings for About Us Page.\",\n \"label\": \"About Us Settings\",\n \"name\": \"About Us Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Settings for Contact Us Page.\",\n \"label\": \"Contact Us Settings\",\n \"name\": \"Contact Us Settings\",\n \"type\": \"doctype\"\n }\n]"
|
||||
},
|
||||
{
|
||||
"icon": "fa fa-wrench",
|
||||
"links": "[\n {\n \"description\": \"Language, Date and Time settings\",\n \"hide_count\": true,\n \"label\": \"System Settings\",\n \"name\": \"System Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Log of error on automated events (scheduler).\",\n \"label\": \"Error Log\",\n \"name\": \"Error Log\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Log of error during requests.\",\n \"label\": \"Error Snapshot\",\n \"name\": \"Error Snapshot\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Enable / Disable Domains\",\n \"hide_count\": true,\n \"label\": \"Domain Settings\",\n \"name\": \"Domain Settings\",\n \"type\": \"doctype\"\n }\n]",
|
||||
"title": "Core"
|
||||
"hidden": 0,
|
||||
"label": "Core",
|
||||
"links": "[\n {\n \"description\": \"Language, Date and Time settings\",\n \"hide_count\": true,\n \"label\": \"System Settings\",\n \"name\": \"System Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Log of error on automated events (scheduler).\",\n \"label\": \"Error Log\",\n \"name\": \"Error Log\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Log of error during requests.\",\n \"label\": \"Error Snapshot\",\n \"name\": \"Error Snapshot\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Enable / Disable Domains\",\n \"hide_count\": true,\n \"label\": \"Domain Settings\",\n \"name\": \"Domain Settings\",\n \"type\": \"doctype\"\n }\n]"
|
||||
},
|
||||
{
|
||||
"icon": "fa fa-print",
|
||||
"links": "[\n {\n \"description\": \"Drag and Drop tool to build and customize Print Formats.\",\n \"label\": \"Print Format Builder\",\n \"name\": \"print-format-builder\",\n \"type\": \"page\"\n },\n {\n \"description\": \"Set default format, page size, print style etc.\",\n \"label\": \"Print Settings\",\n \"name\": \"Print Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Customized HTML Templates for printing transactions.\",\n \"label\": \"Print Format\",\n \"name\": \"Print Format\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Stylesheets for Print Formats\",\n \"label\": \"Print Style\",\n \"name\": \"Print Style\",\n \"type\": \"doctype\"\n }\n]",
|
||||
"title": "Printing"
|
||||
"hidden": 0,
|
||||
"label": "Printing",
|
||||
"links": "[\n {\n \"description\": \"Drag and Drop tool to build and customize Print Formats.\",\n \"label\": \"Print Format Builder\",\n \"name\": \"print-format-builder\",\n \"type\": \"page\"\n },\n {\n \"description\": \"Set default format, page size, print style etc.\",\n \"label\": \"Print Settings\",\n \"name\": \"Print Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Customized HTML Templates for printing transactions.\",\n \"label\": \"Print Format\",\n \"name\": \"Print Format\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Stylesheets for Print Formats\",\n \"label\": \"Print Style\",\n \"name\": \"Print Style\",\n \"type\": \"doctype\"\n }\n]"
|
||||
},
|
||||
{
|
||||
"icon": "fa fa-random",
|
||||
"links": "[\n {\n \"description\": \"Define workflows for forms.\",\n \"label\": \"Workflow\",\n \"name\": \"Workflow\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"States for workflow (e.g. Draft, Approved, Cancelled).\",\n \"label\": \"Workflow State\",\n \"name\": \"Workflow State\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Actions for workflow (e.g. Approve, Cancel).\",\n \"label\": \"Workflow Action\",\n \"name\": \"Workflow Action\",\n \"type\": \"doctype\"\n }\n]",
|
||||
"title": "Workflow"
|
||||
"hidden": 0,
|
||||
"label": "Workflow",
|
||||
"links": "[\n {\n \"description\": \"Define workflows for forms.\",\n \"label\": \"Workflow\",\n \"name\": \"Workflow\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"States for workflow (e.g. Draft, Approved, Cancelled).\",\n \"label\": \"Workflow State\",\n \"name\": \"Workflow State\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Actions for workflow (e.g. Approve, Cancel).\",\n \"label\": \"Workflow Action\",\n \"name\": \"Workflow Action\",\n \"type\": \"doctype\"\n }\n]"
|
||||
}
|
||||
],
|
||||
"category": "Administration",
|
||||
"category": "Modules",
|
||||
"charts": [],
|
||||
"creation": "2020-03-02 15:09:40.527211",
|
||||
"developer_mode_only": 0,
|
||||
|
|
@ -42,29 +42,29 @@
|
|||
"idx": 0,
|
||||
"is_standard": 1,
|
||||
"label": "Settings",
|
||||
"modified": "2020-03-12 16:30:43.510434",
|
||||
"modified": "2020-04-01 11:24:40.636747",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Settings",
|
||||
"owner": "Administrator",
|
||||
"pin_to_bottom": 0,
|
||||
"pin_to_top": 1,
|
||||
"pin_to_bottom": 1,
|
||||
"pin_to_top": 0,
|
||||
"shortcuts": [
|
||||
{
|
||||
"icon": "octicon octicon-settings",
|
||||
"is_query_report": 0,
|
||||
"label": "System Settings",
|
||||
"link_to": "System Settings",
|
||||
"type": "DocType"
|
||||
},
|
||||
{
|
||||
"icon": "fa fa-print",
|
||||
"is_query_report": 0,
|
||||
"label": "Print Settings",
|
||||
"link_to": "Print Settings",
|
||||
"type": "DocType"
|
||||
},
|
||||
{
|
||||
"icon": "fa fa-globe",
|
||||
"is_query_report": 0,
|
||||
"label": "Website Settings",
|
||||
"link_to": "Website Settings",
|
||||
"type": "DocType"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,19 @@
|
|||
{
|
||||
"cards": [
|
||||
{
|
||||
"icon": "fa fa-group",
|
||||
"links": "[\n {\n \"description\": \"System and Website Users\",\n \"label\": \"User\",\n \"name\": \"User\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"User Roles\",\n \"label\": \"Role\",\n \"name\": \"Role\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Role Profile\",\n \"label\": \"Role Profile\",\n \"name\": \"Role Profile\",\n \"type\": \"doctype\"\n }\n]",
|
||||
"title": "Users"
|
||||
"hidden": 0,
|
||||
"label": "Users",
|
||||
"links": "[\n {\n \"description\": \"System and Website Users\",\n \"label\": \"User\",\n \"name\": \"User\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"User Roles\",\n \"label\": \"Role\",\n \"name\": \"Role\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Role Profile\",\n \"label\": \"Role Profile\",\n \"name\": \"Role Profile\",\n \"type\": \"doctype\"\n }\n]"
|
||||
},
|
||||
{
|
||||
"icon": "fa fa-group",
|
||||
"links": "[\n {\n \"description\": \"Activity Log by \",\n \"label\": \"Activity Log\",\n \"name\": \"Activity Log\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"View Log of all print, download and export events\",\n \"label\": \"Access Log\",\n \"name\": \"Access Log\",\n \"type\": \"doctype\"\n }\n]",
|
||||
"title": "Logs"
|
||||
"hidden": 0,
|
||||
"label": "Logs",
|
||||
"links": "[\n {\n \"description\": \"Activity Log by \",\n \"label\": \"Activity Log\",\n \"name\": \"Activity Log\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"View Log of all print, download and export events\",\n \"label\": \"Access Log\",\n \"name\": \"Access Log\",\n \"type\": \"doctype\"\n }\n]"
|
||||
},
|
||||
{
|
||||
"icon": "fa fa-lock",
|
||||
"links": "[\n {\n \"description\": \"Set Permissions on Document Types and Roles\",\n \"icon\": \"fa fa-lock\",\n \"label\": \"Role Permissions Manager\",\n \"name\": \"permission-manager\",\n \"type\": \"page\"\n },\n {\n \"description\": \"Restrict user for specific document\",\n \"icon\": \"fa fa-lock\",\n \"label\": \"User Permissions\",\n \"name\": \"User Permission\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Set custom roles for page and report\",\n \"label\": \"Role Permission for Page and Report\",\n \"name\": \"Role Permission for Page and Report\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"User\"\n ],\n \"description\": \"Check which Documents are readable by a User\",\n \"doctype\": \"User\",\n \"icon\": \"fa fa-eye-open\",\n \"is_query_report\": true,\n \"label\": \"Permitted Documents For User\",\n \"name\": \"Permitted Documents For User\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"DocShare\"\n ],\n \"description\": \"Report of all document shares\",\n \"doctype\": \"DocShare\",\n \"icon\": \"fa fa-share\",\n \"label\": \"Document Share Report\",\n \"name\": \"Document Share Report\",\n \"type\": \"report\"\n }\n]",
|
||||
"title": "Permissions"
|
||||
"hidden": 0,
|
||||
"label": "Permissions",
|
||||
"links": "[\n {\n \"description\": \"Set Permissions on Document Types and Roles\",\n \"icon\": \"fa fa-lock\",\n \"label\": \"Role Permissions Manager\",\n \"name\": \"permission-manager\",\n \"type\": \"page\"\n },\n {\n \"description\": \"Restrict user for specific document\",\n \"icon\": \"fa fa-lock\",\n \"label\": \"User Permissions\",\n \"name\": \"User Permission\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Set custom roles for page and report\",\n \"label\": \"Role Permission for Page and Report\",\n \"name\": \"Role Permission for Page and Report\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"User\"\n ],\n \"description\": \"Check which Documents are readable by a User\",\n \"doctype\": \"User\",\n \"icon\": \"fa fa-eye-open\",\n \"is_query_report\": true,\n \"label\": \"Permitted Documents For User\",\n \"name\": \"Permitted Documents For User\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"DocShare\"\n ],\n \"description\": \"Report of all document shares\",\n \"doctype\": \"DocShare\",\n \"icon\": \"fa fa-share\",\n \"label\": \"Document Share Report\",\n \"name\": \"Document Share Report\",\n \"type\": \"report\"\n }\n]"
|
||||
}
|
||||
],
|
||||
"category": "Administration",
|
||||
|
|
@ -27,7 +27,7 @@
|
|||
"idx": 0,
|
||||
"is_standard": 1,
|
||||
"label": "Users",
|
||||
"modified": "2020-03-12 16:30:42.483376",
|
||||
"modified": "2020-04-01 11:24:40.767676",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Users",
|
||||
|
|
@ -36,22 +36,22 @@
|
|||
"pin_to_top": 0,
|
||||
"shortcuts": [
|
||||
{
|
||||
"is_query_report": 0,
|
||||
"label": "User",
|
||||
"link_to": "User",
|
||||
"type": "DocType"
|
||||
},
|
||||
{
|
||||
"is_query_report": 0,
|
||||
"label": "Role",
|
||||
"link_to": "Role",
|
||||
"type": "DocType"
|
||||
},
|
||||
{
|
||||
"is_query_report": 0,
|
||||
"label": "permission-manager",
|
||||
"link_to": "permission-manager",
|
||||
"type": "Page"
|
||||
},
|
||||
{
|
||||
"is_query_report": 0,
|
||||
"label": "user-profile",
|
||||
"link_to": "user-profile",
|
||||
"type": "Page"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
{
|
||||
"actions": [],
|
||||
"allow_import": 1,
|
||||
"creation": "2013-01-29 10:47:14",
|
||||
"description": "Keeps track of all communications",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Setup",
|
||||
"email_append_to": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"subject",
|
||||
|
|
@ -383,7 +385,8 @@
|
|||
],
|
||||
"icon": "fa fa-comment",
|
||||
"idx": 1,
|
||||
"modified": "2019-10-09 14:22:27.664645",
|
||||
"links": [],
|
||||
"modified": "2019-12-27 14:44:04.880373",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Communication",
|
||||
|
|
@ -430,8 +433,10 @@
|
|||
}
|
||||
],
|
||||
"search_fields": "subject",
|
||||
"sender_field": "sender",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"subject_field": "subject",
|
||||
"title_field": "subject",
|
||||
"track_changes": 1,
|
||||
"track_seen": 1
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ class TestExporter(unittest.TestCase):
|
|||
e = Exporter('Web Page', export_fields='All')
|
||||
csv_array = e.get_csv_array()
|
||||
header = csv_array[0]
|
||||
self.assertEqual(len(header), 24)
|
||||
self.assertEqual(len(header), 28)
|
||||
|
||||
|
||||
def test_exports_selected_fields(self):
|
||||
|
|
|
|||
|
|
@ -11,9 +11,9 @@
|
|||
"label",
|
||||
"fieldtype",
|
||||
"fieldname",
|
||||
"reqd",
|
||||
"precision",
|
||||
"length",
|
||||
"reqd",
|
||||
"search_index",
|
||||
"in_list_view",
|
||||
"in_standard_filter",
|
||||
|
|
@ -102,6 +102,7 @@
|
|||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:!in_list([\"Section Break\", \"Column Break\", \"Button\", \"HTML\"], doc.fieldtype)",
|
||||
"fieldname": "reqd",
|
||||
"fieldtype": "Check",
|
||||
"in_list_view": 1,
|
||||
|
|
@ -452,7 +453,7 @@
|
|||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-03-16 14:49:49.672099",
|
||||
"modified": "2020-04-19 21:54:13.783908",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "DocField",
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ frappe.ui.form.on('DocType', {
|
|||
frm.events.autoname(frm);
|
||||
},
|
||||
|
||||
autoname(frm) {
|
||||
autoname: function(frm) {
|
||||
frm.set_df_property('fields', 'reqd', frm.doc.autoname !== 'Prompt');
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -54,6 +54,10 @@
|
|||
"color",
|
||||
"show_preview_popup",
|
||||
"show_name_in_global_search",
|
||||
"email_settings_sb",
|
||||
"email_append_to",
|
||||
"sender_field",
|
||||
"subject_field",
|
||||
"sb2",
|
||||
"permissions",
|
||||
"restrict_to_domain",
|
||||
|
|
@ -488,11 +492,37 @@
|
|||
"fieldtype": "Table",
|
||||
"label": "Links",
|
||||
"options": "DocType Link"
|
||||
},
|
||||
{
|
||||
"depends_on": "email_append_to",
|
||||
"fieldname": "subject_field",
|
||||
"fieldtype": "Data",
|
||||
"label": "Subject Field"
|
||||
},
|
||||
{
|
||||
"depends_on": "email_append_to",
|
||||
"fieldname": "sender_field",
|
||||
"fieldtype": "Data",
|
||||
"label": "Sender Field",
|
||||
"mandatory_depends_on": "email_append_to"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "email_append_to",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow document creation via Email"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "email_settings_sb",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Email Settings"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-bolt",
|
||||
"idx": 6,
|
||||
"modified": "2019-11-25 17:24:03.690192",
|
||||
"links": [],
|
||||
"modified": "2020-03-27 14:51:44.581128",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "DocType",
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import frappe
|
|||
import frappe.website.render
|
||||
from frappe import _
|
||||
from frappe.utils import now, cint
|
||||
from frappe.model import no_value_fields, default_fields, data_fieldtypes, table_fields
|
||||
from frappe.model import no_value_fields, default_fields, data_fieldtypes, table_fields, data_field_options
|
||||
from frappe.model.document import Document
|
||||
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
|
||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
|
||||
|
|
@ -94,10 +94,11 @@ class DocType(Document):
|
|||
if not self.is_new():
|
||||
self.setup_fields_to_fetch()
|
||||
|
||||
check_email_append_to(self)
|
||||
|
||||
if self.default_print_format and not self.custom:
|
||||
frappe.throw(_('Standard DocType cannot have default print format, use Customize Form'))
|
||||
|
||||
|
||||
def set_default_in_list_view(self):
|
||||
'''Set default in-list-view for first 4 mandatory fields'''
|
||||
if not [d.fieldname for d in self.fields if d.in_list_view]:
|
||||
|
|
@ -108,14 +109,12 @@ class DocType(Document):
|
|||
cnt += 1
|
||||
if cnt == 4: break
|
||||
|
||||
|
||||
def set_default_translatable(self):
|
||||
'''Ensure that non-translatable never will be translatable'''
|
||||
for d in self.fields:
|
||||
if d.translatable and not supports_translation(d.fieldtype):
|
||||
d.translatable = 0
|
||||
|
||||
|
||||
def check_developer_mode(self):
|
||||
"""Throw exception if not developer mode or via patch"""
|
||||
if frappe.flags.in_patch or frappe.flags.in_test:
|
||||
|
|
@ -124,7 +123,6 @@ class DocType(Document):
|
|||
if not frappe.conf.get("developer_mode") and not self.custom:
|
||||
frappe.throw(_("Not in Developer Mode! Set in site_config.json or make 'Custom' DocType."), CannotCreateStandardDoctypeError)
|
||||
|
||||
|
||||
def setup_fields_to_fetch(self):
|
||||
'''Setup query to update values for newly set fetch values'''
|
||||
try:
|
||||
|
|
@ -169,21 +167,18 @@ class DocType(Document):
|
|||
)
|
||||
)
|
||||
|
||||
|
||||
def update_fields_to_fetch(self):
|
||||
'''Update fetch values based on queries setup'''
|
||||
if self.flags.update_fields_to_fetch_queries and not self.issingle:
|
||||
for query in self.flags.update_fields_to_fetch_queries:
|
||||
frappe.db.sql(query)
|
||||
|
||||
|
||||
def validate_document_type(self):
|
||||
if self.document_type=="Transaction":
|
||||
self.document_type = "Document"
|
||||
if self.document_type=="Master":
|
||||
self.document_type = "Setup"
|
||||
|
||||
|
||||
def validate_website(self):
|
||||
"""Ensure that website generator has field 'route'"""
|
||||
if self.has_web_view:
|
||||
|
|
@ -194,7 +189,6 @@ class DocType(Document):
|
|||
# clear website cache
|
||||
frappe.website.render.clear_cache()
|
||||
|
||||
|
||||
def change_modified_of_parent(self):
|
||||
"""Change the timestamp of parent DocType if the current one is a child to clear caches."""
|
||||
if frappe.flags.in_import:
|
||||
|
|
@ -204,7 +198,6 @@ class DocType(Document):
|
|||
for p in parent_list:
|
||||
frappe.db.sql('UPDATE `tabDocType` SET modified=%s WHERE `name`=%s', (now(), p.parent))
|
||||
|
||||
|
||||
def scrub_field_names(self):
|
||||
"""Sluggify fieldnames if not set from Label."""
|
||||
restricted = ('name','parent','creation','modified','modified_by',
|
||||
|
|
@ -213,7 +206,7 @@ class DocType(Document):
|
|||
if d.fieldtype:
|
||||
if (not getattr(d, "fieldname", None)):
|
||||
if d.label:
|
||||
d.fieldname = d.label.strip().lower().replace(' ','_')
|
||||
d.fieldname = d.label.strip().lower().replace(' ','_').strip('?')
|
||||
if d.fieldname in restricted:
|
||||
d.fieldname = d.fieldname + '1'
|
||||
if d.fieldtype=='Section Break':
|
||||
|
|
@ -234,7 +227,6 @@ class DocType(Document):
|
|||
# unique is automatically an index
|
||||
if d.unique: d.search_index = 0
|
||||
|
||||
|
||||
def validate_series(self, autoname=None, name=None):
|
||||
"""Validate if `autoname` property is correctly set."""
|
||||
if not autoname: autoname = self.autoname
|
||||
|
|
@ -271,7 +263,6 @@ class DocType(Document):
|
|||
if used_in:
|
||||
frappe.throw(_("Series {0} already used in {1}").format(prefix, used_in[0][0]))
|
||||
|
||||
|
||||
def on_update(self):
|
||||
"""Update database schema, make controller templates if `custom` is not set and clear cache."""
|
||||
self.delete_duplicate_custom_fields()
|
||||
|
|
@ -325,7 +316,6 @@ class DocType(Document):
|
|||
dt = {0} and fieldname in ({1})
|
||||
'''.format('%s', ', '.join(['%s'] * len(fields))), tuple([self.name] + fields), as_dict=True)
|
||||
|
||||
|
||||
def sync_global_search(self):
|
||||
'''If global search settings are changed, rebuild search properties for this table'''
|
||||
global_search_fields_before_update = [d.fieldname for d in
|
||||
|
|
@ -343,7 +333,6 @@ class DocType(Document):
|
|||
frappe.enqueue('frappe.utils.global_search.rebuild_for_doctype',
|
||||
now=now, doctype=self.name)
|
||||
|
||||
|
||||
def set_base_class_for_controller(self):
|
||||
'''Updates the controller class to subclass from `WebsiteGenertor`,
|
||||
if it is a subclass of `Document`'''
|
||||
|
|
@ -363,14 +352,12 @@ class DocType(Document):
|
|||
with open(controller_path, 'w') as f:
|
||||
f.write(code)
|
||||
|
||||
|
||||
def run_module_method(self, method):
|
||||
from frappe.modules import load_doctype_module
|
||||
module = load_doctype_module(self.name, self.module)
|
||||
if hasattr(module, method):
|
||||
getattr(module, method)()
|
||||
|
||||
|
||||
def before_rename(self, old, new, merge=False):
|
||||
"""Throw exception if merge. DocTypes cannot be merged."""
|
||||
if not self.custom and frappe.session.user != "Administrator":
|
||||
|
|
@ -386,7 +373,6 @@ class DocType(Document):
|
|||
if not self.custom and not frappe.flags.in_test and not frappe.flags.in_patch:
|
||||
self.rename_files_and_folders(old, new)
|
||||
|
||||
|
||||
def after_rename(self, old, new, merge=False):
|
||||
"""Change table name using `RENAME TABLE` if table exists. Or update
|
||||
`doctype` property for Single type."""
|
||||
|
|
@ -397,7 +383,6 @@ class DocType(Document):
|
|||
else:
|
||||
frappe.db.sql("rename table `tab%s` to `tab%s`" % (old, new))
|
||||
|
||||
|
||||
def rename_files_and_folders(self, old, new):
|
||||
# move files
|
||||
new_path = get_doc_path(self.module, 'doctype', new)
|
||||
|
|
@ -414,7 +399,6 @@ class DocType(Document):
|
|||
self.rename_inside_controller(new, old, new_path)
|
||||
frappe.msgprint(_('Renamed files and replaced code in controllers, please check!'))
|
||||
|
||||
|
||||
def rename_inside_controller(self, new, old, new_path):
|
||||
for fname in ('{}.js', '{}.py', '{}_list.js', '{}_calendar.js', 'test_{}.py', 'test_{}.js'):
|
||||
fname = os.path.join(new_path, fname.format(frappe.scrub(new)))
|
||||
|
|
@ -440,7 +424,6 @@ class DocType(Document):
|
|||
if not (self.issingle and self.istable):
|
||||
self.preserve_naming_series_options_in_property_setter()
|
||||
|
||||
|
||||
def preserve_naming_series_options_in_property_setter(self):
|
||||
"""Preserve naming_series as property setter if it does not exist"""
|
||||
naming_series = self.get("fields", {"fieldname": "naming_series"})
|
||||
|
|
@ -460,7 +443,6 @@ class DocType(Document):
|
|||
if naming_series[0].default:
|
||||
make_property_setter(self.name, "naming_series", "default", naming_series[0].default, "Text", validate_fields_for_doctype=False)
|
||||
|
||||
|
||||
def before_export(self, docdict):
|
||||
# remove null and empty fields
|
||||
def remove_null_fields(o):
|
||||
|
|
@ -495,7 +477,8 @@ class DocType(Document):
|
|||
field_dict = list(filter(lambda d: d['fieldname'] == fieldname, docdict['fields']))
|
||||
if field_dict:
|
||||
new_field_dicts.append(field_dict[0])
|
||||
remaining_field_names.remove(fieldname)
|
||||
if fieldname in remaining_field_names:
|
||||
remaining_field_names.remove(fieldname)
|
||||
|
||||
for fieldname in remaining_field_names:
|
||||
field_dict = list(filter(lambda d: d['fieldname'] == fieldname, docdict['fields']))
|
||||
|
|
@ -505,7 +488,6 @@ class DocType(Document):
|
|||
except ValueError:
|
||||
pass
|
||||
|
||||
|
||||
@staticmethod
|
||||
def prepare_for_import(docdict):
|
||||
# set order of fields from field_order
|
||||
|
|
@ -517,7 +499,8 @@ class DocType(Document):
|
|||
field_dict = list(filter(lambda d: d['fieldname'] == fieldname, docdict.get('fields', [])))
|
||||
if field_dict:
|
||||
new_field_dicts.append(field_dict[0])
|
||||
remaining_field_names.remove(fieldname)
|
||||
if fieldname in remaining_field_names:
|
||||
remaining_field_names.remove(fieldname)
|
||||
|
||||
for fieldname in remaining_field_names:
|
||||
field_dict = list(filter(lambda d: d['fieldname'] == fieldname, docdict.get('fields', [])))
|
||||
|
|
@ -528,19 +511,16 @@ class DocType(Document):
|
|||
if "field_order" in docdict:
|
||||
del docdict["field_order"]
|
||||
|
||||
|
||||
def export_doc(self):
|
||||
"""Export to standard folder `[module]/doctype/[name]/[name].json`."""
|
||||
from frappe.modules.export_file import export_to_files
|
||||
export_to_files(record_list=[['DocType', self.name]], create_init=True)
|
||||
|
||||
|
||||
def import_doc(self):
|
||||
"""Import from standard folder `[module]/doctype/[name]/[name].json`."""
|
||||
from frappe.modules.import_module import import_from_files
|
||||
import_from_files(record_list=[[self.module, 'doctype', self.name]])
|
||||
|
||||
|
||||
def make_controller_template(self):
|
||||
"""Make boilerplate controller template."""
|
||||
make_boilerplate("controller._py", self)
|
||||
|
|
@ -557,7 +537,6 @@ class DocType(Document):
|
|||
make_boilerplate('templates/controller.html', self.as_dict())
|
||||
make_boilerplate('templates/controller_row.html', self.as_dict())
|
||||
|
||||
|
||||
def make_amendable(self):
|
||||
"""If is_submittable is set, add amended_from docfields."""
|
||||
if self.is_submittable:
|
||||
|
|
@ -573,7 +552,6 @@ class DocType(Document):
|
|||
"no_copy": 1
|
||||
})
|
||||
|
||||
|
||||
def make_repeatable(self):
|
||||
"""If allow_auto_repeat is set, add auto_repeat custom field."""
|
||||
if self.allow_auto_repeat:
|
||||
|
|
@ -642,14 +620,12 @@ class DocType(Document):
|
|||
})
|
||||
self.nsm_parent_field = parent_field_name
|
||||
|
||||
|
||||
def get_max_idx(self):
|
||||
"""Returns the highest `idx`"""
|
||||
max_idx = frappe.db.sql("""select max(idx) from `tabDocField` where parent = %s""",
|
||||
self.name)
|
||||
return max_idx and max_idx[0][0] or 0
|
||||
|
||||
|
||||
def validate_name(self, name=None):
|
||||
if not name:
|
||||
name = self.name
|
||||
|
|
@ -669,7 +645,6 @@ def validate_fields_for_doctype(doctype):
|
|||
doc.delete_duplicate_custom_fields()
|
||||
validate_fields(frappe.get_meta(doctype, cached=False))
|
||||
|
||||
|
||||
# this is separate because it is also called via custom field
|
||||
def validate_fields(meta):
|
||||
"""Validate doctype fields. Checks
|
||||
|
|
@ -693,29 +668,24 @@ def validate_fields(meta):
|
|||
def check_illegal_characters(fieldname):
|
||||
validate_column_name(fieldname)
|
||||
|
||||
|
||||
def check_invalid_fieldnames(docname, fieldname):
|
||||
invalid_fields = ('doctype',)
|
||||
if fieldname in invalid_fields:
|
||||
frappe.throw(_("{0}: Fieldname cannot be one of {1}")
|
||||
.format(docname, ", ".join([frappe.bold(d) for d in invalid_fields])))
|
||||
|
||||
|
||||
def check_unique_fieldname(docname, fieldname):
|
||||
duplicates = list(filter(None, map(lambda df: df.fieldname==fieldname and str(df.idx) or None, fields)))
|
||||
if len(duplicates) > 1:
|
||||
frappe.throw(_("{0}: Fieldname {1} appears multiple times in rows {2}").format(docname, fieldname, ", ".join(duplicates)), UniqueFieldnameError)
|
||||
|
||||
|
||||
def check_fieldname_length(fieldname):
|
||||
validate_column_length(fieldname)
|
||||
|
||||
|
||||
def check_illegal_mandatory(docname, d):
|
||||
if (d.fieldtype in no_value_fields) and d.fieldtype not in table_fields and d.reqd:
|
||||
frappe.throw(_("{0}: Field {1} of type {2} cannot be mandatory").format(docname, d.label, d.fieldtype), IllegalMandatoryError)
|
||||
|
||||
|
||||
def check_link_table_options(docname, d):
|
||||
if frappe.flags.in_patch: return
|
||||
if d.fieldtype in ("Link",) + table_fields:
|
||||
|
|
@ -734,28 +704,23 @@ def validate_fields(meta):
|
|||
# fix case
|
||||
d.options = options
|
||||
|
||||
|
||||
def check_hidden_and_mandatory(docname, d):
|
||||
if d.hidden and d.reqd and not d.default:
|
||||
frappe.throw(_("{0}: Field {1} in row {2} cannot be hidden and mandatory without default").format(docname, d.label, d.idx), HiddenAndMandatoryWithoutDefaultError)
|
||||
|
||||
|
||||
def check_width(d):
|
||||
if d.fieldtype == "Currency" and cint(d.width) < 100:
|
||||
frappe.throw(_("Max width for type Currency is 100px in row {0}").format(d.idx))
|
||||
|
||||
|
||||
def check_in_list_view(d):
|
||||
if d.in_list_view and (d.fieldtype in not_allowed_in_list_view):
|
||||
frappe.throw(_("'In List View' not allowed for type {0} in row {1}").format(d.fieldtype, d.idx))
|
||||
|
||||
|
||||
def check_in_global_search(d):
|
||||
if d.in_global_search and d.fieldtype in no_value_fields:
|
||||
frappe.throw(_("'In Global Search' not allowed for type {0} in row {1}")
|
||||
.format(d.fieldtype, d.idx))
|
||||
|
||||
|
||||
def check_dynamic_link_options(d):
|
||||
if d.fieldtype=="Dynamic Link":
|
||||
doctype_pointer = list(filter(lambda df: df.fieldname==d.options, fields))
|
||||
|
|
@ -763,7 +728,6 @@ def validate_fields(meta):
|
|||
or (doctype_pointer[0].fieldtype=="Link" and doctype_pointer[0].options!="DocType"):
|
||||
frappe.throw(_("Options 'Dynamic Link' type of field must point to another Link Field with options as 'DocType'"))
|
||||
|
||||
|
||||
def check_illegal_default(d):
|
||||
if d.fieldtype == "Check" and not d.default:
|
||||
d.default = '0'
|
||||
|
|
@ -772,12 +736,10 @@ def validate_fields(meta):
|
|||
if d.fieldtype == "Select" and d.default and (d.default not in d.options.split("\n")):
|
||||
frappe.throw(_("Default for {0} must be an option").format(d.fieldname))
|
||||
|
||||
|
||||
def check_precision(d):
|
||||
if d.fieldtype in ("Currency", "Float", "Percent") and d.precision is not None and not (1 <= cint(d.precision) <= 6):
|
||||
frappe.throw(_("Precision should be between 1 and 6"))
|
||||
|
||||
|
||||
def check_unique_and_text(docname, d):
|
||||
if meta.issingle:
|
||||
d.unique = 0
|
||||
|
|
@ -799,7 +761,6 @@ def validate_fields(meta):
|
|||
if d.search_index and d.fieldtype in ("Text", "Long Text", "Small Text", "Code", "Text Editor"):
|
||||
frappe.throw(_("{0}:Fieldtype {1} for {2} cannot be indexed").format(docname, d.fieldtype, d.label), CannotIndexedError)
|
||||
|
||||
|
||||
def check_fold(fields):
|
||||
fold_exists = False
|
||||
for i, f in enumerate(fields):
|
||||
|
|
@ -814,7 +775,6 @@ def validate_fields(meta):
|
|||
else:
|
||||
frappe.throw(_("Fold can not be at the end of the form"))
|
||||
|
||||
|
||||
def check_search_fields(meta, fields):
|
||||
"""Throw exception if `search_fields` don't contain valid fields."""
|
||||
if not meta.search_fields:
|
||||
|
|
@ -831,7 +791,6 @@ def validate_fields(meta):
|
|||
(fieldname not in fieldname_list):
|
||||
frappe.throw(_("Search field {0} is not valid").format(fieldname))
|
||||
|
||||
|
||||
def check_title_field(meta):
|
||||
"""Throw exception if `title_field` isn't a valid fieldname."""
|
||||
if not meta.get("title_field"):
|
||||
|
|
@ -858,7 +817,6 @@ def validate_fields(meta):
|
|||
_validate_title_field_pattern(df.options)
|
||||
_validate_title_field_pattern(df.default)
|
||||
|
||||
|
||||
def check_image_field(meta):
|
||||
'''check image_field exists and is of type "Attach Image"'''
|
||||
if not meta.image_field:
|
||||
|
|
@ -870,7 +828,6 @@ def validate_fields(meta):
|
|||
if df[0].fieldtype != 'Attach Image':
|
||||
frappe.throw(_("Image field must be of type Attach Image"), InvalidFieldNameError)
|
||||
|
||||
|
||||
def check_is_published_field(meta):
|
||||
if not meta.is_published_field:
|
||||
return
|
||||
|
|
@ -878,7 +835,6 @@ def validate_fields(meta):
|
|||
if meta.is_published_field not in fieldname_list:
|
||||
frappe.throw(_("Is Published Field must be a valid fieldname"), InvalidFieldNameError)
|
||||
|
||||
|
||||
def check_timeline_field(meta):
|
||||
if not meta.timeline_field:
|
||||
return
|
||||
|
|
@ -890,7 +846,6 @@ def validate_fields(meta):
|
|||
if df.fieldtype not in ("Link", "Dynamic Link"):
|
||||
frappe.throw(_("Timeline field must be a Link or Dynamic Link"), InvalidFieldNameError)
|
||||
|
||||
|
||||
def check_sort_field(meta):
|
||||
'''Validate that sort_field(s) is a valid field'''
|
||||
if meta.sort_field:
|
||||
|
|
@ -903,7 +858,6 @@ def validate_fields(meta):
|
|||
frappe.throw(_("Sort field {0} must be a valid fieldname").format(fieldname),
|
||||
InvalidFieldNameError)
|
||||
|
||||
|
||||
def check_illegal_depends_on_conditions(docfield):
|
||||
''' assignment operation should not be allowed in the depends on condition.'''
|
||||
depends_on_fields = ["depends_on", "collapsible_depends_on", "mandatory_depends_on", "read_only_depends_on"]
|
||||
|
|
@ -913,7 +867,6 @@ def validate_fields(meta):
|
|||
re.match("""[\w\.:_]+\s*={1}\s*[\w\.@'"]+""", depends_on):
|
||||
frappe.throw(_("Invalid {0} condition").format(frappe.unscrub(field)), frappe.ValidationError)
|
||||
|
||||
|
||||
def check_table_multiselect_option(docfield):
|
||||
'''check if the doctype provided in Option has atleast 1 Link field'''
|
||||
if not docfield.fieldtype == 'Table MultiSelect': return
|
||||
|
|
@ -926,7 +879,6 @@ def validate_fields(meta):
|
|||
frappe.throw(_('DocType <b>{0}</b> provided for the field <b>{1}</b> must have atleast one Link field')
|
||||
.format(doctype, docfield.fieldname), frappe.ValidationError)
|
||||
|
||||
|
||||
def scrub_options_in_select(field):
|
||||
"""Strip options for whitespaces"""
|
||||
|
||||
|
|
@ -938,11 +890,20 @@ def validate_fields(meta):
|
|||
options_list.append(_option)
|
||||
field.options = '\n'.join(options_list)
|
||||
|
||||
|
||||
def scrub_fetch_from(field):
|
||||
if hasattr(field, 'fetch_from') and getattr(field, 'fetch_from'):
|
||||
field.fetch_from = field.fetch_from.strip('\n').strip()
|
||||
|
||||
def validate_data_field_type(docfield):
|
||||
if docfield.fieldtype == "Data" and not (docfield.oldfieldtype and docfield.oldfieldtype != "Data"):
|
||||
if docfield.options and (docfield.options not in data_field_options):
|
||||
df_str = frappe.bold(_(docfield.label))
|
||||
text_str = _("{0} is an invalid Data field.").format(df_str) + "<br>" * 2 + _("Only Options allowed for Data field are:") + "<br>"
|
||||
df_options_str = "<ul><li>" + "</li><li>".join([_(x) for x in data_field_options]) + "</ul>"
|
||||
|
||||
frappe.msgprint(text_str + df_options_str, title="Invalid Data Field", raise_exception=True)
|
||||
|
||||
|
||||
fields = meta.get("fields")
|
||||
fieldname_list = [d.fieldname for d in fields]
|
||||
|
||||
|
|
@ -955,7 +916,7 @@ def validate_fields(meta):
|
|||
if not d.permlevel: d.permlevel = 0
|
||||
if d.fieldtype not in table_fields: d.allow_bulk_edit = 0
|
||||
if not d.fieldname:
|
||||
d.fieldname = d.fieldname.lower()
|
||||
d.fieldname = d.fieldname.lower().strip('?')
|
||||
|
||||
check_illegal_characters(d.fieldname)
|
||||
check_invalid_fieldnames(meta.get("name"), d.fieldname)
|
||||
|
|
@ -973,6 +934,7 @@ def validate_fields(meta):
|
|||
check_table_multiselect_option(d)
|
||||
scrub_options_in_select(d)
|
||||
scrub_fetch_from(d)
|
||||
validate_data_field_type(d)
|
||||
|
||||
check_fold(fields)
|
||||
check_search_fields(meta, fields)
|
||||
|
|
@ -982,7 +944,6 @@ def validate_fields(meta):
|
|||
check_sort_field(meta)
|
||||
check_image_field(meta)
|
||||
|
||||
|
||||
def validate_permissions_for_doctype(doctype, for_remove=False):
|
||||
"""Validates if permissions are set correctly."""
|
||||
doctype = frappe.get_doc("DocType", doctype)
|
||||
|
|
@ -994,7 +955,6 @@ def validate_permissions_for_doctype(doctype, for_remove=False):
|
|||
|
||||
clear_permissions_cache(doctype.name)
|
||||
|
||||
|
||||
def clear_permissions_cache(doctype):
|
||||
frappe.clear_cache(doctype=doctype)
|
||||
delete_notification_count_for(doctype)
|
||||
|
|
@ -1009,7 +969,6 @@ def clear_permissions_cache(doctype):
|
|||
""", doctype):
|
||||
frappe.clear_cache(user=user)
|
||||
|
||||
|
||||
def validate_permissions(doctype, for_remove=False):
|
||||
permissions = doctype.get("permissions")
|
||||
if not permissions:
|
||||
|
|
@ -1103,7 +1062,6 @@ def validate_permissions(doctype, for_remove=False):
|
|||
check_level_zero_is_set(d)
|
||||
remove_rights_for_single(d)
|
||||
|
||||
|
||||
def make_module_and_roles(doc, perm_fieldname="permissions"):
|
||||
"""Make `Module Def` and `Role` records if already not made. Called while installing."""
|
||||
try:
|
||||
|
|
@ -1134,7 +1092,6 @@ def make_module_and_roles(doc, perm_fieldname="permissions"):
|
|||
else:
|
||||
raise
|
||||
|
||||
|
||||
def check_if_fieldname_conflicts_with_methods(doctype, fieldname):
|
||||
doc = frappe.get_doc({"doctype": doctype})
|
||||
method_list = [method for method in dir(doc) if isinstance(method, str) and callable(getattr(doc, method))]
|
||||
|
|
@ -1142,6 +1099,38 @@ def check_if_fieldname_conflicts_with_methods(doctype, fieldname):
|
|||
if fieldname in method_list:
|
||||
frappe.throw(_("Fieldname {0} conflicting with meta object").format(fieldname))
|
||||
|
||||
|
||||
def clear_linked_doctype_cache():
|
||||
frappe.cache().delete_value('linked_doctypes_without_ignore_user_permissions_enabled')
|
||||
|
||||
def check_email_append_to(doc):
|
||||
if not hasattr(doc, "email_append_to") or not doc.email_append_to:
|
||||
return
|
||||
|
||||
# Subject Field
|
||||
doc.subject_field = doc.subject_field.strip() if doc.subject_field else None
|
||||
subject_field = get_field(doc, doc.subject_field)
|
||||
|
||||
if doc.subject_field and not subject_field:
|
||||
frappe.throw(_("Select a valid Subject field for creating documents from Email"))
|
||||
|
||||
if subject_field and subject_field.fieldtype not in ["Data", "Text", "Long Text", "Small Text", "Text Editor"]:
|
||||
frappe.throw(_("Subject Field type should be Data, Text, Long Text, Small Text, Text Editor"))
|
||||
|
||||
# Sender Field is mandatory
|
||||
doc.sender_field = doc.sender_field.strip() if doc.sender_field else None
|
||||
sender_field = get_field(doc, doc.sender_field)
|
||||
|
||||
if doc.sender_field and not sender_field:
|
||||
frappe.throw(_("Select a valid Sender Field for creating documents from Email"))
|
||||
|
||||
if not sender_field.options == "Email":
|
||||
frappe.throw(_("Sender Field should have Email in options"))
|
||||
|
||||
|
||||
def get_field(doc, fieldname):
|
||||
if not (doc or fieldname):
|
||||
return
|
||||
|
||||
for field in doc.fields:
|
||||
if field.fieldname == fieldname:
|
||||
return field
|
||||
|
|
|
|||
|
|
@ -113,6 +113,32 @@ class TestDocType(unittest.TestCase):
|
|||
if condition:
|
||||
self.assertFalse(re.match(pattern, condition))
|
||||
|
||||
def test_data_field_options(self):
|
||||
doctype_name = "Test Data Fields"
|
||||
valid_data_field_options = frappe.model.data_field_options + ("",)
|
||||
invalid_data_field_options = ("Invalid Option 1", frappe.utils.random_string(5))
|
||||
|
||||
for field_option in (valid_data_field_options + invalid_data_field_options):
|
||||
test_doctype = frappe.get_doc({
|
||||
"doctype": "DocType",
|
||||
"name": doctype_name,
|
||||
"module": "Core",
|
||||
"custom": 1,
|
||||
"fields": [{
|
||||
"fieldname": "{0}_field".format(field_option),
|
||||
"fieldtype": "Data",
|
||||
"options": field_option
|
||||
}]
|
||||
})
|
||||
|
||||
if field_option in invalid_data_field_options:
|
||||
# assert that only data options in frappe.model.data_field_options are valid
|
||||
self.assertRaises(frappe.ValidationError, test_doctype.insert)
|
||||
else:
|
||||
test_doctype.insert()
|
||||
self.assertEqual(test_doctype.name, doctype_name)
|
||||
test_doctype.delete()
|
||||
|
||||
def test_sync_field_order(self):
|
||||
from frappe.modules.import_file import get_file_path
|
||||
import os
|
||||
|
|
@ -349,4 +375,4 @@ class TestDocType(unittest.TestCase):
|
|||
# delete doctype
|
||||
link_doc.delete()
|
||||
doc.delete()
|
||||
frappe.db.commit()
|
||||
frappe.db.commit()
|
||||
|
|
|
|||
|
|
@ -517,7 +517,7 @@ class File(Document):
|
|||
delete_file(self.thumbnail_url)
|
||||
|
||||
def is_downloadable(self):
|
||||
return self.is_private and has_permission(self, 'read')
|
||||
return has_permission(self, 'read')
|
||||
|
||||
def get_extension(self):
|
||||
'''returns split filename and extension'''
|
||||
|
|
@ -608,8 +608,7 @@ def get_local_image(file_url):
|
|||
try:
|
||||
image = Image.open(file_path)
|
||||
except IOError:
|
||||
frappe.msgprint(_("Unable to read file format for {0}").format(file_url))
|
||||
raise
|
||||
frappe.msgprint(_("Unable to read file format for {0}").format(file_url), raise_exception=True)
|
||||
|
||||
content = None
|
||||
|
||||
|
|
@ -712,7 +711,11 @@ def remove_all(dt, dn, from_delete=False):
|
|||
|
||||
|
||||
def has_permission(doc, ptype=None, user=None):
|
||||
permission = True
|
||||
has_access = False
|
||||
user = user or frappe.session.user
|
||||
|
||||
if not doc.is_private or doc.owner == user or user == 'Administrator':
|
||||
has_access = True
|
||||
|
||||
if doc.attached_to_doctype and doc.attached_to_name:
|
||||
attached_to_doctype = doc.attached_to_doctype
|
||||
|
|
@ -722,20 +725,20 @@ def has_permission(doc, ptype=None, user=None):
|
|||
ref_doc = frappe.get_doc(attached_to_doctype, attached_to_name)
|
||||
|
||||
if ptype in ['write', 'create', 'delete']:
|
||||
permission = ref_doc.has_permission('write')
|
||||
has_access = ref_doc.has_permission('write')
|
||||
|
||||
if ptype == 'delete' and permission == False:
|
||||
if ptype == 'delete' and not has_access:
|
||||
frappe.throw(_("Cannot delete file as it belongs to {0} {1} for which you do not have permissions").format(
|
||||
doc.attached_to_doctype, doc.attached_to_name),
|
||||
frappe.PermissionError)
|
||||
else:
|
||||
permission = ref_doc.has_permission('read')
|
||||
has_access = ref_doc.has_permission('read')
|
||||
except frappe.DoesNotExistError:
|
||||
# if parent doc is not created before file is created
|
||||
# we cannot check its permission so allow the file
|
||||
permission = True
|
||||
# we cannot check its permission so we will use file's permission
|
||||
pass
|
||||
|
||||
return permission
|
||||
return has_access
|
||||
|
||||
|
||||
def remove_file_by_url(file_url, doctype=None, name=None):
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "field:language_code",
|
||||
"creation": "2014-08-22 16:12:17.249590",
|
||||
|
|
@ -41,7 +42,9 @@
|
|||
}
|
||||
],
|
||||
"icon": "fa fa-globe",
|
||||
"modified": "2019-07-19 16:32:12.652550",
|
||||
"in_create": 1,
|
||||
"links": [],
|
||||
"modified": "2020-04-16 22:11:33.066852",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Language",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"actions": [],
|
||||
"autoname": "REP.#####",
|
||||
"creation": "2018-06-25 18:39:11.152960",
|
||||
"doctype": "DocType",
|
||||
|
|
@ -101,7 +102,8 @@
|
|||
}
|
||||
],
|
||||
"in_create": 1,
|
||||
"modified": "2019-09-18 04:00:55.644257",
|
||||
"links": [],
|
||||
"modified": "2020-03-05 10:52:56.598365",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Prepared Report",
|
||||
|
|
@ -118,6 +120,15 @@
|
|||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Prepared Report User",
|
||||
"share": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
|
|
|
|||
|
|
@ -98,3 +98,34 @@ def download_attachment(dn):
|
|||
attached_file = frappe.get_doc("File", attachment.name)
|
||||
frappe.local.response.filecontent = gzip_decompress(attached_file.get_content())
|
||||
frappe.local.response.type = "binary"
|
||||
|
||||
|
||||
def get_permission_query_condition(user):
|
||||
if not user: user = frappe.session.user
|
||||
if user == "Administrator":
|
||||
return None
|
||||
|
||||
from frappe.utils.user import UserPermissions
|
||||
user = UserPermissions(user)
|
||||
|
||||
if "System Manager" in user.roles:
|
||||
return None
|
||||
|
||||
reports = [frappe.db.escape(report) for report in user.get_all_reports().keys()]
|
||||
|
||||
return """`tabPrepared Report`.ref_report_doctype in ({reports})"""\
|
||||
.format(reports=','.join(reports))
|
||||
|
||||
|
||||
def has_permission(doc, user):
|
||||
if not user: user = frappe.session.user
|
||||
if user == "Administrator":
|
||||
return True
|
||||
|
||||
from frappe.utils.user import UserPermissions
|
||||
user = UserPermissions(user)
|
||||
|
||||
if "System Manager" in user.roles:
|
||||
return True
|
||||
|
||||
return doc.ref_report_doctype in user.get_all_reports().keys()
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import frappe
|
|||
import json, datetime
|
||||
from frappe import _, scrub
|
||||
import frappe.desk.query_report
|
||||
from frappe.utils import cint
|
||||
from frappe.utils import cint, cstr
|
||||
from frappe.model.document import Document
|
||||
from frappe.modules.export_file import export_to_files
|
||||
from frappe.modules import make_boilerplate
|
||||
|
|
@ -92,6 +92,18 @@ class Report(Document):
|
|||
make_boilerplate("controller.py", self, {"name": self.name})
|
||||
make_boilerplate("controller.js", self, {"name": self.name})
|
||||
|
||||
def execute_query_report(self, filters):
|
||||
if not self.query:
|
||||
frappe.throw(_("Must specify a Query to run"), title=_('Report Document Error'))
|
||||
|
||||
if not self.query.lower().startswith("select"):
|
||||
frappe.throw(_("Query must be a SELECT"), title=_('Report Document Error'))
|
||||
|
||||
result = [list(t) for t in frappe.db.sql(self.query, filters)]
|
||||
columns = [cstr(c[0]) for c in frappe.db.get_description()]
|
||||
|
||||
return [columns, result]
|
||||
|
||||
def execute_script_report(self, filters):
|
||||
# save the timestamp to automatically set to prepared
|
||||
threshold = 30
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Role Profile', {
|
||||
setup: function(frm) {
|
||||
refresh: function(frm) {
|
||||
if(has_common(frappe.user_roles, ["Administrator", "System Manager"])) {
|
||||
if(!frm.roles_editor) {
|
||||
var role_area = $('<div style="min-height: 300px">')
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
"field_order": [
|
||||
"stopped",
|
||||
"method",
|
||||
"server_script",
|
||||
"frequency",
|
||||
"cron_format",
|
||||
"last_execution",
|
||||
|
|
@ -63,6 +64,14 @@
|
|||
"options": "All\nHourly\nHourly Long\nDaily\nDaily Long\nWeekly\nWeekly Long\nMonthly\nMonthly Long\nCron\nYearly\nAnnual",
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "server_script",
|
||||
"fieldtype": "Link",
|
||||
"label": "Server Script",
|
||||
"options": "Server Script",
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
}
|
||||
],
|
||||
"in_create": 1,
|
||||
|
|
@ -72,7 +81,7 @@
|
|||
"link_fieldname": "scheduled_job_type"
|
||||
}
|
||||
],
|
||||
"modified": "2019-12-09 11:10:21.259929",
|
||||
"modified": "2020-04-05 17:27:33.480562",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Scheduled Job Type",
|
||||
|
|
|
|||
|
|
@ -70,7 +70,12 @@ class ScheduledJobType(Document):
|
|||
self.scheduler_log = None
|
||||
try:
|
||||
self.log_status('Start')
|
||||
frappe.get_attr(self.method)()
|
||||
if self.server_script:
|
||||
script_name = frappe.db.get_value("Server Script", self.server_script)
|
||||
if script_name:
|
||||
frappe.get_doc('Server Script', script_name).execute_scheduled_method()
|
||||
else:
|
||||
frappe.get_attr(self.method)()
|
||||
frappe.db.commit()
|
||||
self.log_status('Complete')
|
||||
except Exception:
|
||||
|
|
|
|||
|
|
@ -2,7 +2,45 @@
|
|||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Server Script', {
|
||||
// refresh: function(frm) {
|
||||
refresh: function(frm) {
|
||||
if(frm.doc.script_type === 'Scheduler Event' && !frm.doc.disabled){
|
||||
frm.add_custom_button('Schedule Script', function() {
|
||||
var d = new frappe.ui.Dialog({
|
||||
title: "Schedule Script Execution",
|
||||
fields: [
|
||||
{
|
||||
fieldname: "event_type",
|
||||
label: __('Select Event Type'),
|
||||
fieldtype: "Select",
|
||||
options: "All\nHourly\nDaily\nWeekly\nMonthly\nYearly\nHourly Long\nDaily Long\nWeekly Long\nMonthly Long"
|
||||
},
|
||||
],
|
||||
primary_action_label: __('Schedule Script'),
|
||||
primary_action: () => {
|
||||
d.get_primary_btn().attr('disabled', true);
|
||||
var data = d.get_values();
|
||||
d.hide();
|
||||
if(data) {
|
||||
frm.events.schedule_script(frm, data);
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
d.show();
|
||||
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
schedule_script(frm, data){
|
||||
frm.call({
|
||||
method: "frappe.core.doctype.server_script.server_script.setup_scheduler_events",
|
||||
args: {
|
||||
'script_name': frm.doc.name,
|
||||
'frequency': data.event_type
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// }
|
||||
});
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Script Type",
|
||||
"options": "DocType Event\nAPI",
|
||||
"options": "DocType Event\nScheduler Event\nAPI",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
|
|
@ -75,7 +75,7 @@
|
|||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2019-12-17 12:55:07.389775",
|
||||
"modified": "2020-04-06 11:24:38.161555",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Server Script",
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ from __future__ import unicode_literals
|
|||
import frappe
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils.safe_exec import safe_exec
|
||||
from frappe import _
|
||||
|
||||
|
||||
class ServerScript(Document):
|
||||
@staticmethod
|
||||
|
|
@ -31,3 +33,39 @@ class ServerScript(Document):
|
|||
# execute event
|
||||
safe_exec(self.script, None, dict(doc = doc))
|
||||
|
||||
def execute_scheduled_method(self):
|
||||
if self.script_type == 'Scheduler Event':
|
||||
safe_exec(self.script)
|
||||
else:
|
||||
# wrong report type!
|
||||
raise frappe.DoesNotExistError
|
||||
|
||||
@frappe.whitelist()
|
||||
def setup_scheduler_events(script_name, frequency):
|
||||
method = frappe.scrub(script_name) + '_' + frequency.lower()
|
||||
scheduled_script = frappe.db.get_value('Scheduled Job Type',
|
||||
dict(method=method))
|
||||
|
||||
if not scheduled_script:
|
||||
doc = frappe.get_doc(dict(
|
||||
doctype = 'Scheduled Job Type',
|
||||
method = method,
|
||||
frequency = frequency,
|
||||
server_script = script_name
|
||||
))
|
||||
|
||||
doc.insert()
|
||||
|
||||
frappe.msgprint(_('Enabled scheduled execution for script {0}').format(script_name))
|
||||
|
||||
else:
|
||||
doc = frappe.get_doc('Scheduled Job Type', scheduled_script)
|
||||
doc.update(dict(
|
||||
doctype = 'Scheduled Job Type',
|
||||
method = method,
|
||||
frequency = frequency,
|
||||
server_script = script_name
|
||||
))
|
||||
doc.save()
|
||||
|
||||
frappe.msgprint(_('Scheduled execution for script {0} has updated').format(script_name))
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ def get_server_script_map():
|
|||
script_map.setdefault(script.reference_doctype, {}).setdefault(script.doctype_event, []).append(script.name)
|
||||
else:
|
||||
script_map.setdefault('_api', {})[script.api_method] = script.name
|
||||
|
||||
frappe.cache().set_value('server_script_map', script_map)
|
||||
|
||||
return script_map
|
||||
return script_map
|
||||
|
|
@ -97,6 +97,50 @@ frappe.ui.form.on('User', {
|
|||
});
|
||||
}, __("Password"));
|
||||
|
||||
if (frappe.user.has_role("System Manager")) {
|
||||
frappe.db.get_single_value("LDAP Settings", "enabled").then((value) => {
|
||||
if (value === 1 && frm.doc.name != "Administrator") {
|
||||
frm.add_custom_button(__("Reset LDAP Password"), function() {
|
||||
const d = new frappe.ui.Dialog({
|
||||
title: __("Reset LDAP Password"),
|
||||
fields: [
|
||||
{
|
||||
label: __("New Password"),
|
||||
fieldtype: "Password",
|
||||
fieldname: "new_password",
|
||||
reqd: 1
|
||||
},
|
||||
{
|
||||
label: __("Confirm New Password"),
|
||||
fieldtype: "Password",
|
||||
fieldname: "confirm_password",
|
||||
reqd: 1
|
||||
},
|
||||
{
|
||||
label: __("Logout All Sessions"),
|
||||
fieldtype: "Check",
|
||||
fieldname: "logout_sessions"
|
||||
}
|
||||
],
|
||||
primary_action: (values) => {
|
||||
d.hide();
|
||||
if (values.new_password !== values.confirm_password) {
|
||||
frappe.throw(__("Passwords do not match!"));
|
||||
}
|
||||
frappe.call(
|
||||
"frappe.integrations.doctype.ldap_settings.ldap_settings.reset_password", {
|
||||
user: frm.doc.email,
|
||||
password: values.new_password,
|
||||
logout: values.logout_sessions
|
||||
});
|
||||
}
|
||||
});
|
||||
d.show();
|
||||
}, __("Password"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
frm.add_custom_button(__("Reset OTP Secret"), function() {
|
||||
frappe.call({
|
||||
method: "frappe.core.doctype.user.user.reset_otp_secret",
|
||||
|
|
|
|||
|
|
@ -238,12 +238,14 @@
|
|||
{
|
||||
"fieldname": "phone",
|
||||
"fieldtype": "Data",
|
||||
"label": "Phone"
|
||||
"label": "Phone",
|
||||
"options": "Phone"
|
||||
},
|
||||
{
|
||||
"fieldname": "mobile_no",
|
||||
"fieldtype": "Data",
|
||||
"label": "Mobile No",
|
||||
"options": "Phone",
|
||||
"unique": 1
|
||||
},
|
||||
{
|
||||
|
|
@ -588,7 +590,7 @@
|
|||
"image_field": "user_image",
|
||||
"links": [],
|
||||
"max_attachments": 5,
|
||||
"modified": "2020-03-23 22:59:26.154985",
|
||||
"modified": "2020-04-08 12:27:36.716490",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "User",
|
||||
|
|
|
|||
|
|
@ -205,7 +205,7 @@ class User(Document):
|
|||
_update_password(user=self.name, pwd=new_password,
|
||||
logout_all_sessions=self.logout_all_sessions)
|
||||
|
||||
if not self.flags.no_welcome_mail and self.send_welcome_email:
|
||||
if not self.flags.no_welcome_mail and cint(self.send_welcome_email):
|
||||
self.send_welcome_mail_to_user()
|
||||
self.flags.email_sent = 1
|
||||
if frappe.session.user != 'Guest':
|
||||
|
|
@ -551,6 +551,7 @@ def update_password(new_password, logout_all_sessions=0, key=None, old_password=
|
|||
|
||||
res = _get_user_for_update_password(key, old_password)
|
||||
if res.get('message'):
|
||||
frappe.local.response.http_status_code = 410
|
||||
return res['message']
|
||||
else:
|
||||
user = res['user']
|
||||
|
|
@ -577,7 +578,7 @@ def update_password(new_password, logout_all_sessions=0, key=None, old_password=
|
|||
return redirect_url if redirect_url else "/"
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def test_password_strength(new_password, key=None, old_password=None, user_data=[]):
|
||||
def test_password_strength(new_password, key=None, old_password=None, user_data=None):
|
||||
from frappe.utils.password_strength import test_password_strength as _test_password_strength
|
||||
|
||||
password_policy = frappe.db.get_value("System Settings", None,
|
||||
|
|
@ -718,7 +719,7 @@ def _get_user_for_update_password(key, old_password):
|
|||
user = frappe.db.get_value("User", {"reset_password_key": key})
|
||||
if not user:
|
||||
return {
|
||||
'message': _("Cannot Update: Incorrect / Expired Link.")
|
||||
'message': _("The Link specified has either been used before or Invalid")
|
||||
}
|
||||
|
||||
elif old_password:
|
||||
|
|
|
|||
|
|
@ -1,201 +1,63 @@
|
|||
{
|
||||
"allow_copy": 0,
|
||||
"allow_guest_to_view": 0,
|
||||
"allow_import": 0,
|
||||
"allow_rename": 0,
|
||||
"beta": 0,
|
||||
"creation": "2016-03-30 10:04:25.828742",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "",
|
||||
"editable_grid": 1,
|
||||
"actions": [],
|
||||
"creation": "2016-03-30 10:04:25.828742",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"email_account",
|
||||
"email_id",
|
||||
"column_break_3",
|
||||
"awaiting_password",
|
||||
"enable_outgoing"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "email_account",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Email Account",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Email Account",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "email_account",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Email Account",
|
||||
"options": "Email Account",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fetch_from": "email_account.email_id",
|
||||
"fieldname": "email_id",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Email ID",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 1,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "email_id",
|
||||
"fieldtype": "Data",
|
||||
"label": "Email ID",
|
||||
"options": "Email",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "column_break_3",
|
||||
"fieldtype": "Column Break",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "column_break_3",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fetch_from": "email_account.awaiting_password",
|
||||
"fieldname": "awaiting_password",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Awaiting Password",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 1,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"default": "0",
|
||||
"fetch_from": "email_account.awaiting_password",
|
||||
"fieldname": "awaiting_password",
|
||||
"fieldtype": "Check",
|
||||
"in_list_view": 1,
|
||||
"label": "Awaiting Password",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fetch_from": "email_account.enable_outgoing",
|
||||
"fieldname": "enable_outgoing",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Enable Outgoing",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 1,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"default": "0",
|
||||
"fetch_from": "email_account.enable_outgoing",
|
||||
"fieldname": "enable_outgoing",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enable Outgoing",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"has_web_view": 0,
|
||||
"hide_heading": 0,
|
||||
"hide_toolbar": 0,
|
||||
"idx": 0,
|
||||
"image_view": 0,
|
||||
"in_create": 0,
|
||||
"is_submittable": 0,
|
||||
"issingle": 0,
|
||||
"istable": 1,
|
||||
"max_attachments": 0,
|
||||
"modified": "2018-05-25 22:43:34.045787",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "User Email",
|
||||
"name_case": "",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 0,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"show_name_in_global_search": 0,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 0,
|
||||
"track_seen": 0
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-04-06 19:19:12.130246",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "User Email",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC"
|
||||
}
|
||||
0
frappe/core/doctype/video/__init__.py
Normal file
0
frappe/core/doctype/video/__init__.py
Normal file
10
frappe/core/doctype/video/test_video.py
Normal file
10
frappe/core/doctype/video/test_video.py
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2020, Frappe Technologies and Contributors
|
||||
# See license.txt
|
||||
from __future__ import unicode_literals
|
||||
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
class TestVideo(unittest.TestCase):
|
||||
pass
|
||||
8
frappe/core/doctype/video/video.js
Normal file
8
frappe/core/doctype/video/video.js
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
// Copyright (c) 2020, Frappe Technologies and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Video', {
|
||||
// refresh: function(frm) {
|
||||
|
||||
// }
|
||||
});
|
||||
106
frappe/core/doctype/video/video.json
Normal file
106
frappe/core/doctype/video/video.json
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
{
|
||||
"actions": [],
|
||||
"allow_import": 1,
|
||||
"allow_rename": 1,
|
||||
"autoname": "field:title",
|
||||
"creation": "2018-10-17 05:47:13.087395",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"title",
|
||||
"provider",
|
||||
"url",
|
||||
"column_break_4",
|
||||
"publish_date",
|
||||
"duration",
|
||||
"section_break_7",
|
||||
"description"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "title",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Title",
|
||||
"reqd": 1,
|
||||
"unique": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "provider",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Provider",
|
||||
"options": "YouTube\nVimeo",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "url",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "URL",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_4",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "publish_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Publish Date"
|
||||
},
|
||||
{
|
||||
"fieldname": "duration",
|
||||
"fieldtype": "Data",
|
||||
"label": "Duration"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_7",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Text Editor",
|
||||
"in_list_view": 1,
|
||||
"label": "Description",
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2020-04-22 12:09:49.057403",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Video",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"if_owner": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "All",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
10
frappe/core/doctype/video/video.py
Normal file
10
frappe/core/doctype/video/video.py
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2020, Frappe Technologies and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
class Video(Document):
|
||||
pass
|
||||
|
|
@ -28,6 +28,7 @@ def get_info(show_failed=False):
|
|||
if j.kwargs.get('site')==frappe.local.site:
|
||||
jobs.append({
|
||||
'job_name': j.kwargs.get('kwargs', {}).get('playbook_method') \
|
||||
or j.kwargs.get('kwargs', {}).get('job_type') \
|
||||
or str(j.kwargs.get('job_name')),
|
||||
'status': j.get_status(), 'queue': name,
|
||||
'creation': format_datetime(convert_utc_to_user_timezone(j.created_at)),
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ class Dashboard {
|
|||
}
|
||||
|
||||
show_dashboard(current_dashboard_name) {
|
||||
if(this.dashboard_name !== current_dashboard_name) {
|
||||
if (this.dashboard_name !== current_dashboard_name) {
|
||||
this.dashboard_name = current_dashboard_name;
|
||||
let title = this.dashboard_name;
|
||||
if (!this.dashboard_name.toLowerCase().includes(__('dashboard'))) {
|
||||
|
|
@ -76,30 +76,48 @@ class Dashboard {
|
|||
}
|
||||
|
||||
refresh() {
|
||||
this.get_dashboard_doc().then((doc) => {
|
||||
this.dashboard_doc = doc;
|
||||
this.charts = this.dashboard_doc.charts
|
||||
.map(chart => {
|
||||
return {
|
||||
chart_name: chart.chart,
|
||||
label: chart.chart,
|
||||
...chart
|
||||
}
|
||||
});
|
||||
this.get_permitted_dashboard_charts().then(charts => {
|
||||
if (!charts.length) {
|
||||
frappe.msgprint(__('No Permitted Charts on this Dashboard'), __('No Permitted Charts'))
|
||||
}
|
||||
|
||||
this.chart_group = new frappe.widget.WidgetGroup({
|
||||
title: null,
|
||||
container: this.container,
|
||||
type: "chart",
|
||||
columns: 2,
|
||||
allow_sorting: false,
|
||||
widgets: this.charts,
|
||||
});
|
||||
frappe.dashboard_utils.get_dashboard_settings().then((settings) => {
|
||||
let chart_config = settings.chart_config? JSON.parse(settings.chart_config): {};
|
||||
this.charts =
|
||||
charts.map(chart => {
|
||||
return {
|
||||
chart_name: chart.chart,
|
||||
label: chart.chart,
|
||||
chart_settings: chart_config[chart.chart] || {},
|
||||
...chart
|
||||
}
|
||||
});
|
||||
this.chart_group = new frappe.widget.WidgetGroup({
|
||||
title: null,
|
||||
container: this.container,
|
||||
type: "chart",
|
||||
columns: 2,
|
||||
options: {
|
||||
allow_sorting: false,
|
||||
allow_create: false,
|
||||
allow_delete: false,
|
||||
allow_hiding: false,
|
||||
allow_edit: false,
|
||||
},
|
||||
widgets: this.charts,
|
||||
});
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
get_dashboard_doc() {
|
||||
return frappe.model.with_doc('Dashboard', this.dashboard_name);
|
||||
get_permitted_dashboard_charts() {
|
||||
return frappe.xcall(
|
||||
'frappe.desk.doctype.dashboard.dashboard.get_permitted_charts',
|
||||
{
|
||||
dashboard_name: this.dashboard_name
|
||||
}).then(charts => {
|
||||
return charts;
|
||||
});
|
||||
}
|
||||
|
||||
set_dropdown() {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
"docstatus": 0,
|
||||
"doctype": "Page",
|
||||
"idx": 0,
|
||||
"modified": "2019-01-08 19:19:48.073410",
|
||||
"modified": "2020-03-26 13:30:44.603948",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "dashboard",
|
||||
|
|
|
|||
|
|
@ -217,6 +217,7 @@ frappe.PermissionEngine = Class.extend({
|
|||
|
||||
me.rights.forEach(r => {
|
||||
if (!d.is_submittable && ['submit', 'cancel', 'amend'].includes(r)) return;
|
||||
if (d.in_create && ['create', 'write', 'delete'].includes(r)) return;
|
||||
me.add_check(perm_container, d, r);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ def get_permissions(doctype=None, role=None):
|
|||
meta = frappe.get_meta(d.parent)
|
||||
if meta:
|
||||
d.is_submittable = meta.is_submittable
|
||||
d.in_create = meta.in_create
|
||||
|
||||
return out
|
||||
|
||||
|
|
|
|||
|
|
@ -67,3 +67,19 @@ def find_all(list_of_dict, match_function):
|
|||
if match_function(entry):
|
||||
found.append(entry)
|
||||
return found
|
||||
|
||||
def ljust_list(_list, length, fill_word=None):
|
||||
"""
|
||||
Similar to ljust but for list.
|
||||
|
||||
Usage:
|
||||
$ ljust_list([1, 2, 3], 5)
|
||||
> [1, 2, 3, None, None]
|
||||
"""
|
||||
# make a copy to avoid mutation of passed list
|
||||
_list = list(_list)
|
||||
fill_length = length - len(_list)
|
||||
if fill_length > 0:
|
||||
_list.extend([fill_word] * fill_length)
|
||||
|
||||
return _list
|
||||
|
|
|
|||
|
|
@ -1,17 +1,19 @@
|
|||
{
|
||||
"cards": [
|
||||
{
|
||||
"links": "[\n {\n \"label\": \"Dashboard\",\n \"name\": \"Dashboard\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Dashboard Chart\",\n \"name\": \"Dashboard Chart\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Dashboard Chart Source\",\n \"name\": \"Dashboard Chart Source\",\n \"type\": \"doctype\"\n }\n]",
|
||||
"title": "Dashboards"
|
||||
"hidden": 0,
|
||||
"label": "Dashboards",
|
||||
"links": "[\n {\n \"label\": \"Dashboard\",\n \"name\": \"Dashboard\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Dashboard Chart\",\n \"name\": \"Dashboard Chart\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Dashboard Chart Source\",\n \"name\": \"Dashboard Chart Source\",\n \"type\": \"doctype\"\n }\n]"
|
||||
},
|
||||
{
|
||||
"icon": "fa fa-glass",
|
||||
"links": "[\n {\n \"description\": \"Change field properties (hide, readonly, permission etc.)\",\n \"label\": \"Customize Form\",\n \"name\": \"Customize Form\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Add fields to forms.\",\n \"label\": \"Custom Field\",\n \"name\": \"Custom Field\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Add custom javascript to forms.\",\n \"label\": \"Custom Script\",\n \"name\": \"Custom Script\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Add custom forms.\",\n \"label\": \"DocType\",\n \"name\": \"DocType\",\n \"type\": \"doctype\"\n }\n]",
|
||||
"title": "Form Customization"
|
||||
"hidden": 0,
|
||||
"label": "Form Customization",
|
||||
"links": "[\n {\n \"description\": \"Change field properties (hide, readonly, permission etc.)\",\n \"label\": \"Customize Form\",\n \"name\": \"Customize Form\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Add fields to forms.\",\n \"label\": \"Custom Field\",\n \"name\": \"Custom Field\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Add custom javascript to forms.\",\n \"label\": \"Custom Script\",\n \"name\": \"Custom Script\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Add custom forms.\",\n \"label\": \"DocType\",\n \"name\": \"DocType\",\n \"type\": \"doctype\"\n }\n]"
|
||||
},
|
||||
{
|
||||
"links": "[\n {\n \"description\": \"Add your own translations\",\n \"label\": \"Custom Translations\",\n \"name\": \"Translation\",\n \"type\": \"doctype\"\n }\n]",
|
||||
"title": "Other"
|
||||
"hidden": 0,
|
||||
"label": "Other",
|
||||
"links": "[\n {\n \"description\": \"Add your own translations\",\n \"label\": \"Custom Translations\",\n \"name\": \"Translation\",\n \"type\": \"doctype\"\n }\n]"
|
||||
}
|
||||
],
|
||||
"category": "Administration",
|
||||
|
|
@ -25,7 +27,7 @@
|
|||
"idx": 0,
|
||||
"is_standard": 1,
|
||||
"label": "Customization",
|
||||
"modified": "2020-03-12 16:30:42.155206",
|
||||
"modified": "2020-04-01 11:24:40.787109",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Custom",
|
||||
"name": "Customization",
|
||||
|
|
@ -34,17 +36,17 @@
|
|||
"pin_to_top": 0,
|
||||
"shortcuts": [
|
||||
{
|
||||
"is_query_report": 0,
|
||||
"label": "Customize Form",
|
||||
"link_to": "Customize Form",
|
||||
"type": "DocType"
|
||||
},
|
||||
{
|
||||
"is_query_report": 0,
|
||||
"label": "Custom Role",
|
||||
"link_to": "Custom Role",
|
||||
"type": "DocType"
|
||||
},
|
||||
{
|
||||
"is_query_report": 0,
|
||||
"label": "Custom Script",
|
||||
"link_to": "Custom Script",
|
||||
"type": "DocType"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@
|
|||
"in_list_view",
|
||||
"in_standard_filter",
|
||||
"in_global_search",
|
||||
"in_preview",
|
||||
"bold",
|
||||
"report_hide",
|
||||
"search_index",
|
||||
|
|
@ -371,12 +372,18 @@
|
|||
"fieldname": "allow_in_quick_entry",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow in Quick Entry"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "in_preview",
|
||||
"fieldtype": "Check",
|
||||
"label": "In Preview"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-glass",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2020-03-16 14:52:43.954709",
|
||||
"modified": "2020-04-10 11:57:10.392218",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Custom",
|
||||
"name": "Custom Field",
|
||||
|
|
|
|||
|
|
@ -143,8 +143,7 @@ frappe.ui.form.on("Customize Form", {
|
|||
}, 1000);
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
frappe.ui.form.on("Customize Form Field", {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"actions": [],
|
||||
"autoname": "DL.####",
|
||||
"creation": "2013-01-29 17:55:08",
|
||||
"doctype": "DocType",
|
||||
|
|
@ -19,6 +20,7 @@
|
|||
"track_views",
|
||||
"allow_auto_repeat",
|
||||
"allow_import",
|
||||
"show_preview_popup",
|
||||
"image_view",
|
||||
"column_break_5",
|
||||
"title_field",
|
||||
|
|
@ -28,6 +30,10 @@
|
|||
"sort_field",
|
||||
"column_break_10",
|
||||
"sort_order",
|
||||
"section_break_23",
|
||||
"email_append_to",
|
||||
"sender_field",
|
||||
"subject_field",
|
||||
"fields_section_break",
|
||||
"fields"
|
||||
],
|
||||
|
|
@ -174,13 +180,44 @@
|
|||
"fieldname": "allow_import",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow Import (via Data Import Tool)"
|
||||
},
|
||||
{
|
||||
"depends_on": "email_append_to",
|
||||
"fieldname": "subject_field",
|
||||
"fieldtype": "Data",
|
||||
"label": "Subject Field"
|
||||
},
|
||||
{
|
||||
"depends_on": "email_append_to",
|
||||
"fieldname": "sender_field",
|
||||
"fieldtype": "Data",
|
||||
"label": "Sender Field",
|
||||
"mandatory_depends_on": "email_append_to"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "email_append_to",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow document creation via Email"
|
||||
},
|
||||
{
|
||||
"depends_on": "doc_type",
|
||||
"fieldname": "section_break_23",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "show_preview_popup",
|
||||
"fieldtype": "Check",
|
||||
"label": "Show Preview Popup"
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
"icon": "fa fa-glass",
|
||||
"idx": 1,
|
||||
"issingle": 1,
|
||||
"modified": "2019-10-08 11:16:36.698006",
|
||||
"links": [],
|
||||
"modified": "2020-04-10 12:16:01.320411",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Custom",
|
||||
"name": "Customize Form",
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ from frappe import _
|
|||
from frappe.utils import cint
|
||||
from frappe.model.document import Document
|
||||
from frappe.model import no_value_fields, core_doctypes_list
|
||||
from frappe.core.doctype.doctype.doctype import validate_fields_for_doctype
|
||||
from frappe.core.doctype.doctype.doctype import validate_fields_for_doctype, check_email_append_to
|
||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
|
||||
from frappe.model.docfield import supports_translation
|
||||
|
||||
|
|
@ -31,7 +31,11 @@ doctype_properties = {
|
|||
'track_changes': 'Check',
|
||||
'track_views': 'Check',
|
||||
'allow_auto_repeat': 'Check',
|
||||
'allow_import': 'Check'
|
||||
'allow_import': 'Check',
|
||||
'show_preview_popup': 'Check',
|
||||
'email_append_to': 'Check',
|
||||
'subject_field': 'Data',
|
||||
'sender_field': 'Data'
|
||||
}
|
||||
|
||||
docfield_properties = {
|
||||
|
|
@ -50,6 +54,7 @@ docfield_properties = {
|
|||
'in_list_view': 'Check',
|
||||
'in_standard_filter': 'Check',
|
||||
'in_global_search': 'Check',
|
||||
'in_preview': 'Check',
|
||||
'bold': 'Check',
|
||||
'hidden': 'Check',
|
||||
'collapsible': 'Check',
|
||||
|
|
@ -170,6 +175,7 @@ class CustomizeForm(Document):
|
|||
self.update_custom_fields()
|
||||
self.set_name_translation()
|
||||
validate_fields_for_doctype(self.doc_type)
|
||||
check_email_append_to(self)
|
||||
|
||||
if self.flags.update_db:
|
||||
frappe.db.updatedb(self.doc_type)
|
||||
|
|
@ -204,9 +210,11 @@ class CustomizeForm(Document):
|
|||
self.validate_fieldtype_change(df, meta_df[0].get(property), df.get(property))
|
||||
|
||||
elif property == "allow_on_submit" and df.get(property):
|
||||
frappe.msgprint(_("Row {0}: Not allowed to enable Allow on Submit for standard fields")\
|
||||
.format(df.idx))
|
||||
continue
|
||||
if not frappe.db.get_value("DocField",
|
||||
{"parent": self.doc_type, "fieldname": df.fieldname}, "allow_on_submit"):
|
||||
frappe.msgprint(_("Row {0}: Not allowed to enable Allow on Submit for standard fields")\
|
||||
.format(df.idx))
|
||||
continue
|
||||
|
||||
elif property == "reqd" and \
|
||||
((frappe.db.get_value("DocField",
|
||||
|
|
@ -365,7 +373,12 @@ class CustomizeForm(Document):
|
|||
for allowed_changes in allowed_fieldtype_change:
|
||||
if (old_value in allowed_changes and new_value in allowed_changes):
|
||||
allowed = True
|
||||
if frappe.db.type_map.get(old_value)[1] > frappe.db.type_map.get(new_value)[1]:
|
||||
old_value_length = cint(frappe.db.type_map.get(old_value)[1])
|
||||
new_value_length = cint(frappe.db.type_map.get(new_value)[1])
|
||||
|
||||
# Ignore fieldtype check validation if new field type has unspecified maxlength
|
||||
# Changes like DATA to TEXT, where new_value_lenth equals 0 will not be validated
|
||||
if new_value_length and (old_value_length > new_value_length):
|
||||
self.check_length_for_fieldtypes.append({'df': df, 'old_value': old_value})
|
||||
self.validate_fieldtype_length()
|
||||
else:
|
||||
|
|
@ -377,7 +390,7 @@ class CustomizeForm(Document):
|
|||
def validate_fieldtype_length(self):
|
||||
for field in self.check_length_for_fieldtypes:
|
||||
df = field.get('df')
|
||||
max_length = frappe.db.type_map.get(df.fieldtype)[1]
|
||||
max_length = cint(frappe.db.type_map.get(df.fieldtype)[1])
|
||||
fieldname = df.fieldname
|
||||
docs = frappe.db.sql('''
|
||||
SELECT name, {fieldname}, LENGTH({fieldname}) AS len
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ class TestCustomizeForm(unittest.TestCase):
|
|||
|
||||
d = self.get_customize_form("Event")
|
||||
self.assertEquals(d.doc_type, "Event")
|
||||
self.assertEquals(len(d.get("fields")), 35)
|
||||
self.assertEquals(len(d.get("fields")), 36)
|
||||
|
||||
d = self.get_customize_form("Event")
|
||||
self.assertEquals(d.doc_type, "Event")
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
"in_list_view",
|
||||
"in_standard_filter",
|
||||
"in_global_search",
|
||||
"in_preview",
|
||||
"bold",
|
||||
"allow_in_quick_entry",
|
||||
"translatable",
|
||||
|
|
@ -93,6 +94,7 @@
|
|||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:!in_list([\"Section Break\", \"Column Break\", \"Button\", \"HTML\"], doc.fieldtype)",
|
||||
"fieldname": "reqd",
|
||||
"fieldtype": "Check",
|
||||
"label": "Mandatory",
|
||||
|
|
@ -358,7 +360,7 @@
|
|||
"default": "0",
|
||||
"fieldname": "allow_in_quick_entry",
|
||||
"fieldtype": "Check",
|
||||
"label": " Allow in Quick Entry "
|
||||
"label": "Allow in Quick Entry"
|
||||
},
|
||||
{
|
||||
"fieldname": "property_depends_on_section",
|
||||
|
|
@ -380,12 +382,18 @@
|
|||
"fieldtype": "Code",
|
||||
"label": "Read Only Depends On",
|
||||
"options": "JS"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "in_preview",
|
||||
"fieldtype": "Check",
|
||||
"label": "In Preview"
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-03-16 14:53:40.619043",
|
||||
"modified": "2020-04-10 11:58:44.573537",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Custom",
|
||||
"name": "Customize Form Field",
|
||||
|
|
|
|||
|
|
@ -217,6 +217,9 @@ CREATE TABLE `tabDocType` (
|
|||
`allow_guest_to_view` int(1) NOT NULL DEFAULT 0,
|
||||
`route` varchar(255) DEFAULT NULL,
|
||||
`is_published_field` varchar(255) DEFAULT NULL,
|
||||
`email_append_to` int(1) NOT NULL DEFAULT 0,
|
||||
`subject_field` varchar(255) DEFAULT NULL,
|
||||
`sender_field` varchar(255) DEFAULT NULL,
|
||||
PRIMARY KEY (`name`),
|
||||
KEY `parent` (`parent`)
|
||||
) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
|
|
|||
|
|
@ -222,6 +222,9 @@ CREATE TABLE "tabDocType" (
|
|||
"allow_guest_to_view" smallint NOT NULL DEFAULT 0,
|
||||
"route" varchar(255) DEFAULT NULL,
|
||||
"is_published_field" varchar(255) DEFAULT NULL,
|
||||
"email_append_to" smallint NOT NULL DEFAULT 0,
|
||||
"subject_field" varchar(255) DEFAULT NULL,
|
||||
"sender_field" varchar(255) DEFAULT NULL,
|
||||
PRIMARY KEY ("name")
|
||||
) ;
|
||||
|
||||
|
|
|
|||
|
|
@ -4,41 +4,62 @@
|
|||
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
import json
|
||||
from frappe import _, DoesNotExistError
|
||||
from json import loads, dumps
|
||||
from frappe import _, DoesNotExistError, ValidationError, _dict
|
||||
from frappe.boot import get_allowed_pages, get_allowed_reports
|
||||
from six import string_types
|
||||
from frappe.cache_manager import build_domain_restriced_doctype_cache, build_domain_restriced_page_cache, build_table_count_cache
|
||||
from frappe.cache_manager import (
|
||||
build_domain_restriced_doctype_cache,
|
||||
build_domain_restriced_page_cache,
|
||||
build_table_count_cache
|
||||
)
|
||||
|
||||
class Workspace:
|
||||
def __init__(self, page_name):
|
||||
self.page_name = page_name
|
||||
|
||||
def build_cache(self):
|
||||
self.doc = frappe.get_doc("Desk Page", self.page_name)
|
||||
self.get_pages_to_extend()
|
||||
self.extended_cards = []
|
||||
self.extended_charts = []
|
||||
self.extended_shortcuts = []
|
||||
|
||||
user = frappe.get_user()
|
||||
user.build_permissions()
|
||||
self.user = user
|
||||
|
||||
user_doc = frappe.get_doc('User', frappe.session.user)
|
||||
self.blocked_modules = user_doc.get_blocked_modules()
|
||||
self.doc = self.get_page_for_user()
|
||||
|
||||
if self.doc.module in self.blocked_modules:
|
||||
raise frappe.PermissionError
|
||||
|
||||
self.user = user
|
||||
self.allowed_pages = get_allowed_pages()
|
||||
self.allowed_reports = get_allowed_reports()
|
||||
|
||||
self.table_counts = get_table_with_counts()
|
||||
self.restricted_doctypes = build_domain_restriced_doctype_cache()
|
||||
self.restricted_pages = build_domain_restriced_page_cache()
|
||||
self.restricted_doctypes = frappe.cache().get_value("domain_restricted_doctypes") or build_domain_restriced_doctype_cache()
|
||||
self.restricted_pages = frappe.cache().get_value("domain_restricted_pages") or build_domain_restriced_page_cache()
|
||||
|
||||
def get_page_for_user(self):
|
||||
filters = {
|
||||
'extends': self.page_name,
|
||||
'for_user': frappe.session.user
|
||||
}
|
||||
pages = frappe.get_list("Desk Page", filters=filters)
|
||||
if pages:
|
||||
return frappe.get_doc("Desk Page", pages[0])
|
||||
|
||||
self.get_pages_to_extend()
|
||||
return frappe.get_doc("Desk Page", self.page_name)
|
||||
|
||||
def get_pages_to_extend(self):
|
||||
pages = frappe.get_all("Desk Page", filters={
|
||||
"extends": self.page_name,
|
||||
'restrict_to_domain': ['in', frappe.get_active_domains()]
|
||||
'restrict_to_domain': ['in', frappe.get_active_domains()],
|
||||
'for_user': '',
|
||||
'module': ['not in', self.blocked_modules]
|
||||
})
|
||||
|
||||
pages = [frappe.get_doc("Desk Page", page['name']) for page in pages]
|
||||
self.extended_cards = []
|
||||
self.extended_charts = []
|
||||
self.extended_shortcuts = []
|
||||
|
||||
for page in pages:
|
||||
self.extended_cards = self.extended_cards + page.cards
|
||||
|
|
@ -61,17 +82,17 @@ class Workspace:
|
|||
|
||||
def build_workspace(self):
|
||||
self.cards = {
|
||||
'label': self.doc.cards_label,
|
||||
'label': _(self.doc.cards_label),
|
||||
'items': self.get_cards()
|
||||
}
|
||||
|
||||
self.charts = {
|
||||
'label': self.doc.charts_label,
|
||||
'label': _(self.doc.charts_label),
|
||||
'items': self.get_charts()
|
||||
}
|
||||
|
||||
self.shortcuts = {
|
||||
'label': self.doc.shortcuts_label,
|
||||
'label': _(self.doc.shortcuts_label),
|
||||
'items': self.get_shortcuts()
|
||||
}
|
||||
|
||||
|
|
@ -105,18 +126,21 @@ class Workspace:
|
|||
|
||||
item["count"] = count
|
||||
|
||||
# Translate label
|
||||
item["label"] = _(item.label) if item.label else _(item.name)
|
||||
|
||||
return item
|
||||
|
||||
new_data = []
|
||||
for section in cards:
|
||||
new_items = []
|
||||
if isinstance(section.links, string_types):
|
||||
links = json.loads(section.links)
|
||||
links = loads(section.links)
|
||||
else:
|
||||
links = section.links
|
||||
|
||||
for item in links:
|
||||
item = frappe._dict(item)
|
||||
item = _dict(item)
|
||||
|
||||
# Condition: based on country
|
||||
if item.country and item.country != default_country:
|
||||
|
|
@ -125,15 +149,15 @@ class Workspace:
|
|||
# Check if user is allowed to view
|
||||
if self.is_item_allowed(item.name, item.type):
|
||||
prepared_item = _prepare_item(item)
|
||||
new_items.append(item)
|
||||
new_items.append(prepared_item)
|
||||
|
||||
if new_items:
|
||||
if isinstance(section, frappe._dict):
|
||||
if isinstance(section, _dict):
|
||||
new_section = section.copy()
|
||||
else:
|
||||
new_section = section.as_dict().copy()
|
||||
new_section["links"] = new_items
|
||||
new_section["label"] = section.title
|
||||
new_section["label"] = _(new_section["label"])
|
||||
new_data.append(new_section)
|
||||
|
||||
return new_data
|
||||
|
|
@ -146,8 +170,10 @@ class Workspace:
|
|||
charts = charts + self.extended_charts
|
||||
|
||||
for chart in charts:
|
||||
chart.label = chart.label if chart.label else chart.chart_name
|
||||
all_charts.append(chart)
|
||||
if frappe.has_permission('Dashboard Chart', doc=chart.chart_name):
|
||||
# Translate label
|
||||
chart.label = _(chart.label) if chart.label else _(chart.chart_name)
|
||||
all_charts.append(chart)
|
||||
|
||||
return all_charts
|
||||
|
||||
|
|
@ -166,21 +192,23 @@ class Workspace:
|
|||
|
||||
for item in shortcuts:
|
||||
new_item = item.as_dict().copy()
|
||||
new_item['name'] = _(item.link_to)
|
||||
if self.is_item_allowed(item.link_to, item.type) and _in_active_domains(item):
|
||||
if item.type == "Page":
|
||||
page = self.allowed_pages[item.link_to]
|
||||
new_item['label'] = _(page.get("title", frappe.unscrub(item.link_to)))
|
||||
if item.type == "Report":
|
||||
report = self.allowed_reports.get(item.link_to, {})
|
||||
if report.get("report_type") in ["Query Report", "Script Report"]:
|
||||
new_item['is_query_report'] = 1
|
||||
else:
|
||||
new_item['ref_doctype'] = report.get('ref_doctype')
|
||||
|
||||
# Translate label
|
||||
new_item["label"] = _(item.label) if item.label else _(item.link_to)
|
||||
|
||||
items.append(new_item)
|
||||
|
||||
return items
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.read_only()
|
||||
def get_desktop_page(page):
|
||||
"""Applies permissions, customizations and returns the configruration for a page
|
||||
on desk.
|
||||
|
|
@ -191,9 +219,8 @@ def get_desktop_page(page):
|
|||
Returns:
|
||||
dict: dictionary of cards, charts and shortcuts to be displayed on website
|
||||
"""
|
||||
wspace = Workspace(page)
|
||||
try:
|
||||
wspace.build_cache()
|
||||
wspace = Workspace(page)
|
||||
wspace.build_workspace()
|
||||
return {
|
||||
'charts': wspace.charts,
|
||||
|
|
@ -208,13 +235,18 @@ def get_desktop_page(page):
|
|||
return None
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_desk_sidebar_items():
|
||||
def get_desk_sidebar_items(flatten=False):
|
||||
"""Get list of sidebar items for desk
|
||||
"""
|
||||
# don't get domain restricted pages
|
||||
blocked_modules = frappe.get_doc('User', frappe.session.user).get_blocked_modules()
|
||||
|
||||
filters = {
|
||||
'restrict_to_domain': ['in', frappe.get_active_domains()],
|
||||
'extends_another_page': False
|
||||
'extends_another_page': 0,
|
||||
'is_standard': 1,
|
||||
'for_user': '',
|
||||
'module': ['not in', blocked_modules]
|
||||
}
|
||||
|
||||
if not frappe.local.conf.developer_mode:
|
||||
|
|
@ -223,12 +255,16 @@ def get_desk_sidebar_items():
|
|||
# pages sorted based on pinned to top and then by name
|
||||
order_by = "pin_to_top desc, pin_to_bottom asc, name asc"
|
||||
pages = frappe.get_all("Desk Page", fields=["name", "category"], filters=filters, order_by=order_by, ignore_permissions=True)
|
||||
if flatten:
|
||||
return pages
|
||||
|
||||
from collections import defaultdict
|
||||
sidebar_items = defaultdict(list)
|
||||
|
||||
# The order will be maintained while categorizing
|
||||
for page in pages:
|
||||
# The order will be maintained while categorizing
|
||||
# Translate label
|
||||
page['label'] = _(page.get('name'))
|
||||
sidebar_items[page["category"]].append(page)
|
||||
return sidebar_items
|
||||
|
||||
|
|
@ -241,10 +277,14 @@ def get_table_with_counts():
|
|||
|
||||
def get_custom_reports_and_doctypes(module):
|
||||
return [
|
||||
frappe._dict({
|
||||
"title": "Custom",
|
||||
"links": get_custom_doctype_list(module) + get_custom_report_list(module)
|
||||
})
|
||||
_dict({
|
||||
"label": _("Custom Documents"),
|
||||
"links": get_custom_doctype_list(module)
|
||||
}),
|
||||
_dict({
|
||||
"label": _("Custom Reports"),
|
||||
"links": get_custom_report_list(module)
|
||||
}),
|
||||
]
|
||||
|
||||
def get_custom_doctype_list(module):
|
||||
|
|
@ -279,104 +319,116 @@ def get_custom_report_list(module):
|
|||
|
||||
return out
|
||||
|
||||
def make_them_pages():
|
||||
"""Helper function to make pages
|
||||
def get_custom_workspace_for_user(page):
|
||||
"""Get custom page from desk_page if exists or create one
|
||||
|
||||
Args:
|
||||
page (stirng): Page name
|
||||
|
||||
Returns:
|
||||
Object: Document object
|
||||
"""
|
||||
pages = [
|
||||
('Desk', 'frappe', 'octicon octicon-calendar'),
|
||||
('Settings', 'frappe', 'octicon octicon-settings'),
|
||||
('Users and Permissions', 'frappe', 'octicon octicon-settings'),
|
||||
('Customization', 'frappe', 'octicon octicon-settings'),
|
||||
('Integrations', 'frappe', 'octicon octicon-globe'),
|
||||
('Core', 'frappe', 'octicon octicon-circuit-board'),
|
||||
('Website', 'frappe', 'octicon octicon-globe'),
|
||||
('Getting Started', 'erpnext', 'fa fa-check-square-o'),
|
||||
('Accounts', 'erpnext', 'octicon octicon-repo'),
|
||||
('Selling', 'erpnext', 'octicon octicon-tag'),
|
||||
('Buying', 'erpnext', 'octicon octicon-briefcase'),
|
||||
('Stock', 'erpnext', 'octicon octicon-package'),
|
||||
('Assets', 'erpnext', 'octicon octicon-database'),
|
||||
('Projects', 'erpnext', 'octicon octicon-rocket'),
|
||||
('CRM', 'erpnext', 'octicon octicon-broadcast'),
|
||||
('Support', 'erpnext', 'fa fa-check-square-o'),
|
||||
('HR', 'erpnext', 'octicon octicon-organization'),
|
||||
('Quality Management', 'erpnext', 'fa fa-check-square-o'),
|
||||
('Manufacturing', 'erpnext', 'octicon octicon-tools'),
|
||||
('Retail', 'erpnext', 'octicon octicon-credit-card'),
|
||||
('Education', 'erpnext', 'octicon octicon-mortar-board'),
|
||||
('Healthcare', 'erpnext', 'fa fa-heartbeat'),
|
||||
('Agriculture', 'erpnext', 'octicon octicon-globe'),
|
||||
('Non Profit', 'erpnext', 'octicon octicon-heart'),
|
||||
('Help', 'erpnext', 'octicon octicon-device-camera-video')
|
||||
]
|
||||
|
||||
for page in pages:
|
||||
print("Processing Page: {0}".format(page[0]))
|
||||
make_them_cards(page[0], page[2])
|
||||
filters = {
|
||||
'extends': page,
|
||||
'for_user': frappe.session.user
|
||||
}
|
||||
pages = frappe.get_list("Desk Page", filters=filters)
|
||||
if pages:
|
||||
return frappe.get_doc("Desk Page", pages[0])
|
||||
doc = frappe.new_doc("Desk Page")
|
||||
doc.extends = page
|
||||
doc.for_user = frappe.session.user
|
||||
return doc
|
||||
|
||||
|
||||
def make_them_cards(page_name, from_module=None, to_module=None, icon=None):
|
||||
from frappe.desk.moduleview import get
|
||||
@frappe.whitelist()
|
||||
def save_customization(page, config):
|
||||
"""Save customizations as a separate doctype in Desk page per user
|
||||
|
||||
if not from_module:
|
||||
from_module = page_name
|
||||
Args:
|
||||
page (string): Name of the page to be edited
|
||||
config (dict): Dictionary config of al widgets
|
||||
|
||||
if not to_module:
|
||||
to_module = page_name
|
||||
Returns:
|
||||
Boolean: Customization saving status
|
||||
"""
|
||||
original_page = frappe.get_doc("Desk Page", page)
|
||||
page_doc = get_custom_workspace_for_user(page)
|
||||
|
||||
# Update field values
|
||||
page_doc.update({
|
||||
"charts_label": original_page.charts_label,
|
||||
"cards_label": original_page.cards_label,
|
||||
"shortcuts_label": original_page.shortcuts_label,
|
||||
"icon": original_page.icon,
|
||||
"module": original_page.module,
|
||||
"developer_mode_only": original_page.developer_mode_only,
|
||||
"category": original_page.category
|
||||
})
|
||||
|
||||
config = _dict(loads(config))
|
||||
if config.charts:
|
||||
page_doc.charts = prepare_widget(config.charts, "Desk Chart", "charts")
|
||||
if config.shortcuts:
|
||||
page_doc.shortcuts = prepare_widget(config.shortcuts, "Desk Shortcut", "shortcuts")
|
||||
if config.cards:
|
||||
page_doc.cards = prepare_widget(config.cards, "Desk Card", "cards")
|
||||
|
||||
# Set label
|
||||
page_doc.label = page + '-' + frappe.session.user
|
||||
|
||||
try:
|
||||
modules = get(from_module)['data']
|
||||
except:
|
||||
return
|
||||
if page_doc.is_new():
|
||||
page_doc.insert(ignore_permissions=True)
|
||||
else:
|
||||
page_doc.save(ignore_permissions=True)
|
||||
except (ValidationError, TypeError) as e:
|
||||
# Create a json string to log
|
||||
json_config = dumps(config, sort_keys=True, indent=4)
|
||||
|
||||
# Find or make page doc
|
||||
if frappe.db.exists("Desk Page", page_name):
|
||||
page = frappe.get_doc("Desk Page", page_name)
|
||||
print("--- Got Page: {0}".format(page.name))
|
||||
else:
|
||||
page = frappe.new_doc("Desk Page")
|
||||
page.label = page_name
|
||||
page.cards = []
|
||||
page.icon = icon
|
||||
print("--- New Page: {0}".format(page.name))
|
||||
# Error log body
|
||||
log = \
|
||||
"""
|
||||
page: {0}
|
||||
config: {1}
|
||||
exception: {2}
|
||||
""".format(page, json_config, e)
|
||||
frappe.log_error(log, _("Could not save customization"))
|
||||
return False
|
||||
|
||||
# Guess Which Module
|
||||
if not to_module and frappe.db.exists("Module Def", page_name):
|
||||
page.module = page_name
|
||||
return True
|
||||
|
||||
if to_module:
|
||||
page.module = to_module
|
||||
elif frappe.db.exists("Module Def", page_name):
|
||||
page.module = page_name
|
||||
|
||||
for data in modules:
|
||||
# Create a New Card Child Doc
|
||||
card = frappe.new_doc("Desk Card")
|
||||
def prepare_widget(config, doctype, parentfield):
|
||||
"""Create widget child table entries with parent details
|
||||
|
||||
# Data clean up
|
||||
for item in data['items']:
|
||||
try:
|
||||
del item['count']
|
||||
del item['incomplete_dependencies']
|
||||
except KeyError:
|
||||
pass
|
||||
Args:
|
||||
config (dict): Dictionary containing widget config
|
||||
doctype (string): Doctype name of the child table
|
||||
parentfield (string): Parent field for the child table
|
||||
|
||||
# Set Child doc values
|
||||
card.title = data['label']
|
||||
card.icon = data.get('icon')
|
||||
# Pretty dump JSON
|
||||
card.links = json.dumps(data['items'], indent=4, sort_keys=True)
|
||||
Returns:
|
||||
TYPE: List of Document objects
|
||||
"""
|
||||
if not config:
|
||||
return []
|
||||
order = config.get('order')
|
||||
widgets = config.get('widgets')
|
||||
prepare_widget_list = []
|
||||
for idx, name in enumerate(order):
|
||||
wid_config = widgets[name].copy()
|
||||
# Some cleanup
|
||||
wid_config.pop("name", None)
|
||||
|
||||
# Set Parent attributes
|
||||
card.parent = page.name
|
||||
card.parenttype = page.doctype
|
||||
card.parentfield = "cards"
|
||||
# New Doc
|
||||
doc = frappe.new_doc(doctype)
|
||||
doc.update(wid_config)
|
||||
|
||||
# Add cards to page doc
|
||||
print("------- Adding Card: {0}".format(card.title))
|
||||
page.cards.append(card)
|
||||
# Manually Set IDX
|
||||
doc.idx = idx + 1
|
||||
|
||||
# End it all
|
||||
page.save()
|
||||
frappe.db.commit()
|
||||
return
|
||||
# Set Parent Field
|
||||
doc.parentfield = parentfield
|
||||
|
||||
prepare_widget_list.append(doc)
|
||||
return prepare_widget_list
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@
|
|||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2020-01-26 20:00:10.069817",
|
||||
"modified": "2020-03-25 21:09:37.080132",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "Dashboard",
|
||||
|
|
@ -51,6 +51,27 @@
|
|||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Dashboard Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "All",
|
||||
"share": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
|
|
|
|||
|
|
@ -12,3 +12,12 @@ class Dashboard(Document):
|
|||
# make all other dashboards non-default
|
||||
frappe.db.sql('''update
|
||||
tabDashboard set is_default = 0 where name != %s''', self.name)
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_permitted_charts(dashboard_name):
|
||||
permitted_charts = []
|
||||
dashboard = frappe.get_doc('Dashboard', dashboard_name)
|
||||
for chart in dashboard.charts:
|
||||
if frappe.has_permission('Dashboard Chart', doc=chart.chart):
|
||||
permitted_charts.append(chart)
|
||||
return permitted_charts
|
||||
|
|
|
|||
|
|
@ -49,7 +49,8 @@
|
|||
"fieldname": "chart_type",
|
||||
"fieldtype": "Select",
|
||||
"label": "Chart Type",
|
||||
"options": "Count\nSum\nAverage\nGroup By\nCustom\nReport"
|
||||
"options": "Count\nSum\nAverage\nGroup By\nCustom\nReport",
|
||||
"set_only_once": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.chart_type === 'Custom'",
|
||||
|
|
@ -215,7 +216,7 @@
|
|||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2020-03-13 19:19:37.162771",
|
||||
"modified": "2020-04-08 18:54:36.739183",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "Dashboard Chart",
|
||||
|
|
@ -232,6 +233,27 @@
|
|||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Dashboard Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "All",
|
||||
"share": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
|
|
|
|||
|
|
@ -10,8 +10,51 @@ import json
|
|||
from frappe.core.page.dashboard.dashboard import cache_source, get_from_date_from_timespan
|
||||
from frappe.utils import nowdate, add_to_date, getdate, get_last_day, formatdate, get_datetime
|
||||
from frappe.model.naming import append_number_if_name_exists
|
||||
from frappe.boot import get_allowed_reports
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
def get_permission_query_conditions(user):
|
||||
|
||||
if not user:
|
||||
user = frappe.session.user
|
||||
|
||||
if user == 'Administrator':
|
||||
return
|
||||
|
||||
roles = frappe.get_roles(user)
|
||||
if "System Manager" in roles:
|
||||
return None
|
||||
|
||||
allowed_doctypes = tuple(frappe.permissions.get_doctypes_with_read())
|
||||
allowed_reports = tuple([key.encode('UTF8') for key in get_allowed_reports()])
|
||||
|
||||
return '''
|
||||
`tabDashboard Chart`.`document_type` in {allowed_doctypes}
|
||||
or `tabDashboard Chart`.`report_name` in {allowed_reports}
|
||||
'''.format(
|
||||
allowed_doctypes=allowed_doctypes,
|
||||
allowed_reports=allowed_reports
|
||||
)
|
||||
|
||||
|
||||
def has_permission(doc, ptype, user):
|
||||
roles = frappe.get_roles(user)
|
||||
if "System Manager" in roles:
|
||||
return True
|
||||
|
||||
|
||||
if doc.chart_type == 'Report':
|
||||
allowed_reports = tuple([key.encode('UTF8') for key in get_allowed_reports()])
|
||||
if doc.report_name in allowed_reports:
|
||||
return True
|
||||
else:
|
||||
allowed_doctypes = tuple(frappe.permissions.get_doctypes_with_read())
|
||||
if doc.document_type in allowed_doctypes:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@frappe.whitelist()
|
||||
@cache_source
|
||||
def get(chart_name = None, chart = None, no_cache = None, filters = None, from_date = None,
|
||||
|
|
|
|||
0
frappe/desk/doctype/dashboard_settings/__init__.py
Normal file
0
frappe/desk/doctype/dashboard_settings/__init__.py
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
// Copyright (c) 2020, Frappe Technologies and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Dashboard Settings', {
|
||||
// refresh: function(frm) {
|
||||
|
||||
// }
|
||||
});
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
{
|
||||
"actions": [],
|
||||
"autoname": "Prompt",
|
||||
"creation": "2020-03-31 19:41:45.785014",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"user",
|
||||
"chart_config"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "user",
|
||||
"fieldtype": "Link",
|
||||
"label": "User",
|
||||
"options": "User",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "chart_config",
|
||||
"fieldtype": "Code",
|
||||
"label": "Chart Configuration",
|
||||
"options": "JSON",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"in_create": 1,
|
||||
"links": [],
|
||||
"modified": "2020-04-01 00:07:26.489561",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "Dashboard Settings",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "All",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"read_only": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
43
frappe/desk/doctype/dashboard_settings/dashboard_settings.py
Normal file
43
frappe/desk/doctype/dashboard_settings/dashboard_settings.py
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2020, Frappe Technologies and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
import frappe
|
||||
import json
|
||||
|
||||
class DashboardSettings(Document):
|
||||
pass
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_dashboard_settings(user):
|
||||
if not frappe.db.exists("Dashboard Settings", user):
|
||||
doc = frappe.new_doc('Dashboard Settings')
|
||||
doc.name = user
|
||||
doc.insert(ignore_permissions=True)
|
||||
frappe.db.commit()
|
||||
return doc
|
||||
|
||||
def get_permission_query_conditions(user):
|
||||
if not user: user = frappe.session.user
|
||||
|
||||
return '''(`tabDashboard Settings`.name = '{user}')'''.format(user=user)
|
||||
|
||||
@frappe.whitelist()
|
||||
def save_chart_config(reset, config, chart_name):
|
||||
reset = frappe.parse_json(reset)
|
||||
doc = frappe.get_doc('Dashboard Settings', frappe.session.user)
|
||||
chart_config = frappe.parse_json(doc.chart_config) or {}
|
||||
|
||||
if reset:
|
||||
chart_config[chart_name] = {}
|
||||
else:
|
||||
config = frappe.parse_json(config)
|
||||
if not chart_name in chart_config:
|
||||
chart_config[chart_name] = {}
|
||||
chart_config[chart_name].update(config)
|
||||
|
||||
frappe.db.set_value('Dashboard Settings', frappe.session.user, 'chart_config', json.dumps(chart_config))
|
||||
|
|
@ -2,11 +2,12 @@
|
|||
"actions": [],
|
||||
"creation": "2020-01-29 14:45:54.383089",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"title",
|
||||
"label",
|
||||
"column_break_2",
|
||||
"icon",
|
||||
"hidden",
|
||||
"section_break_3",
|
||||
"links"
|
||||
],
|
||||
|
|
@ -18,13 +19,6 @@
|
|||
"options": "JSON",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "title",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Title",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_3",
|
||||
"fieldtype": "Section Break"
|
||||
|
|
@ -34,14 +28,23 @@
|
|||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "icon",
|
||||
"default": "0",
|
||||
"fieldname": "hidden",
|
||||
"fieldtype": "Check",
|
||||
"in_list_view": 1,
|
||||
"label": "Hidden"
|
||||
},
|
||||
{
|
||||
"fieldname": "label",
|
||||
"fieldtype": "Data",
|
||||
"label": "Icon"
|
||||
"in_list_view": 1,
|
||||
"label": "Label",
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-02-03 12:40:42.595122",
|
||||
"modified": "2020-03-31 14:38:06.303847",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "Desk Card",
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@
|
|||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-03-20 10:04:13.992228",
|
||||
"modified": "2020-03-31 13:33:13.128804",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "Desk Chart",
|
||||
|
|
|
|||
|
|
@ -2,17 +2,21 @@
|
|||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Desk Page', {
|
||||
refresh: function(frm) {
|
||||
setup: function(frm) {
|
||||
frm.get_field("is_standard").toggle(frappe.boot.developer_mode);
|
||||
frm.get_field("extends_another_page").toggle(frappe.boot.developer_mode);
|
||||
if (!frappe.boot.developer_mode) {
|
||||
frm.set_read_only();
|
||||
frm.fields
|
||||
.filter(field => field.has_input)
|
||||
.forEach(field => {
|
||||
frm.set_df_property(field.df.fieldname, "read_only", "1");
|
||||
});
|
||||
frm.disable_save();
|
||||
if (!frappe.boot.developer_mode || frm.doc.for_user) {
|
||||
frm.trigger('disable_form');
|
||||
}
|
||||
},
|
||||
|
||||
disable_form: function(frm) {
|
||||
frm.set_read_only();
|
||||
frm.fields
|
||||
.filter(field => field.has_input)
|
||||
.forEach(field => {
|
||||
frm.set_df_property(field.df.fieldname, "read_only", "1");
|
||||
});
|
||||
frm.disable_save();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -9,6 +9,7 @@
|
|||
"field_order": [
|
||||
"label",
|
||||
"extends",
|
||||
"for_user",
|
||||
"module",
|
||||
"category",
|
||||
"restrict_to_domain",
|
||||
|
|
@ -36,7 +37,6 @@
|
|||
"fieldname": "label",
|
||||
"fieldtype": "Data",
|
||||
"label": "Name",
|
||||
"length": 22,
|
||||
"unique": 1
|
||||
},
|
||||
{
|
||||
|
|
@ -52,6 +52,7 @@
|
|||
"options": "Desk Chart"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.extends_another_page || !doc.is_standard",
|
||||
"fieldname": "shortcuts",
|
||||
"fieldtype": "Table",
|
||||
"label": "Shortcuts",
|
||||
|
|
@ -136,16 +137,19 @@
|
|||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.extends_another_page || !doc.is_standard",
|
||||
"fieldname": "charts_label",
|
||||
"fieldtype": "Data",
|
||||
"label": "Label"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.extends_another_page || !doc.is_standard",
|
||||
"fieldname": "shortcuts_label",
|
||||
"fieldtype": "Data",
|
||||
"label": "Label"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.extends_another_page || !doc.is_standard",
|
||||
"fieldname": "cards_label",
|
||||
"fieldtype": "Data",
|
||||
"label": "Label"
|
||||
|
|
@ -166,24 +170,36 @@
|
|||
"default": "0",
|
||||
"fieldname": "is_standard",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Standard"
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Is Standard",
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "extends_another_page",
|
||||
"fieldtype": "Check",
|
||||
"label": "Extends Another Page"
|
||||
"label": "Extends Another Page",
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.extends_another_page == 1",
|
||||
"fieldname": "extends",
|
||||
"fieldtype": "Link",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Extends",
|
||||
"options": "Desk Page"
|
||||
"options": "Desk Page",
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "for_user",
|
||||
"fieldtype": "Data",
|
||||
"label": "For User",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2020-03-12 16:38:16.206732",
|
||||
"modified": "2020-03-26 12:35:41.981432",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "Desk Page",
|
||||
|
|
|
|||
|
|
@ -6,11 +6,11 @@
|
|||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"type",
|
||||
"icon",
|
||||
"label",
|
||||
"column_break_4",
|
||||
"link_to",
|
||||
"icon",
|
||||
"restrict_to_domain",
|
||||
"is_query_report",
|
||||
"section_break_5",
|
||||
"stats_filter",
|
||||
"column_break_3",
|
||||
|
|
@ -51,6 +51,7 @@
|
|||
"label": "Format"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.type == \"DocType\" && frappe.boot.developer_mode",
|
||||
"fieldname": "section_break_5",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Count Filter"
|
||||
|
|
@ -61,13 +62,7 @@
|
|||
"label": "Color"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.type === \"Report\"",
|
||||
"fieldname": "is_query_report",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Query Report"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:frappe.boot.developer_mode",
|
||||
"fieldname": "icon",
|
||||
"fieldtype": "Data",
|
||||
"label": "Icon"
|
||||
|
|
@ -77,15 +72,22 @@
|
|||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:frappe.boot.developer_mode",
|
||||
"fieldname": "restrict_to_domain",
|
||||
"fieldtype": "Link",
|
||||
"label": "Restrict to Domain",
|
||||
"options": "Domain"
|
||||
},
|
||||
{
|
||||
"fieldname": "label",
|
||||
"fieldtype": "Data",
|
||||
"label": "Label",
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-03-11 13:09:00.180528",
|
||||
"modified": "2020-04-07 19:04:23.645198",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "Desk Shortcut",
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
{
|
||||
"actions": [],
|
||||
"allow_import": 1,
|
||||
"autoname": "EV.#####",
|
||||
"creation": "2013-06-10 13:17:47",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Document",
|
||||
"email_append_to": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"details",
|
||||
|
|
@ -17,6 +19,7 @@
|
|||
"starts_on",
|
||||
"ends_on",
|
||||
"status",
|
||||
"sender",
|
||||
"all_day",
|
||||
"sync_with_google_calendar",
|
||||
"sb_00",
|
||||
|
|
@ -262,11 +265,19 @@
|
|||
"fieldtype": "Check",
|
||||
"label": "Pulled from Google Calendar",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "sender",
|
||||
"fieldtype": "Data",
|
||||
"label": "Sender",
|
||||
"options": "Email",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-calendar",
|
||||
"idx": 1,
|
||||
"modified": "2019-08-08 16:01:19.489396",
|
||||
"links": [],
|
||||
"modified": "2020-01-14 21:47:15.825287",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "Event",
|
||||
|
|
@ -297,8 +308,10 @@
|
|||
}
|
||||
],
|
||||
"read_only": 1,
|
||||
"sender_field": "sender",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"subject_field": "subject",
|
||||
"title_field": "subject",
|
||||
"track_changes": 1,
|
||||
"track_seen": 1,
|
||||
|
|
|
|||
|
|
@ -6,14 +6,13 @@ from __future__ import unicode_literals
|
|||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.desk.doctype.notification_settings.notification_settings import (is_notifications_enabled,
|
||||
is_email_notifications_enabled, is_email_notifications_enabled_for_type, set_seen_value)
|
||||
from frappe.desk.doctype.notification_settings.notification_settings import (is_notifications_enabled, is_email_notifications_enabled_for_type, set_seen_value)
|
||||
|
||||
class NotificationLog(Document):
|
||||
def after_insert(self):
|
||||
frappe.publish_realtime('notification', after_commit=True, user=self.for_user)
|
||||
set_notifications_as_unseen(self.for_user)
|
||||
if is_email_notifications_enabled(self.for_user):
|
||||
if is_email_notifications_enabled_for_type(self.for_user, self.type):
|
||||
send_notification_email(self)
|
||||
|
||||
|
||||
|
|
@ -73,9 +72,6 @@ def make_notification_logs(doc, users):
|
|||
_doc.insert(ignore_permissions=True)
|
||||
|
||||
def send_notification_email(doc):
|
||||
is_type_enabled = is_email_notifications_enabled_for_type(doc.for_user, doc.type)
|
||||
if not is_type_enabled:
|
||||
return
|
||||
|
||||
if doc.type == 'Energy Point' and doc.email_content is None:
|
||||
return
|
||||
|
|
|
|||
|
|
@ -8,5 +8,14 @@ frappe.ui.form.on('Notification Settings', {
|
|||
route: '#modules/Settings',
|
||||
type: 'Custom'
|
||||
});
|
||||
},
|
||||
|
||||
refresh: (frm) => {
|
||||
if (frappe.user.has_role('System Manager')) {
|
||||
frm.add_custom_button('Go to Notification Settings List', () => {
|
||||
frappe.set_route('List', 'Notification Settings');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
|
|
|||
|
|
@ -25,6 +25,9 @@ def is_email_notifications_enabled(user):
|
|||
return enabled
|
||||
|
||||
def is_email_notifications_enabled_for_type(user, notification_type):
|
||||
if not is_email_notifications_enabled(user):
|
||||
return False
|
||||
|
||||
fieldname = 'enable_email_' + frappe.scrub(notification_type)
|
||||
enabled = frappe.db.get_value('Notification Settings', user, fieldname)
|
||||
if enabled is None:
|
||||
|
|
@ -59,7 +62,14 @@ def get_subscribed_documents():
|
|||
def get_permission_query_conditions(user):
|
||||
if not user: user = frappe.session.user
|
||||
|
||||
return '''(`tabNotification Settings`.user = '{user}')'''.format(user=user)
|
||||
if user == 'Administrator':
|
||||
return
|
||||
|
||||
roles = frappe.get_roles(user)
|
||||
if "System Manager" in roles:
|
||||
return '''(`tabNotification Settings`.name != 'Administrator')'''
|
||||
|
||||
return '''(`tabNotification Settings`.name = '{user}')'''.format(user=user)
|
||||
|
||||
@frappe.whitelist()
|
||||
def set_seen_value(value, user):
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
{
|
||||
"actions": [],
|
||||
"autoname": "hash",
|
||||
"creation": "2012-07-03 13:30:35",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Setup",
|
||||
"email_append_to": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"description_and_status",
|
||||
|
|
@ -142,7 +144,8 @@
|
|||
"fieldname": "sender",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Sender"
|
||||
"label": "Sender",
|
||||
"options": "Email"
|
||||
},
|
||||
{
|
||||
"fieldname": "assignment_rule",
|
||||
|
|
@ -154,7 +157,8 @@
|
|||
],
|
||||
"icon": "fa fa-check",
|
||||
"idx": 2,
|
||||
"modified": "2019-09-10 14:34:59.161750",
|
||||
"links": [],
|
||||
"modified": "2020-01-14 17:04:36.971002",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "ToDo",
|
||||
|
|
@ -185,9 +189,11 @@
|
|||
],
|
||||
"quick_entry": 1,
|
||||
"search_fields": "description, reference_type, reference_name",
|
||||
"sender_field": "sender",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"subject_field": "description",
|
||||
"title_field": "description",
|
||||
"track_changes": 1,
|
||||
"track_seen": 1
|
||||
}
|
||||
}
|
||||
|
|
@ -8,8 +8,6 @@ import json
|
|||
from frappe.model.document import Document
|
||||
from frappe.utils import get_fullname
|
||||
|
||||
subject_field = "description"
|
||||
sender_field = "sender"
|
||||
exclude_from_linked_with = True
|
||||
|
||||
class ToDo(Document):
|
||||
|
|
|
|||
31
frappe/desk/doctype/todo/todo_calendar.js
Normal file
31
frappe/desk/doctype/todo/todo_calendar.js
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
// License: GNU General Public License v3. See license.txt
|
||||
|
||||
frappe.views.calendar["ToDo"] = {
|
||||
field_map: {
|
||||
"start": "date",
|
||||
"end": "date",
|
||||
"id": "name",
|
||||
"title": "description",
|
||||
"allDay": "allDay",
|
||||
"progress": "progress"
|
||||
},
|
||||
gantt: true,
|
||||
filters: [
|
||||
{
|
||||
"fieldtype": "Link",
|
||||
"fieldname": "reference_type",
|
||||
"options": "Task",
|
||||
"label": __("Task")
|
||||
},
|
||||
{
|
||||
"fieldtype": "Dynamic Link",
|
||||
"fieldname": "reference_name",
|
||||
"options": "reference_type",
|
||||
"label": __("Task")
|
||||
}
|
||||
|
||||
],
|
||||
get_events_method: "frappe.desk.calendar.get_events"
|
||||
};
|
||||
|
||||
|
|
@ -196,8 +196,6 @@ class FormMeta(Meta):
|
|||
self.get("__messages").update(messages, as_value=True)
|
||||
|
||||
def load_dashboard(self):
|
||||
if self.custom:
|
||||
return
|
||||
self.set('__dashboard', self.get_dashboard_data())
|
||||
|
||||
def load_kanban_meta(self):
|
||||
|
|
|
|||
|
|
@ -56,18 +56,20 @@ def validate_link():
|
|||
frappe.response['valid_value'] = valid_value
|
||||
frappe.response['message'] = 'Ok'
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def add_comment(reference_doctype, reference_name, content, comment_email):
|
||||
def add_comment(reference_doctype, reference_name, content, comment_email, comment_by):
|
||||
"""allow any logged user to post a comment"""
|
||||
doc = frappe.get_doc(dict(
|
||||
doctype = 'Comment',
|
||||
reference_doctype = reference_doctype,
|
||||
reference_name = reference_name,
|
||||
comment_email = comment_email,
|
||||
comment_type = 'Comment'
|
||||
doctype='Comment',
|
||||
reference_doctype=reference_doctype,
|
||||
reference_name=reference_name,
|
||||
comment_email=comment_email,
|
||||
comment_type='Comment',
|
||||
comment_by=comment_by
|
||||
))
|
||||
doc.content = extract_images_from_html(doc, content)
|
||||
doc.insert(ignore_permissions = True)
|
||||
doc.insert(ignore_permissions=True)
|
||||
|
||||
follow_document(doc.reference_doctype, doc.reference_name, frappe.session.user)
|
||||
return doc.as_dict()
|
||||
|
|
|
|||
|
|
@ -268,8 +268,9 @@ def get_open_count(doctype, name, items=[]):
|
|||
"count": out,
|
||||
}
|
||||
|
||||
module = frappe.get_meta_module(doctype)
|
||||
if hasattr(module, "get_timeline_data"):
|
||||
out["timeline_data"] = module.get_timeline_data(doctype, name)
|
||||
if not meta.custom:
|
||||
module = frappe.get_meta_module(doctype)
|
||||
if hasattr(module, "get_timeline_data"):
|
||||
out["timeline_data"] = module.get_timeline_data(doctype, name)
|
||||
|
||||
return out
|
||||
|
|
|
|||
|
|
@ -8,16 +8,22 @@ from frappe import _
|
|||
from frappe.desk.doctype.global_search_settings.global_search_settings import update_global_search_doctypes
|
||||
|
||||
def install():
|
||||
update_genders_and_salutations()
|
||||
update_genders()
|
||||
update_salutations()
|
||||
update_global_search_doctypes()
|
||||
setup_email_linking()
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_genders_and_salutations():
|
||||
default_genders = [_("Male"), _("Female"), _("Other")]
|
||||
default_salutations = [_("Mr"), _("Ms"), _('Mx'), _("Dr"), _("Mrs"), _("Madam"), _("Miss"), _("Master"), _("Prof")]
|
||||
def update_genders():
|
||||
default_genders = [_("Male"), _("Female"), _("Other"),_("Transgender"), _("Genderqueer"), _("Non-Conforming"),_("Prefer not to say")]
|
||||
records = [{'doctype': 'Gender', 'gender': d} for d in default_genders]
|
||||
records += [{'doctype': 'Salutation', 'salutation': d} for d in default_salutations]
|
||||
for record in records:
|
||||
frappe.get_doc(record).insert(ignore_permissions=True, ignore_if_duplicate=True)
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_salutations():
|
||||
default_salutations = [_("Mr"), _("Ms"), _('Mx'), _("Dr"), _("Mrs"), _("Madam"), _("Miss"), _("Master"), _("Prof")]
|
||||
records = [{'doctype': 'Salutation', 'salutation': d} for d in default_salutations]
|
||||
for record in records:
|
||||
doc = frappe.new_doc(record.get("doctype"))
|
||||
doc.update(record)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import os, json
|
|||
|
||||
from frappe import _
|
||||
from frappe.modules import scrub, get_module_path
|
||||
from frappe.utils import flt, cint, get_html_format, cstr, get_url_to_form
|
||||
from frappe.utils import flt, cint, get_html_format, get_url_to_form
|
||||
from frappe.model.utils import render_include
|
||||
from frappe.translate import send_translations
|
||||
import frappe.desk.reportview
|
||||
|
|
@ -16,6 +16,7 @@ from frappe.permissions import get_role_permissions
|
|||
from six import string_types, iteritems
|
||||
from datetime import timedelta
|
||||
from frappe.utils import gzip_decompress
|
||||
from frappe.core.utils import ljust_list
|
||||
|
||||
def get_report_doc(report_name):
|
||||
doc = frappe.get_doc("Report", report_name)
|
||||
|
|
@ -42,44 +43,32 @@ def get_report_doc(report_name):
|
|||
return doc
|
||||
|
||||
|
||||
def generate_report_result(report, filters=None, user=None):
|
||||
status = None
|
||||
if not user:
|
||||
user = frappe.session.user
|
||||
if not filters:
|
||||
filters = []
|
||||
def generate_report_result(report, filters=None, user=None, custom_columns=None):
|
||||
user = user or frappe.session.user
|
||||
filters = filters or []
|
||||
|
||||
if filters and isinstance(filters, string_types):
|
||||
filters = json.loads(filters)
|
||||
columns, result, message, chart, report_summary, skip_total_row = [], [], None, None, None, 0
|
||||
|
||||
res = []
|
||||
|
||||
if report.report_type == "Query Report":
|
||||
if not report.query:
|
||||
status = "error"
|
||||
frappe.msgprint(_("Must specify a Query to run"), raise_exception=True)
|
||||
|
||||
if not report.query.lower().startswith("select"):
|
||||
status = "error"
|
||||
frappe.msgprint(_("Query must be a SELECT"), raise_exception=True)
|
||||
|
||||
result = [list(t) for t in frappe.db.sql(report.query, filters)]
|
||||
columns = [cstr(c[0]) for c in frappe.db.get_description()]
|
||||
res = report.execute_query_report(filters)
|
||||
|
||||
elif report.report_type == 'Script Report':
|
||||
res = report.execute_script_report(filters)
|
||||
|
||||
columns, result = res[0], res[1]
|
||||
if len(res) > 2:
|
||||
message = res[2]
|
||||
if len(res) > 3:
|
||||
chart = res[3]
|
||||
if len(res) > 4:
|
||||
report_summary = res[4]
|
||||
if len(res) > 5:
|
||||
skip_total_row = cint(res[5])
|
||||
columns, result, message, chart, report_summary, skip_total_row = \
|
||||
ljust_list(res, 6)
|
||||
|
||||
if report.custom_columns:
|
||||
columns = json.loads(report.custom_columns)
|
||||
result = add_data_to_custom_columns(columns, result)
|
||||
if custom_columns:
|
||||
result = add_data_to_custom_columns(custom_columns, result)
|
||||
|
||||
for custom_column in custom_columns:
|
||||
columns.insert(custom_column['insert_after_index'] + 1, custom_column)
|
||||
|
||||
if result:
|
||||
result = get_filtered_data(report.ref_doctype, columns, result, user)
|
||||
|
|
@ -93,8 +82,8 @@ def generate_report_result(report, filters=None, user=None):
|
|||
"message": message,
|
||||
"chart": chart,
|
||||
"report_summary": report_summary,
|
||||
"skip_total_row": skip_total_row,
|
||||
"status": status,
|
||||
"skip_total_row": skip_total_row or 0,
|
||||
"status": None,
|
||||
"execution_time": frappe.cache().hget('report_execution_time', report.name) or 0
|
||||
}
|
||||
|
||||
|
|
@ -161,7 +150,7 @@ def get_script(report_name):
|
|||
|
||||
@frappe.whitelist()
|
||||
@frappe.read_only()
|
||||
def run(report_name, filters=None, user=None, ignore_prepared_report=False):
|
||||
def run(report_name, filters=None, user=None, ignore_prepared_report=False, custom_columns=None):
|
||||
|
||||
report = get_report_doc(report_name)
|
||||
if not user:
|
||||
|
|
@ -183,7 +172,7 @@ def run(report_name, filters=None, user=None, ignore_prepared_report=False):
|
|||
dn = ""
|
||||
result = get_prepared_report_result(report, filters, dn, user)
|
||||
else:
|
||||
result = generate_report_result(report, filters, user)
|
||||
result = generate_report_result(report, filters, user, custom_columns)
|
||||
|
||||
result["add_total_row"] = report.add_total_row and not result.get('skip_total_row', False)
|
||||
|
||||
|
|
@ -253,7 +242,7 @@ def get_prepared_report_result(report, filters, dn="", user=None):
|
|||
columns = json.loads(doc.columns) if doc.columns else data[0]
|
||||
|
||||
for column in columns:
|
||||
if isinstance(column, dict):
|
||||
if isinstance(column, dict) and column.get("label"):
|
||||
column["label"] = _(column["label"])
|
||||
|
||||
latest_report_data = {
|
||||
|
|
@ -294,6 +283,8 @@ def export_query():
|
|||
if isinstance(data.get("file_format_type"), string_types):
|
||||
file_format_type = data["file_format_type"]
|
||||
|
||||
custom_columns = frappe.parse_json(data["custom_columns"])
|
||||
|
||||
include_indentation = data["include_indentation"]
|
||||
if isinstance(data.get("visible_idx"), string_types):
|
||||
visible_idx = json.loads(data.get("visible_idx"))
|
||||
|
|
@ -301,13 +292,14 @@ def export_query():
|
|||
visible_idx = None
|
||||
|
||||
if file_format_type == "Excel":
|
||||
data = run(report_name, filters)
|
||||
data = run(report_name, filters, custom_columns=custom_columns)
|
||||
data = frappe._dict(data)
|
||||
if not data.columns:
|
||||
frappe.respond_as_web_page(_("No data to export"),
|
||||
_("You can try changing the filters of your report."))
|
||||
return
|
||||
|
||||
data.columns = [col for col in data.columns if isinstance(col, dict) and not col.get('hidden')]
|
||||
columns = get_columns_dict(data.columns)
|
||||
|
||||
from frappe.utils.xlsxutils import make_xlsx
|
||||
|
|
@ -319,7 +311,7 @@ def export_query():
|
|||
frappe.response['type'] = 'binary'
|
||||
|
||||
|
||||
def build_xlsx_data(columns, data, visible_idx,include_indentation):
|
||||
def build_xlsx_data(columns, data, visible_idx, include_indentation):
|
||||
result = [[]]
|
||||
|
||||
# add column headings
|
||||
|
|
|
|||
|
|
@ -367,8 +367,11 @@ def scrub_user_tags(tagcount):
|
|||
return rlist
|
||||
|
||||
# used in building query in queries.py
|
||||
def get_match_cond(doctype):
|
||||
cond = DatabaseQuery(doctype).build_match_conditions()
|
||||
def get_match_cond(doctype, as_condition=True):
|
||||
cond = DatabaseQuery(doctype).build_match_conditions(as_condition=as_condition)
|
||||
if not as_condition:
|
||||
return cond
|
||||
|
||||
return ((' and ' + cond) if cond else "").replace("%", "%%")
|
||||
|
||||
def build_match_conditions(doctype, user=None, as_condition=True):
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ def get_communication_doctype(doctype, txt, searchfield, start, page_len, filter
|
|||
com_doctypes = []
|
||||
if len(txt)<2:
|
||||
|
||||
for name in ["Customer", "Supplier"]:
|
||||
for name in frappe.get_hooks("communication_doctypes"):
|
||||
try:
|
||||
module = load_doctype_module(name, suffix='_dashboard')
|
||||
if hasattr(module, 'get_data'):
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@
|
|||
"fieldtype": "Data",
|
||||
"in_global_search": 1,
|
||||
"label": "Email Address",
|
||||
"options": "Email",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
|
|
@ -410,7 +411,7 @@
|
|||
],
|
||||
"icon": "fa fa-inbox",
|
||||
"links": [],
|
||||
"modified": "2019-12-18 15:56:39.744520",
|
||||
"modified": "2020-04-06 19:20:50.491146",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Email",
|
||||
"name": "Email Account",
|
||||
|
|
|
|||
|
|
@ -452,16 +452,15 @@ class EmailAccount(Document):
|
|||
def set_sender_field_and_subject_field(self):
|
||||
'''Identify the sender and subject fields from the `append_to` DocType'''
|
||||
# set subject_field and sender_field
|
||||
meta_module = frappe.get_meta_module(self.append_to)
|
||||
meta = frappe.get_meta(self.append_to)
|
||||
self.subject_field = None
|
||||
self.sender_field = None
|
||||
|
||||
self.subject_field = getattr(meta_module, "subject_field", "subject")
|
||||
if not meta.get_field(self.subject_field):
|
||||
self.subject_field = None
|
||||
if hasattr(meta, "subject_field"):
|
||||
self.subject_field = meta.subject_field
|
||||
|
||||
self.sender_field = getattr(meta_module, "sender_field", "sender")
|
||||
if not meta.get_field(self.sender_field):
|
||||
self.sender_field = None
|
||||
if hasattr(meta, "sender_field"):
|
||||
self.sender_field = meta.sender_field
|
||||
|
||||
def find_parent_based_on_subject_and_sender(self, communication, email):
|
||||
'''Find parent document based on subject and sender match'''
|
||||
|
|
@ -675,8 +674,21 @@ class EmailAccount(Document):
|
|||
|
||||
@frappe.whitelist()
|
||||
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]
|
||||
txt = txt if txt else ""
|
||||
email_append_to_list = []
|
||||
|
||||
# Set Email Append To DocTypes via DocType
|
||||
filters = {"istable": 0, "issingle": 0, "email_append_to": 1}
|
||||
for dt in frappe.get_all("DocType", filters=filters, fields=["name", "email_append_to"]):
|
||||
email_append_to_list.append(dt.name)
|
||||
|
||||
# Set Email Append To DocTypes set via Customize Form
|
||||
for dt in frappe.get_list("Property Setter", filters={"property": "email_append_to", "value": 1}, fields=["doc_type"]):
|
||||
email_append_to_list.append(dt.doc_type)
|
||||
|
||||
email_append_to = [[d] for d in set(email_append_to_list) if txt in d]
|
||||
|
||||
return email_append_to
|
||||
|
||||
def test_internet(host="8.8.8.8", port=53, timeout=3):
|
||||
"""Returns True if internet is connected
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ class EmailDomain(Document):
|
|||
|
||||
except Exception:
|
||||
frappe.throw(_("Incoming email account not correct"))
|
||||
return None
|
||||
|
||||
finally:
|
||||
try:
|
||||
if self.use_imap:
|
||||
|
|
@ -48,9 +48,10 @@ class EmailDomain(Document):
|
|||
test.quit()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
if self.use_ssl_for_outgoing:
|
||||
if not self.smtp_port:
|
||||
if self.get('use_ssl_for_outgoing'):
|
||||
if not self.get('smtp_port'):
|
||||
self.smtp_port = 465
|
||||
|
||||
sess = smtplib.SMTP_SSL((self.smtp_server or "").encode('utf-8'),
|
||||
|
|
@ -62,28 +63,15 @@ class EmailDomain(Document):
|
|||
sess.quit()
|
||||
except Exception:
|
||||
frappe.throw(_("Outgoing email account not correct"))
|
||||
return None
|
||||
return
|
||||
|
||||
def on_update(self):
|
||||
"""update all email accounts using this domain"""
|
||||
for email_account in frappe.get_all("Email Account",
|
||||
filters={"domain": self.name}):
|
||||
|
||||
for email_account in frappe.get_all("Email Account", filters={"domain": self.name}):
|
||||
try:
|
||||
email_account = frappe.get_doc("Email Account",
|
||||
email_account.name)
|
||||
email_account.set("email_server",self.email_server)
|
||||
email_account.set("use_imap",self.use_imap)
|
||||
email_account.set("use_ssl",self.use_ssl)
|
||||
email_account.set("use_tls",self.use_tls)
|
||||
email_account.set("attachment_limit",self.attachment_limit)
|
||||
email_account.set("smtp_server",self.smtp_server)
|
||||
email_account.set("smtp_port",self.smtp_port)
|
||||
email_account.set("use_ssl_for_outgoing", self.use_ssl_for_outgoing)
|
||||
email_account.set("append_emails_to_sent_folder", self.append_emails_to_sent_folder)
|
||||
email_account = frappe.get_doc("Email Account", email_account.name)
|
||||
for attr in ["email_server", "use_imap", "use_ssl", "use_tls", "attachment_limit", "smtp_server", "smtp_port", "use_ssl_for_outgoing", "append_emails_to_sent_folder"]:
|
||||
email_account.set(attr, self.get(attr, default=0))
|
||||
email_account.save()
|
||||
|
||||
except Exception as e:
|
||||
frappe.msgprint(email_account.name)
|
||||
frappe.throw(e)
|
||||
return None
|
||||
frappe.msgprint(_("Error has occurred in {0}").format(email_account.name), raise_exception=e.__class__)
|
||||
|
|
|
|||
|
|
@ -1,120 +1,70 @@
|
|||
{
|
||||
"allow_copy": 0,
|
||||
"allow_import": 1,
|
||||
"allow_rename": 0,
|
||||
"autoname": "field:title",
|
||||
"beta": 0,
|
||||
"creation": "2015-03-18 06:08:32.729800",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "Setup",
|
||||
"editable_grid": 0,
|
||||
"actions": [],
|
||||
"allow_import": 1,
|
||||
"autoname": "field:title",
|
||||
"creation": "2015-03-18 06:08:32.729800",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Setup",
|
||||
"field_order": [
|
||||
"title",
|
||||
"total_subscribers",
|
||||
"confirmation_email_template",
|
||||
"welcome_email_template"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "title",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Title",
|
||||
"length": 0,
|
||||
"no_copy": 1,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "title",
|
||||
"fieldtype": "Data",
|
||||
"label": "Title",
|
||||
"no_copy": 1,
|
||||
"reqd": 1,
|
||||
"unique": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "0",
|
||||
"fieldname": "total_subscribers",
|
||||
"fieldtype": "Int",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Total Subscribers",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 1,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
"default": "0",
|
||||
"fieldname": "total_subscribers",
|
||||
"fieldtype": "Int",
|
||||
"in_list_view": 1,
|
||||
"label": "Total Subscribers",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "confirmation_email_template",
|
||||
"fieldtype": "Link",
|
||||
"label": "Confirmation Email Template",
|
||||
"options": "Email Template"
|
||||
},
|
||||
{
|
||||
"fieldname": "welcome_email_template",
|
||||
"fieldtype": "Link",
|
||||
"label": "Welcome Email Template",
|
||||
"options": "Email Template"
|
||||
}
|
||||
],
|
||||
"hide_heading": 0,
|
||||
"hide_toolbar": 0,
|
||||
"idx": 0,
|
||||
"image_view": 0,
|
||||
"in_create": 0,
|
||||
|
||||
"is_submittable": 0,
|
||||
"issingle": 0,
|
||||
"istable": 0,
|
||||
"max_attachments": 0,
|
||||
"modified": "2017-02-27 19:01:17.203845",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Email",
|
||||
"name": "Email Group",
|
||||
"name_case": "",
|
||||
"owner": "Administrator",
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2020-02-21 14:12:48.884738",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Email",
|
||||
"name": "Email Group",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"amend": 0,
|
||||
"apply_user_permissions": 0,
|
||||
"cancel": 0,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"if_owner": 0,
|
||||
"import": 1,
|
||||
"permlevel": 0,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Newsletter Manager",
|
||||
"set_user_permissions": 0,
|
||||
"share": 1,
|
||||
"submit": 0,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"import": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Newsletter Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1,
|
||||
"track_seen": 0
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
|
|
@ -66,6 +66,10 @@ def import_from(name, doctype):
|
|||
def add_subscribers(name, email_list):
|
||||
if not isinstance(email_list, (list, tuple)):
|
||||
email_list = email_list.replace(",", "\n").split("\n")
|
||||
|
||||
template = frappe.db.get_value('Email Group', name, 'welcome_email_template')
|
||||
welcome_email = frappe.get_doc("Email Template", template) if template else None
|
||||
|
||||
count = 0
|
||||
for email in email_list:
|
||||
email = email.strip()
|
||||
|
|
@ -78,7 +82,9 @@ def add_subscribers(name, email_list):
|
|||
"doctype": "Email Group Member",
|
||||
"email_group": name,
|
||||
"email": parsed_email
|
||||
}).insert(ignore_permissions = frappe.flags.ignore_permissions)
|
||||
}).insert(ignore_permissions=frappe.flags.ignore_permissions)
|
||||
|
||||
send_welcome_email(welcome_email, parsed_email, name)
|
||||
|
||||
count += 1
|
||||
else:
|
||||
|
|
@ -90,3 +96,15 @@ def add_subscribers(name, email_list):
|
|||
|
||||
return frappe.get_doc("Email Group", name).update_total_subscribers()
|
||||
|
||||
def send_welcome_email(welcome_email, email, email_group):
|
||||
"""Send welcome email for the subscribers of a given email group."""
|
||||
if not welcome_email:
|
||||
return
|
||||
|
||||
args = dict(
|
||||
email=email,
|
||||
email_group=email_group
|
||||
)
|
||||
|
||||
message = frappe.render_template(welcome_email.response, args)
|
||||
frappe.sendmail(email, subject=welcome_email.subject, message=message)
|
||||
|
|
|
|||
|
|
@ -4,23 +4,65 @@
|
|||
frappe.ui.form.on('Newsletter', {
|
||||
refresh(frm) {
|
||||
let doc = frm.doc;
|
||||
if(!doc.__islocal && !cint(doc.email_sent) && !doc.__unsaved
|
||||
if (!doc.__islocal && !cint(doc.email_sent) && !doc.__unsaved
|
||||
&& in_list(frappe.boot.user.can_write, doc.doctype)) {
|
||||
frm.add_custom_button(__('Send'), function() {
|
||||
frm.call('send_emails').then(() => {
|
||||
frm.refresh();
|
||||
frm.add_custom_button(__('Send Now'), function() {
|
||||
frappe.confirm(__("Do you really want to send this email newsletter?"), function() {
|
||||
frm.call('send_emails').then(() => {
|
||||
frm.refresh();
|
||||
});
|
||||
});
|
||||
}, "fa fa-play", "btn-success");
|
||||
}
|
||||
if (!doc.__islocal && cint(doc.email_sent)) {
|
||||
frm.set_df_property('schedule_send', "read_only", 1);
|
||||
}
|
||||
|
||||
frm.events.setup_dashboard(frm);
|
||||
|
||||
if(doc.__islocal && !doc.send_from) {
|
||||
if (doc.__islocal && !doc.send_from) {
|
||||
let { fullname, email } = frappe.user_info(doc.owner);
|
||||
frm.set_value('send_from', `${fullname} <${email}>`);
|
||||
}
|
||||
},
|
||||
|
||||
onload_post_render(frm) {
|
||||
frm.trigger('setup_schedule_send');
|
||||
},
|
||||
|
||||
setup_schedule_send(frm) {
|
||||
let today = new Date();
|
||||
|
||||
// setting datepicker options to set min date & min time
|
||||
today.setHours(today.getHours() + 1 );
|
||||
frm.get_field('schedule_send').$input.datepicker({
|
||||
maxMinutes: 0,
|
||||
minDate: today,
|
||||
timeFormat: 'hh:00:00',
|
||||
onSelect: function (fd, d, picker) {
|
||||
if (!d) return;
|
||||
var date = d.toDateString();
|
||||
if (date === today.toDateString()) {
|
||||
picker.update({
|
||||
minHours: (today.getHours() + 1)
|
||||
});
|
||||
} else {
|
||||
picker.update({
|
||||
minHours: 0
|
||||
});
|
||||
}
|
||||
frm.get_field('schedule_send').$input.trigger('change');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
const $tp = frm.get_field('schedule_send').datepicker.timepicker;
|
||||
$tp.$minutes.parent().css('display', 'none');
|
||||
$tp.$minutesText.css('display', 'none');
|
||||
$tp.$minutesText.prev().css('display', 'none');
|
||||
$tp.$seconds.parent().css('display', 'none');
|
||||
},
|
||||
|
||||
setup_dashboard(frm) {
|
||||
if(!frm.doc.__islocal && cint(frm.doc.email_sent)
|
||||
&& frm.doc.__onload && frm.doc.__onload.status_count) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2013-01-10 16:34:31",
|
||||
"description": "Create and Send Newsletters",
|
||||
|
|
@ -6,9 +7,11 @@
|
|||
"document_type": "Other",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"send_from",
|
||||
"column_break_2",
|
||||
"schedule_send",
|
||||
"recipients",
|
||||
"email_group",
|
||||
"send_from",
|
||||
"email_sent",
|
||||
"newsletter_content",
|
||||
"subject",
|
||||
|
|
@ -41,7 +44,7 @@
|
|||
"default": "0",
|
||||
"fieldname": "email_sent",
|
||||
"fieldtype": "Check",
|
||||
"label": "Email Sent?",
|
||||
"label": "Email Sent",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
|
|
@ -115,14 +118,24 @@
|
|||
"fieldname": "recipients",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Recipients"
|
||||
},
|
||||
{
|
||||
"fieldname": "schedule_send",
|
||||
"fieldtype": "Datetime",
|
||||
"label": "Schedule Send"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_2",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"has_web_view": 1,
|
||||
"icon": "fa fa-envelope",
|
||||
"idx": 1,
|
||||
"is_published_field": "published",
|
||||
"links": [],
|
||||
"max_attachments": 3,
|
||||
"modified": "2019-09-06 22:15:55.471254",
|
||||
"modified": "2020-03-02 06:26:51.622521",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Email",
|
||||
"name": "Newsletter",
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ from frappe.utils.verified_command import get_signed_params, verify_request
|
|||
from frappe.utils.background_jobs import enqueue
|
||||
from frappe.email.queue import send
|
||||
from frappe.email.doctype.email_group.email_group import add_subscribers
|
||||
from frappe.utils import parse_addr
|
||||
from frappe.utils import parse_addr, now_datetime
|
||||
from frappe.utils import validate_email_address
|
||||
|
||||
|
||||
|
|
@ -42,7 +42,6 @@ class Newsletter(WebsiteGenerator):
|
|||
if self.recipients:
|
||||
if getattr(frappe.local, "is_ajax", False):
|
||||
self.validate_send()
|
||||
|
||||
# using default queue with a longer timeout as this isn't a scheduled task
|
||||
enqueue(send_newsletter, queue='default', timeout=6000, event='send_newsletter',
|
||||
newsletter=self.name)
|
||||
|
|
@ -52,8 +51,6 @@ class Newsletter(WebsiteGenerator):
|
|||
|
||||
frappe.msgprint(_("Scheduled to send to {0} recipients").format(len(self.recipients)))
|
||||
|
||||
frappe.db.set(self, "email_sent", 1)
|
||||
frappe.db.set(self, 'scheduled_to_send', len(self.recipients))
|
||||
else:
|
||||
frappe.msgprint(_("Newsletter should have atleast one recipient"))
|
||||
|
||||
|
|
@ -71,8 +68,8 @@ class Newsletter(WebsiteGenerator):
|
|||
|
||||
attachments = []
|
||||
if self.send_attachements:
|
||||
files = frappe.get_all("File", fields = ["name"], filters = {"attached_to_doctype": "Newsletter",
|
||||
"attached_to_name":self.name}, order_by="creation desc")
|
||||
files = frappe.get_all("File", fields=["name"], filters={"attached_to_doctype": "Newsletter",
|
||||
"attached_to_name": self.name}, order_by="creation desc")
|
||||
|
||||
for file in files:
|
||||
try:
|
||||
|
|
@ -82,17 +79,21 @@ class Newsletter(WebsiteGenerator):
|
|||
except IOError:
|
||||
frappe.throw(_("Unable to find attachment {0}").format(file.name))
|
||||
|
||||
send(recipients = self.recipients, sender = sender,
|
||||
subject = self.subject, message = self.message,
|
||||
reference_doctype = self.doctype, reference_name = self.name,
|
||||
add_unsubscribe_link = self.send_unsubscribe_link, attachments=attachments,
|
||||
unsubscribe_method = "/unsubscribe",
|
||||
unsubscribe_params = {"name": self.name},
|
||||
send_priority = 0, queue_separately=True)
|
||||
send(recipients=self.recipients, sender=sender,
|
||||
subject=self.subject, message=self.message,
|
||||
reference_doctype=self.doctype, reference_name=self.name,
|
||||
add_unsubscribe_link=self.send_unsubscribe_link, attachments=attachments,
|
||||
unsubscribe_method="/unsubscribe",
|
||||
unsubscribe_params={"name": self.name},
|
||||
send_priority=0, queue_separately=True)
|
||||
|
||||
if not frappe.flags.in_test:
|
||||
frappe.db.auto_commit_on_many_writes = False
|
||||
|
||||
self.db_set("email_sent", 1)
|
||||
self.db_set("schedule_send", now_datetime())
|
||||
self.db_set("scheduled_to_send", len(self.recipients))
|
||||
|
||||
def get_recipients(self):
|
||||
"""Get recipients from Email Group"""
|
||||
recipients_list = []
|
||||
|
|
@ -160,39 +161,52 @@ def create_lead(email_id):
|
|||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def subscribe(email):
|
||||
def subscribe(email, email_group=_('Website')):
|
||||
url = frappe.utils.get_url("/api/method/frappe.email.doctype.newsletter.newsletter.confirm_subscription") +\
|
||||
"?" + get_signed_params({"email": email})
|
||||
"?" + get_signed_params({"email": email, "email_group": email_group})
|
||||
|
||||
messages = (
|
||||
_("Thank you for your interest in subscribing to our updates"),
|
||||
_("Please verify your Email Address"),
|
||||
url,
|
||||
_("Click here to verify")
|
||||
)
|
||||
email_template = frappe.db.get_value('Email Group', email_group, ['confirmation_email_template'])
|
||||
|
||||
content = """
|
||||
<p>{0}. {1}.</p>
|
||||
<p><a href="{2}">{3}</a></p>
|
||||
"""
|
||||
content=''
|
||||
if email_template:
|
||||
args = dict(
|
||||
email=email,
|
||||
confirmation_url=url,
|
||||
email_group=email_group
|
||||
)
|
||||
|
||||
frappe.sendmail(email, subject=_("Confirm Your Email"), content=content.format(*messages))
|
||||
email_template = frappe.get_doc("Email Template", email_template)
|
||||
content = frappe.render_template(email_template.response, args)
|
||||
|
||||
if not content:
|
||||
messages = (
|
||||
_("Thank you for your interest in subscribing to our updates"),
|
||||
_("Please verify your Email Address"),
|
||||
url,
|
||||
_("Click here to verify")
|
||||
)
|
||||
|
||||
content = """
|
||||
<p>{0}. {1}.</p>
|
||||
<p><a href="{2}">{3}</a></p>
|
||||
""".format(*messages)
|
||||
|
||||
frappe.sendmail(email, subject=getattr('email_template', 'subject', '') or _("Confirm Your Email"), content=content)
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def confirm_subscription(email):
|
||||
def confirm_subscription(email, email_group=_('Website')):
|
||||
if not verify_request():
|
||||
return
|
||||
|
||||
if not frappe.db.exists("Email Group", _("Website")):
|
||||
if not frappe.db.exists("Email Group", email_group):
|
||||
frappe.get_doc({
|
||||
"doctype": "Email Group",
|
||||
"title": _("Website")
|
||||
"title": email_group
|
||||
}).insert(ignore_permissions=True)
|
||||
|
||||
|
||||
frappe.flags.ignore_permissions = True
|
||||
|
||||
add_subscribers(_("Website"), email)
|
||||
add_subscribers(email_group, email)
|
||||
frappe.db.commit()
|
||||
|
||||
frappe.respond_as_web_page(_("Confirmed"),
|
||||
|
|
@ -212,7 +226,7 @@ def send_newsletter(newsletter):
|
|||
doc.db_set("email_sent", 0)
|
||||
frappe.db.commit()
|
||||
|
||||
frappe.log_error("send_newsletter")
|
||||
frappe.log_error(title='Send Newsletter')
|
||||
|
||||
raise
|
||||
|
||||
|
|
@ -250,3 +264,11 @@ def get_newsletter_list(doctype, txt, filters, limit_start, limit_page_length=20
|
|||
'''.format(','.join(['%s'] * len(email_group_list)),
|
||||
limit_page_length, limit_start), email_group_list, as_dict=1)
|
||||
|
||||
def send_scheduled_email():
|
||||
"""Send scheduled newsletter to the recipients."""
|
||||
scheduled_newsletter = frappe.get_all('Newsletter', filters = {
|
||||
'schedule_send': ('<=', now_datetime()),
|
||||
'email_sent': 0
|
||||
}, fields = ['name'], ignore_ifnull=True)
|
||||
for newsletter in scheduled_newsletter:
|
||||
send_newsletter(newsletter.name)
|
||||
|
|
|
|||
|
|
@ -3,8 +3,9 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
import frappe, unittest
|
||||
from frappe.utils import getdate, add_days
|
||||
|
||||
from frappe.email.doctype.newsletter.newsletter import confirmed_unsubscribe
|
||||
from frappe.email.doctype.newsletter.newsletter import confirmed_unsubscribe, send_scheduled_email
|
||||
from six.moves.urllib.parse import unquote
|
||||
|
||||
test_dependencies = ["Email Group"]
|
||||
|
|
@ -58,7 +59,7 @@ class TestNewsletter(unittest.TestCase):
|
|||
self.assertTrue(email in recipients)
|
||||
|
||||
@staticmethod
|
||||
def send_newsletter(published=0):
|
||||
def send_newsletter(published=0, schedule_send=None):
|
||||
frappe.db.sql("delete from `tabEmail Queue`")
|
||||
frappe.db.sql("delete from `tabEmail Queue Recipient`")
|
||||
frappe.db.sql("delete from `tabNewsletter`")
|
||||
|
|
@ -67,11 +68,16 @@ class TestNewsletter(unittest.TestCase):
|
|||
"subject": "_Test Newsletter",
|
||||
"send_from": "Test Sender <test_sender@example.com>",
|
||||
"message": "Testing my news.",
|
||||
"published": published
|
||||
"published": published,
|
||||
"schedule_send": schedule_send
|
||||
}).insert(ignore_permissions=True)
|
||||
|
||||
newsletter.append("email_group", {"email_group": "_Test Email Group"})
|
||||
newsletter.save()
|
||||
if schedule_send:
|
||||
send_scheduled_email()
|
||||
return
|
||||
|
||||
newsletter.send_emails()
|
||||
return newsletter.name
|
||||
|
||||
|
|
@ -89,4 +95,13 @@ class TestNewsletter(unittest.TestCase):
|
|||
doc = frappe.get_doc("Newsletter", newsletter_name)
|
||||
doc.get_context(context)
|
||||
self.assertEqual(context.no_cache, 1)
|
||||
self.assertTrue("attachments" not in list(context))
|
||||
self.assertTrue("attachments" not in list(context))
|
||||
|
||||
def test_schedule_send(self):
|
||||
self.send_newsletter(schedule_send=add_days(getdate(), -1))
|
||||
|
||||
email_queue_list = [frappe.get_doc('Email Queue', e.name) for e in frappe.get_all("Email Queue")]
|
||||
self.assertEqual(len(email_queue_list), 4)
|
||||
recipients = [e.recipients[0].recipient for e in email_queue_list]
|
||||
for email in emails:
|
||||
self.assertTrue(email in recipients)
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue