Merge branch 'develop' into report_view_scroll_fix

This commit is contained in:
Suraj Shetty 2021-07-02 11:13:13 +05:30 committed by GitHub
commit d1af0ae4c0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
1032 changed files with 6802 additions and 8187 deletions

View file

@ -18,7 +18,7 @@ def is_js(file):
return file.endswith("js")
def is_docs(file):
regex = re.compile('\.(md|png|jpg|jpeg)$|^.github|LICENSE')
regex = re.compile(r'\.(md|png|jpg|jpeg)$|^.github|LICENSE')
return bool(regex.search(file))

9
.github/helper/semgrep_rules/ux.js vendored Normal file
View file

@ -0,0 +1,9 @@
// ok: frappe-missing-translate-function-js
frappe.msgprint('{{ _("Both login and password required") }}');
// ruleid: frappe-missing-translate-function-js
frappe.msgprint('What');
// ok: frappe-missing-translate-function-js
frappe.throw(' {{ _("Both login and password required") }}. ');

View file

@ -2,30 +2,30 @@ import frappe
from frappe import msgprint, throw, _
# ruleid: frappe-missing-translate-function
# ruleid: frappe-missing-translate-function-python
throw("Error Occured")
# ruleid: frappe-missing-translate-function
# ruleid: frappe-missing-translate-function-python
frappe.throw("Error Occured")
# ruleid: frappe-missing-translate-function
# ruleid: frappe-missing-translate-function-python
frappe.msgprint("Useful message")
# ruleid: frappe-missing-translate-function
# ruleid: frappe-missing-translate-function-python
msgprint("Useful message")
# ok: frappe-missing-translate-function
# ok: frappe-missing-translate-function-python
translatedmessage = _("Hello")
# ok: frappe-missing-translate-function
# ok: frappe-missing-translate-function-python
throw(translatedmessage)
# ok: frappe-missing-translate-function
# ok: frappe-missing-translate-function-python
msgprint(translatedmessage)
# ok: frappe-missing-translate-function
# ok: frappe-missing-translate-function-python
msgprint(_("Helpful message"))
# ok: frappe-missing-translate-function
# ok: frappe-missing-translate-function-python
frappe.throw(_("Error occured"))

View file

@ -1,15 +1,30 @@
rules:
- id: frappe-missing-translate-function
- id: frappe-missing-translate-function-python
pattern-either:
- patterns:
- pattern: frappe.msgprint("...", ...)
- pattern-not: frappe.msgprint(_("..."), ...)
- pattern-not: frappe.msgprint(__("..."), ...)
- patterns:
- pattern: frappe.throw("...", ...)
- pattern-not: frappe.throw(_("..."), ...)
- pattern-not: frappe.throw(__("..."), ...)
message: |
All user facing text must be wrapped in translate function. Please refer to translation documentation. https://frappeframework.com/docs/user/en/guides/basics/translations
languages: [python, javascript, json]
languages: [python]
severity: ERROR
- id: frappe-missing-translate-function-js
pattern-either:
- patterns:
- pattern: frappe.msgprint("...", ...)
- pattern-not: frappe.msgprint(__("..."), ...)
# ignore microtemplating e.g. msgprint("{{ _("server side translation") }}")
- pattern-not: frappe.msgprint("=~/\{\{.*\_.*\}\}/i", ...)
- patterns:
- pattern: frappe.throw("...", ...)
- pattern-not: frappe.throw(__("..."), ...)
# ignore microtemplating
- pattern-not: frappe.throw("=~/\{\{.*\_.*\}\}/i", ...)
message: |
All user facing text must be wrapped in translate function. Please refer to translation documentation. https://frappeframework.com/docs/user/en/guides/basics/translations
languages: [javascript]
severity: ERROR

View file

@ -0,0 +1,83 @@
name: Patch
on: [pull_request, workflow_dispatch]
jobs:
test:
runs-on: ubuntu-18.04
name: Patch Test
services:
mysql:
image: mariadb:10.3
env:
MYSQL_ALLOW_EMPTY_PASSWORD: YES
ports:
- 3306:3306
options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3
steps:
- name: Clone
uses: actions/checkout@v2
- name: Setup Python
uses: actions/setup-python@v2
with:
python-version: 3.7
- name: Add to Hosts
run: echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts
- name: Cache pip
uses: actions/cache@v2
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-
${{ runner.os }}-
- name: Cache node modules
uses: actions/cache@v2
env:
cache-name: cache-node-modules
with:
path: ~/.npm
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build-
${{ runner.os }}-
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- uses: actions/cache@v2
id: yarn-cache
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Install Dependencies
run: bash ${GITHUB_WORKSPACE}/.github/helper/install_dependencies.sh
env:
BEFORE: ${{ env.GITHUB_EVENT_PATH.before }}
AFTER: ${{ env.GITHUB_EVENT_PATH.after }}
TYPE: server
- name: Install
run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
env:
DB: mariadb
TYPE: server
- name: Run Patch Tests
run: |
cd ~/frappe-bench/
wget https://frappeframework.com/files/v10-frappe.sql.gz
bench --site test_site --force restore ~/frappe-bench/v10-frappe.sql.gz
bench --site test_site migrate

View file

@ -3,6 +3,8 @@ name: Server
on:
pull_request:
workflow_dispatch:
push:
branches: [ develop ]
jobs:
test:
@ -89,7 +91,6 @@ jobs:
DB: mariadb
TYPE: server
- name: Run Tests
run: cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --use-orchestrator --with-coverage
env:

View file

@ -3,6 +3,8 @@ name: UI
on:
pull_request:
workflow_dispatch:
push:
branches: [ develop ]
jobs:
test:
@ -103,3 +105,5 @@ jobs:
- name: UI Tests
run: cd ~/frappe-bench/ && bench --site test_site run-ui-tests frappe --headless --parallel --ci-build-id $GITHUB_RUN_ID
env:
CYPRESS_RECORD_KEY: 4a48f41c-11b3-425b-aa88-c58048fa69eb

View file

@ -4,13 +4,10 @@
# the repo. Unless a later match takes precedence,
* @frappe/frappe-review-team
website/ @prssanna
web_form/ @prssanna
templates/ @surajshetty3416
www/ @surajshetty3416
integrations/ @leela
patches/ @surajshetty3416
dashboard/ @prssanna
email/ @leela
event_streaming/ @ruchamahabal
data_import* @netchampfaris

View file

@ -14,18 +14,21 @@
</div>
<div align="center">
<a href="https://github.com/frappe/frappe/actions/workflows/ci-tests.yml">
<img src="https://github.com/frappe/frappe/actions/workflows/ci-tests.yml/badge.svg?branch=develop">
</a>
<a href='https://frappeframework.com/docs'>
<img src='https://img.shields.io/badge/docs-📖-7575FF.svg?style=flat-square'/>
</a>
<a href="https://github.com/frappe/frappe/actions/workflows/server-mariadb-tests.yml">
<img src="https://github.com/frappe/frappe/actions/workflows/server-mariadb-tests.yml/badge.svg">
</a>
<a href="https://github.com/frappe/frappe/actions/workflows/ui-tests.yml">
<img src="https://github.com/frappe/frappe/actions/workflows/ui-tests.yml/badge.svg?branch=develop">
</a>
<a href='https://frappeframework.com/docs'>
<img src='https://img.shields.io/badge/docs-📖-7575FF.svg?style=flat-square'/>
</a>
<a href='https://www.codetriage.com/frappe/frappe'>
<img src='https://www.codetriage.com/frappe/frappe/badges/users.svg'>
</a>
<a href='https://coveralls.io/github/frappe/frappe?branch=develop'>
<img src='https://coveralls.io/repos/github/frappe/frappe/badge.svg?branch=develop'>
</a>
<a href='https://coveralls.io/github/frappe/frappe?branch=develop'>
<img src='https://coveralls.io/repos/github/frappe/frappe/badge.svg?branch=develop'>
</a>
</div>

View file

@ -18,6 +18,7 @@ context('Form', () => {
cy.get('.primary-action').click();
cy.wait('@form_save').its('response.statusCode').should('eq', 200);
cy.visit('/app/todo');
cy.wait(300);
cy.get('.title-text').should('be.visible').and('contain', 'To Do');
cy.get('.list-row').should('contain', 'this is a test todo');
});

View file

@ -0,0 +1,88 @@
context('Form Tour', () => {
before(() => {
cy.login();
cy.visit('/app/form-tour');
return cy.window().its('frappe').then(frappe => {
return frappe.call("frappe.tests.ui_test_helpers.create_form_tour");
});
});
const open_test_form_tour = () => {
cy.visit('/app/form-tour/Test Form Tour');
cy.get('button[data-label="Show%20Tour"]').should('be.visible').and('contain', 'Show Tour').as('show_tour');
cy.get('@show_tour').click();
cy.wait(500);
cy.url().should('include', '/app/contact');
};
it('jump to a form tour', open_test_form_tour);
it('navigates a form tour', () => {
open_test_form_tour();
cy.get('#driver-popover-item').should('be.visible');
cy.get('.frappe-control[data-fieldname="first_name"]').as('first_name');
cy.get('@first_name').should('have.class', 'driver-highlighted-element');
cy.get('.driver-next-btn').as('next_btn');
// next btn shouldn't move to next step, if first name is not entered
cy.get('@next_btn').click();
cy.wait(500);
cy.get('@first_name').should('have.class', 'driver-highlighted-element');
// after filling the field, next step should be highlighted
cy.fill_field('first_name', 'Test Name', 'Data');
cy.wait(500);
cy.get('@next_btn').click();
cy.wait(500);
// assert field is highlighted
cy.get('.frappe-control[data-fieldname="last_name"]').as('last_name');
cy.get('@last_name').should('have.class', 'driver-highlighted-element');
// after filling the field, next step should be highlighted
cy.fill_field('last_name', 'Test Last Name', 'Data');
cy.wait(500);
cy.get('@next_btn').click();
cy.wait(500);
// assert field is highlighted
cy.get('.frappe-control[data-fieldname="phone_nos"]').as('phone_nos');
cy.get('@phone_nos').should('have.class', 'driver-highlighted-element');
// move to next step
cy.wait(500);
cy.get('@next_btn').click();
cy.wait(500);
// assert add row btn is highlighted
cy.get('@phone_nos').find('.grid-add-row').as('add_row');
cy.get('@add_row').should('have.class', 'driver-highlighted-element');
// add a row & move to next step
cy.wait(500);
cy.get('@add_row').click();
cy.wait(500);
// assert table field is highlighted
cy.get('.grid-row-open .frappe-control[data-fieldname="phone"]').as('phone');
cy.get('@phone').should('have.class', 'driver-highlighted-element');
// enter value in a table field
cy.fill_table_field('phone_nos', '1', 'phone', '1234567890');
// move to collapse row step
cy.wait(500);
cy.get('@next_btn').click();
cy.wait(500);
// collapse row
cy.get('.grid-row-open .grid-collapse-row').click();
cy.wait(500);
// assert save btn is highlighted
cy.get('.primary-action').should('have.class', 'driver-highlighted-element');
cy.get('@next_btn').should('contain', 'Save');
});
});

View file

@ -258,12 +258,17 @@ function get_watch_config() {
async function clean_dist_folders(apps) {
for (let app of apps) {
let public_path = get_public_path(app);
await fs.promises.rmdir(path.resolve(public_path, "dist", "js"), {
recursive: true
});
await fs.promises.rmdir(path.resolve(public_path, "dist", "css"), {
recursive: true
});
let paths = [
path.resolve(public_path, "dist", "js"),
path.resolve(public_path, "dist", "css")
];
for (let target of paths) {
if (fs.existsSync(target)) {
// rmdir is deprecated in node 16, this will work in both node 14 and 16
let rmdir = fs.promises.rm || fs.promises.rmdir;
await rmdir(target, { recursive: true });
}
}
}
}
@ -343,12 +348,7 @@ async function write_assets_json(metafile) {
}
}
let assets_json_path = path.resolve(
assets_path,
"frappe",
"dist",
"assets.json"
);
let assets_json_path = path.resolve(assets_path, "assets.json");
let assets_json;
try {
assets_json = await fs.promises.readFile(assets_json_path, "utf-8");

View file

@ -21,7 +21,6 @@ if _dev_server:
from werkzeug.local import Local, release_local
import sys, importlib, inspect, json
import typing
from past.builtins import cmp
import click
# Local application imports
@ -528,16 +527,20 @@ def sendmail(recipients=[], sender="", subject="No Subject", message="No Message
if not delayed:
now = True
from frappe.email import queue
queue.send(recipients=recipients, sender=sender,
from frappe.email.doctype.email_queue.email_queue import QueueBuilder
builder = QueueBuilder(recipients=recipients, sender=sender,
subject=subject, message=message, text_content=text_content,
reference_doctype = doctype or reference_doctype, reference_name = name or reference_name, add_unsubscribe_link=add_unsubscribe_link,
unsubscribe_method=unsubscribe_method, unsubscribe_params=unsubscribe_params, unsubscribe_message=unsubscribe_message,
attachments=attachments, reply_to=reply_to, cc=cc, bcc=bcc, message_id=message_id, in_reply_to=in_reply_to,
send_after=send_after, expose_recipients=expose_recipients, send_priority=send_priority, queue_separately=queue_separately,
communication=communication, now=now, read_receipt=read_receipt, is_notification=is_notification,
communication=communication, read_receipt=read_receipt, is_notification=is_notification,
inline_images=inline_images, header=header, print_letterhead=print_letterhead, with_container=with_container)
# build email queue and send the email if send_now is True.
builder.process(send_now=now)
whitelisted = []
guest_methods = []
xss_safe_methods = []
@ -1107,9 +1110,7 @@ def setup_module_map():
if not (local.app_modules and local.module_app):
local.module_app, local.app_modules = {}, {}
for app in get_all_apps(True):
if app == "webnotes":
app = "frappe"
for app in get_all_apps(with_internal_apps=True):
local.app_modules.setdefault(app, [])
for module in get_module_list(app):
module = scrub(module)
@ -1490,7 +1491,7 @@ def get_print(doctype=None, name=None, print_format=None, style=None,
:param style: Print Format style.
:param as_pdf: Return as PDF. Default False.
:param password: Password to encrypt the pdf with. Default None"""
from frappe.website.render import build_page
from frappe.website.serve import get_response_content
from frappe.utils.pdf import get_pdf
local.form_dict.doctype = doctype
@ -1505,7 +1506,7 @@ def get_print(doctype=None, name=None, print_format=None, style=None,
options = {'password': password}
if not html:
html = build_page("printview")
html = get_response_content("printview")
if as_pdf:
return get_pdf(html, output = output, options = options)
@ -1682,7 +1683,7 @@ def get_desk_link(doctype, name):
)
def bold(text):
return '<b>{0}</b>'.format(text)
return '<strong>{0}</strong>'.format(text)
def safe_eval(code, eval_globals=None, eval_locals=None):
'''A safer `eval`'''
@ -1693,6 +1694,23 @@ def safe_eval(code, eval_globals=None, eval_locals=None):
"round": round
}
UNSAFE_ATTRIBUTES = {
# Generator Attributes
"gi_frame", "gi_code",
# Coroutine Attributes
"cr_frame", "cr_code", "cr_origin",
# Async Generator Attributes
"ag_code", "ag_frame",
# Traceback Attributes
"tb_frame", "tb_next",
# Format Attributes
"format", "format_map",
}
for attribute in UNSAFE_ATTRIBUTES:
if attribute in code:
throw('Illegal rule {0}. Cannot use "{1}"'.format(bold(code), attribute))
if '__' in code:
throw('Illegal rule {0}. Cannot use "__"'.format(bold(code)))

View file

@ -1,6 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
import base64
import binascii
import json
@ -11,6 +10,7 @@ import frappe.client
import frappe.handler
from frappe import _
from frappe.utils.response import build_response
from frappe.utils.data import sbool
def handle():
@ -108,25 +108,40 @@ def handle():
elif doctype:
if frappe.local.request.method == "GET":
if frappe.local.form_dict.get('fields'):
frappe.local.form_dict['fields'] = json.loads(frappe.local.form_dict['fields'])
frappe.local.form_dict.setdefault('limit_page_length', 20)
frappe.local.response.update({
"data": frappe.call(
frappe.client.get_list,
doctype,
**frappe.local.form_dict
)
})
# set fields for frappe.get_list
if frappe.local.form_dict.get("fields"):
frappe.local.form_dict["fields"] = json.loads(frappe.local.form_dict["fields"])
# set limit of records for frappe.get_list
frappe.local.form_dict.setdefault(
"limit_page_length",
frappe.local.form_dict.limit or frappe.local.form_dict.limit_page_length or 20,
)
# convert strings to native types - only as_dict and debug accept bool
for param in ["as_dict", "debug"]:
param_val = frappe.local.form_dict.get(param)
if param_val is not None:
frappe.local.form_dict[param] = sbool(param_val)
# evaluate frappe.get_list
data = frappe.call(frappe.client.get_list, doctype, **frappe.local.form_dict)
# set frappe.get_list result to response
frappe.local.response.update({"data": data})
if frappe.local.request.method == "POST":
# fetch data from from dict
data = get_request_form_data()
data.update({
"doctype": doctype
})
frappe.local.response.update({
"data": frappe.get_doc(data).insert().as_dict()
})
data.update({"doctype": doctype})
# insert document from request data
doc = frappe.get_doc(data).insert()
# set response data
frappe.local.response.update({"data": doc.as_dict()})
# commit for POST requests
frappe.db.commit()
else:
raise frappe.DoesNotExistError

View file

@ -1,10 +1,8 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
from __future__ import unicode_literals
import os
from six import iteritems
import logging
from werkzeug.local import LocalManager
@ -18,9 +16,9 @@ import frappe.handler
import frappe.auth
import frappe.api
import frappe.utils.response
import frappe.website.render
from frappe.utils import get_site_name, sanitize_html
from frappe.middlewares import StaticDataMiddleware
from frappe.website.serve import get_response
from frappe.utils.error import make_error_snapshot
from frappe.core.doctype.comment.comment import update_comments_in_parent_after_request
from frappe import _
@ -74,7 +72,7 @@ def application(request):
response = frappe.utils.response.download_private_file(request.path)
elif request.method in ('GET', 'HEAD', 'POST'):
response = frappe.website.render.render()
response = get_response()
else:
raise NotFound
@ -191,8 +189,9 @@ def make_form_dict(request):
frappe.throw(_("Invalid request arguments"))
try:
frappe.local.form_dict = frappe._dict({ k:v[0] if isinstance(v, (list, tuple)) else v \
for k, v in iteritems(args) })
frappe.local.form_dict = frappe._dict({
k: v[0] if isinstance(v, (list, tuple)) else v for k, v in args.items()
})
except IndexError:
frappe.local.form_dict = frappe._dict(args)
@ -267,8 +266,7 @@ def handle_exception(e):
make_error_snapshot(e)
if return_as_message:
response = frappe.website.render.render("message",
http_status_code=http_status_code)
response = get_response("message", http_status_code=http_status_code)
return response

View file

@ -1,9 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
from __future__ import unicode_literals
import datetime
from frappe import _
import frappe
import frappe.database
@ -19,8 +16,7 @@ from frappe.core.doctype.activity_log.activity_log import add_authentication_log
from frappe.twofactor import (should_run_2fa, authenticate_for_2factor,
confirm_otp_token, get_cached_user_pass)
from frappe.website.utils import get_home_page
from six.moves.urllib.parse import quote
from urllib.parse import quote
class HTTPRequest:

View file

@ -2,8 +2,6 @@
# Copyright (c) 2019, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
from frappe.desk.form import assign_to

View file

@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and Contributors
# See license.txt
from __future__ import unicode_literals
import frappe
import unittest
from frappe.utils import random_string

View file

@ -2,7 +2,6 @@
# Copyright (c) 2019, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document

View file

@ -2,7 +2,6 @@
# Copyright (c) 2019, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document

View file

@ -2,7 +2,6 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe import _
from datetime import timedelta
@ -334,7 +333,7 @@ class AutoRepeat(Document):
if self.reference_doctype and self.reference_document:
res = get_contacts_linking_to(self.reference_doctype, self.reference_document, fields=['email_id'])
res += get_contacts_linked_from(self.reference_doctype, self.reference_document, fields=['email_id'])
email_ids = list(set([d.email_id for d in res]))
email_ids = {d.email_id for d in res}
if not email_ids:
frappe.msgprint(_('No contacts linked to document'), alert=True)
else:

View file

@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2018, Frappe Technologies and Contributors
# See license.txt
from __future__ import unicode_literals
import unittest
import frappe

View file

@ -2,7 +2,6 @@
# 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

View file

@ -2,8 +2,6 @@
# Copyright (c) 2019, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document

View file

@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and Contributors
# See license.txt
from __future__ import unicode_literals
#import frappe
import unittest

View file

@ -2,8 +2,6 @@
# Copyright (c) 2019, 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.cache_manager

View file

@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and Contributors
# See license.txt
from __future__ import unicode_literals
import frappe
import frappe.cache_manager
import unittest

View file

@ -1,10 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
from __future__ import unicode_literals
from six import iteritems, text_type
"""
bootstrap client session
"""
@ -75,7 +70,7 @@ def get_bootinfo():
frappe.get_attr(method)(bootinfo)
if bootinfo.lang:
bootinfo.lang = text_type(bootinfo.lang)
bootinfo.lang = str(bootinfo.lang)
bootinfo.versions = {k: v['version'] for k, v in get_versions().items()}
bootinfo.error_report_email = frappe.conf.error_report_email
@ -220,7 +215,7 @@ def load_translations(bootinfo):
messages[name] = frappe._(name)
# only untranslated
messages = {k:v for k, v in iteritems(messages) if k!=v}
messages = {k: v for k, v in messages.items() if k!=v}
bootinfo["__messages"] = messages

View file

@ -1,11 +1,11 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
import os
import re
import json
import shutil
import subprocess
from io import StringIO
from tempfile import mkdtemp, mktemp
from distutils.spawn import find_executable
@ -50,7 +50,7 @@ def build_missing_files():
development = frappe.local.conf.developer_mode or frappe.local.dev_server
build_mode = "development" if development else "production"
assets_json = frappe.read_file(frappe.get_app_path('frappe', 'public', 'dist', 'assets.json'))
assets_json = frappe.read_file("assets/assets.json")
if assets_json:
assets_json = frappe.parse_json(assets_json)
@ -402,8 +402,6 @@ def get_build_maps():
def pack(target, sources, no_compress, verbose):
from six import StringIO
outtype, outtxt = target.split(".")[-1], ""
jsm = JavascriptMinify()

View file

@ -1,8 +1,6 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
from __future__ import unicode_literals
import frappe, json
from frappe.model.document import Document
from frappe.desk.notifications import (delete_notification_count_for,
@ -55,7 +53,7 @@ def clear_domain_cache(user=None):
cache.delete_value(domain_cache_keys)
def clear_global_cache():
from frappe.website.render import clear_cache as clear_website_cache
from frappe.website.utils import clear_website_cache
clear_doctype_cache()
clear_website_cache()

View file

@ -1,4 +1,4 @@
from __future__ import unicode_literals
import frappe
from frappe import _

View file

@ -1,5 +1,3 @@
from __future__ import unicode_literals
# imports - standard imports
import json

View file

@ -1,5 +1,3 @@
from __future__ import unicode_literals
# imports - module imports
from frappe.model.document import Document
from frappe import _

View file

@ -1,5 +1,3 @@
from __future__ import unicode_literals
# imports - module imports
from frappe.model.document import Document
from frappe import _

View file

@ -1,5 +1,3 @@
from __future__ import unicode_literals
# imports - module imports
from frappe.model.document import Document
import frappe

View file

@ -2,7 +2,6 @@
# Copyright (c) 2018, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document

View file

@ -1,5 +1,3 @@
from __future__ import unicode_literals
# imports - module imports
from frappe.chat.util.util import (
get_user_doc,

View file

@ -1,5 +1,3 @@
from __future__ import unicode_literals
# imports - standard imports
import unittest
@ -9,7 +7,6 @@ from frappe.chat.util import (
safe_json_loads
)
import frappe
import six
class TestChatUtil(unittest.TestCase):
def test_safe_json_loads(self):
@ -20,7 +17,7 @@ class TestChatUtil(unittest.TestCase):
self.assertEqual(type(number), float)
string = safe_json_loads("foobar")
self.assertEqual(type(string), six.text_type)
self.assertEqual(type(string), str)
array = safe_json_loads('[{ "foo": "bar" }]')
self.assertEqual(type(array), list)

View file

@ -1,5 +1,3 @@
from __future__ import unicode_literals
# imports - standard imports
import json
from collections.abc import MutableMapping, MutableSequence, Sequence

View file

@ -1,4 +1,4 @@
from __future__ import unicode_literals
import frappe
from frappe.chat.util import filter_dict, safe_json_loads

View file

@ -1,7 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
from __future__ import unicode_literals
import frappe
from frappe import _
import frappe.model
@ -11,7 +9,6 @@ from frappe.utils import get_safe_filters
from frappe.desk.reportview import validate_args
from frappe.model.db_query import check_parent_permission
from six import iteritems, string_types, integer_types
'''
Handle RESTful requests that are mapped to the `/api/resource` route.
@ -86,7 +83,7 @@ def get_value(doctype, fieldname, filters=None, as_dict=True, debug=False, paren
frappe.throw(_("No permission for {0}").format(doctype), frappe.PermissionError)
filters = get_safe_filters(filters)
if isinstance(filters, string_types):
if isinstance(filters, str):
filters = {"name": filters}
try:
@ -135,7 +132,7 @@ def set_value(doctype, name, fieldname, value=None):
if not value:
values = fieldname
if isinstance(fieldname, string_types):
if isinstance(fieldname, str):
try:
values = json.loads(fieldname)
except ValueError:
@ -161,7 +158,7 @@ def insert(doc=None):
'''Insert a document
:param doc: JSON or dict object to be inserted'''
if isinstance(doc, string_types):
if isinstance(doc, str):
doc = json.loads(doc)
if doc.get("parent") and doc.get("parenttype"):
@ -179,7 +176,7 @@ def insert_many(docs=None):
'''Insert multiple documents
:param docs: JSON or list of dict objects to be inserted in one request'''
if isinstance(docs, string_types):
if isinstance(docs, str):
docs = json.loads(docs)
out = []
@ -205,7 +202,7 @@ def save(doc):
'''Update (save) an existing document
:param doc: JSON or dict object with the properties of the document to be updated'''
if isinstance(doc, string_types):
if isinstance(doc, str):
doc = json.loads(doc)
doc = frappe.get_doc(doc)
@ -228,7 +225,7 @@ def submit(doc):
'''Submit a document
:param doc: JSON or dict object to be submitted remotely'''
if isinstance(doc, string_types):
if isinstance(doc, str):
doc = json.loads(doc)
doc = frappe.get_doc(doc)
@ -266,7 +263,7 @@ def make_width_property_setter(doc):
'''Set width Property Setter
:param doc: Property Setter document with `width` property'''
if isinstance(doc, string_types):
if isinstance(doc, str):
doc = json.loads(doc)
if doc["doctype"]=="Property Setter" and doc["property"]=="width":
frappe.get_doc(doc).insert(ignore_permissions = True)
@ -280,7 +277,7 @@ def bulk_update(docs):
failed_docs = []
for doc in docs:
try:
ddoc = {key: val for key, val in iteritems(doc) if key not in ['doctype', 'docname']}
ddoc = {key: val for key, val in doc.items() if key not in ['doctype', 'docname']}
doctype = doc['doctype']
docname = doc['docname']
doc = frappe.get_doc(doctype, docname)

View file

@ -1,7 +1,6 @@
# Copyright (c) 2015, Web Notes Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
from __future__ import unicode_literals, absolute_import, print_function
import sys
import click
import cProfile
@ -10,7 +9,7 @@ import frappe
import frappe.utils
import subprocess # nosec
from functools import wraps
from six import StringIO
from io import StringIO
from os import environ
click.disable_unicode_literals_warning = True

View file

@ -1,4 +1,3 @@
from __future__ import unicode_literals, absolute_import, print_function
import click
import sys
import frappe

View file

@ -1,4 +1,3 @@
from __future__ import unicode_literals, absolute_import, print_function
import click
from frappe.commands import pass_context, get_site
from frappe.exceptions import SiteNotSpecifiedError

View file

@ -69,14 +69,14 @@ def watch(apps=None):
def clear_cache(context):
"Clear cache, doctype cache and defaults"
import frappe.sessions
import frappe.website.render
from frappe.website.utils import clear_website_cache
from frappe.desk.notifications import clear_notifications
for site in context.sites:
try:
frappe.connect(site)
frappe.clear_cache()
clear_notifications()
frappe.website.render.clear_cache()
clear_website_cache()
finally:
frappe.destroy()
if not context.sites:
@ -86,12 +86,12 @@ def clear_cache(context):
@pass_context
def clear_website_cache(context):
"Clear website cache"
import frappe.website.render
from frappe.website.utils import clear_website_cache
for site in context.sites:
try:
frappe.init(site=site)
frappe.connect()
frappe.website.render.clear_cache()
clear_website_cache()
finally:
frappe.destroy()
if not context.sites:
@ -222,7 +222,7 @@ def execute(context, method, args=None, kwargs=None, profile=False):
if profile:
import pstats
from six import StringIO
from io import StringIO
pr.disable()
s = StringIO()
@ -572,22 +572,29 @@ def run_tests(context, app=None, module=None, doctype=None, test=(), profile=Fal
# Generate coverage report only for app that is being tested
source_path = os.path.join(get_bench_path(), 'apps', app or 'frappe')
omit=[
'*.html',
incl = [
'*.py',
]
omit = [
'*.js',
'*.xml',
'*.pyc',
'*.css',
'*.less',
'*.scss',
'*.vue',
'*.html',
'*/test_*',
'*/node_modules/*',
'*/doctype/*/*_dashboard.py',
'*/patches/*'
'*/patches/*',
]
if not app or app == 'frappe':
omit.append('*/tests/*')
omit.append('*/commands/*')
cov = Coverage(source=[source_path], omit=omit)
cov = Coverage(source=[source_path], omit=omit, include=incl)
cov.start()
ret = frappe.test_runner.main(app, module, doctype, context.verbose, tests=tests,
@ -654,7 +661,7 @@ def run_ui_tests(context, app, headless=False, parallel=True, ci_build_id=None):
frappe.commands.popen("yarn add cypress@^6 cypress-file-upload@^5 --no-lockfile")
# run for headless mode
run_or_open = 'run --browser firefox --record --key 4a48f41c-11b3-425b-aa88-c58048fa69eb' if headless else 'open'
run_or_open = 'run --browser firefox --record' if headless else 'open'
command = '{site_env} {password_env} {cypress} {run_or_open}'
formatted_command = command.format(site_env=site_env, password_env=password_env, cypress=cypress_path, run_or_open=run_or_open)

View file

@ -1,6 +1,3 @@
from __future__ import unicode_literals
import json
from six import iteritems
import frappe
from frappe import _
from frappe.desk.moduleview import (get_data, get_onboard_items, config_exists, get_module_link_items_from_list)

View file

@ -1,7 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
import frappe
from frappe import _
@ -154,7 +153,7 @@ def filter_dynamic_link_doctypes(doctype, txt, searchfield, start, page_len, fil
doctypes = frappe.db.get_all("DocField", filters=filters, fields=["parent"],
distinct=True, as_list=True)
doctypes = tuple([d for d in doctypes if re.search(txt+".*", _(d[0]), re.IGNORECASE)])
doctypes = tuple(d for d in doctypes if re.search(txt+".*", _(d[0]), re.IGNORECASE))
filters.update({
"dt": ("not in", [d[0] for d in doctypes])

View file

@ -2,7 +2,6 @@
# Copyright (c) 2015, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe import throw, _
@ -10,15 +9,10 @@ from frappe.utils import cstr
from frappe.model.document import Document
from jinja2 import TemplateSyntaxError
from frappe.utils.user import is_website_user
from frappe.model.naming import make_autoname
from frappe.core.doctype.dynamic_link.dynamic_link import deduplicate_dynamic_links
from six import iteritems, string_types
from past.builtins import cmp
from frappe.contacts.address_and_contact import set_link_title
import functools
class Address(Document):
def __setup__(self):
@ -112,10 +106,13 @@ def get_default_address(doctype, name, sort_key='is_primary_address'):
WHERE
dl.parent = addr.name and dl.link_doctype = %s and
dl.link_name = %s and ifnull(addr.disabled, 0) = 0
""" %(sort_key, '%s', '%s'), (doctype, name))
""" %(sort_key, '%s', '%s'), (doctype, name), as_dict=True)
if out:
return sorted(out, key = functools.cmp_to_key(lambda x,y: cmp(y[1], x[1])))[0][0]
for contact in out:
if contact.get(sort_key):
return contact.name
return out[0].name
else:
return None
@ -141,7 +138,7 @@ def get_territory_from_address(address):
if not address:
return
if isinstance(address, string_types):
if isinstance(address, str):
address = frappe.get_cached_doc("Address", address)
territory = None
@ -174,14 +171,11 @@ def get_address_list(doctype, txt, filters, limit_start, limit_page_length = 20,
def has_website_permission(doc, ptype, user, verbose=False):
"""Returns true if there is a related lead or contact related to this document"""
contact_name = frappe.db.get_value("Contact", {"email_id": frappe.session.user})
if contact_name:
contact = frappe.get_doc('Contact', contact_name)
return contact.has_common_link(doc)
lead_name = frappe.db.get_value("Lead", {"email_id": frappe.session.user})
if lead_name:
return doc.has_link('Lead', lead_name)
return False
def get_address_templates(address):
@ -214,7 +208,7 @@ def address_query(doctype, txt, searchfield, start, page_len, filters):
condition = ""
meta = frappe.get_meta("Address")
for fieldname, value in iteritems(filters):
for fieldname, value in filters.items():
if meta.get_field(fieldname) or fieldname in frappe.db.DEFAULT_COLUMNS:
condition += " and {field}={value}".format(
field=fieldname,
@ -263,7 +257,7 @@ def address_query(doctype, txt, searchfield, start, page_len, filters):
def get_condensed_address(doc):
fields = ["address_title", "address_line1", "address_line2", "city", "county", "state", "country"]
return ", ".join([doc.get(d) for d in fields if doc.get(d)])
return ", ".join(doc.get(d) for d in fields if doc.get(d))
def update_preferred_address(address, field):
frappe.db.set_value('Address', address, field, 0)

View file

@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and Contributors
# See license.txt
from __future__ import unicode_literals
import frappe, unittest
from frappe.contacts.doctype.address.address import get_address_display

View file

@ -2,7 +2,6 @@
# Copyright (c) 2015, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
from frappe.utils import cint

View file

@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and Contributors
# See license.txt
from __future__ import unicode_literals
import frappe, unittest
class TestAddressTemplate(unittest.TestCase):
@ -42,4 +40,4 @@ class TestAddressTemplate(unittest.TestCase):
"doctype": "Address Template",
"country": 'Brazil',
"template": template
}).insert()
}).insert()

View file

@ -1,18 +1,13 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
import frappe
from frappe.utils import cstr, has_gravatar, cint
from frappe.utils import cstr, has_gravatar
from frappe import _
from frappe.model.document import Document
from frappe.core.doctype.dynamic_link.dynamic_link import deduplicate_dynamic_links
from six import iteritems
from past.builtins import cmp
from frappe.model.naming import append_number_if_name_exists
from frappe.contacts.address_and_contact import set_link_title
import functools
class Contact(Document):
def autoname(self):
@ -120,7 +115,7 @@ class Contact(Document):
if len(is_primary) > 1:
frappe.throw(_("Only one {0} can be set as primary.").format(frappe.bold(frappe.unscrub(fieldname))))
primary_number_exists = False
primary_number_exists = False
for d in self.phone_nos:
if d.get(field_name) == 1:
primary_number_exists = True
@ -140,10 +135,13 @@ def get_default_contact(doctype, name):
where
dl.link_doctype=%s and
dl.link_name=%s and
dl.parenttype = "Contact"''', (doctype, name))
dl.parenttype = "Contact"''', (doctype, name), as_dict=True)
if out:
return sorted(out, key = functools.cmp_to_key(lambda x,y: cmp(cint(y[1]), cint(x[1]))))[0][0]
for contact in out:
if contact.is_primary_contact:
return contact.parent
return out[0].parent
else:
return None

View file

@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and Contributors
# See license.txt
from __future__ import unicode_literals
import frappe
import unittest

View file

@ -2,7 +2,6 @@
# Copyright (c) 2019, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document

View file

@ -2,7 +2,6 @@
# Copyright (c) 2019, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document

View file

@ -2,7 +2,6 @@
# Copyright (c) 2017, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
from frappe.model.document import Document
class Gender(Document):

View file

@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and Contributors
# See license.txt
from __future__ import unicode_literals
import unittest
class TestGender(unittest.TestCase):

View file

@ -2,7 +2,6 @@
# Copyright (c) 2017, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
from frappe.model.document import Document
class Salutation(Document):

View file

@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and Contributors
# See license.txt
from __future__ import unicode_literals
import unittest
class TestSalutation(unittest.TestCase):

View file

@ -1,8 +1,5 @@
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
from six import iteritems
import frappe
from frappe import _
@ -58,7 +55,7 @@ def get_reference_addresses_and_contact(reference_doctype, reference_name):
reference_details = get_reference_details(reference_doctype, "Address", reference_list, reference_details)
reference_details = get_reference_details(reference_doctype, "Contact", reference_list, reference_details)
for reference_name, details in iteritems(reference_details):
for reference_name, details in reference_details.items():
addresses = details.get("address", [])
contacts = details.get("contact", [])
if not any([addresses, contacts]):

View file

@ -1,4 +1,4 @@
from __future__ import unicode_literals
import frappe
import frappe.defaults
import unittest

View file

@ -1,4 +1,2 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
from __future__ import unicode_literals

View file

@ -1,4 +1,3 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
from __future__ import unicode_literals

View file

@ -3,8 +3,6 @@
# For license information, please see license.txt
# imports - standard imports
from __future__ import unicode_literals
# imports - module imports
import frappe
from frappe.model.document import Document

View file

@ -2,7 +2,6 @@
# Copyright (c) 2017, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
from frappe import _
from frappe.utils import get_fullname, now
from frappe.model.document import Document

View file

@ -1,13 +1,11 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: See license.txt
from __future__ import unicode_literals
import frappe
import frappe.permissions
from frappe.utils import get_fullname
from frappe import _
from frappe.core.doctype.activity_log.activity_log import add_authentication_log
from six import string_types
def update_feed(doc, method=None):
if frappe.flags.in_patch or frappe.flags.in_install or frappe.flags.in_import:
@ -23,7 +21,7 @@ def update_feed(doc, method=None):
feed = doc.get_feed()
if feed:
if isinstance(feed, string_types):
if isinstance(feed, str):
feed = {"subject": feed}
feed = frappe._dict(feed)

View file

@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and Contributors
# See license.txt
from __future__ import unicode_literals
import frappe
import unittest
import time

View file

@ -2,7 +2,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document

View file

@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals, absolute_import
import frappe
from frappe import _
import json
@ -11,7 +9,7 @@ from frappe.core.doctype.user.user import extract_mentions
from frappe.desk.doctype.notification_log.notification_log import enqueue_create_notification,\
get_title, get_title_html
from frappe.utils import get_fullname
from frappe.website.render import clear_cache
from frappe.website.utils import clear_cache
from frappe.database.schema import add_column
from frappe.exceptions import ImplicitCommitError

View file

@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and Contributors
# See license.txt
from __future__ import unicode_literals
import frappe, json
import unittest

View file

@ -1,29 +1,31 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
from __future__ import unicode_literals, absolute_import
from collections import Counter
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import validate_email_address, strip_html, cstr, time_diff_in_seconds
from frappe.core.doctype.communication.email import validate_email, notify, _notify
from frappe.core.doctype.communication.email import validate_email
from frappe.core.doctype.communication.mixins import CommunicationEmailMixin
from frappe.core.utils import get_parent_doc
from frappe.utils.bot import BotReply
from frappe.utils import parse_addr
from frappe.utils import parse_addr, split_emails
from frappe.core.doctype.comment.comment import update_comment_in_doc
from email.utils import parseaddr
from six.moves.urllib.parse import unquote
from urllib.parse import unquote
from frappe.utils.user import is_system_user
from frappe.contacts.doctype.contact.contact import get_contact_name
from frappe.automation.doctype.assignment_rule.assignment_rule import apply as apply_assignment_rule
exclude_from_linked_with = True
class Communication(Document):
class Communication(Document, CommunicationEmailMixin):
"""Communication represents an external communication like Email.
"""
no_feed_on_delete = True
DOCTYPE = 'Communication'
"""Communication represents an external communication like Email."""
def onload(self):
"""create email flag queue"""
if self.communication_type == "Communication" and self.communication_medium == "Email" \
@ -124,6 +126,45 @@ class Communication(Document):
if self.communication_type == "Communication":
self.notify_change('delete')
@property
def sender_mailid(self):
return parse_addr(self.sender)[1] if self.sender else ""
@staticmethod
def _get_emails_list(emails=None, exclude_displayname = False):
"""Returns list of emails from given email string.
* Removes duplicate mailids
* Removes display name from email address if exclude_displayname is True
"""
emails = split_emails(emails) if isinstance(emails, str) else (emails or [])
if exclude_displayname:
return [email.lower() for email in set([parse_addr(email)[1] for email in emails]) if email]
return [email.lower() for email in set(emails) if email]
def to_list(self, exclude_displayname = True):
"""Returns to list.
"""
return self._get_emails_list(self.recipients, exclude_displayname=exclude_displayname)
def cc_list(self, exclude_displayname = True):
"""Returns cc list.
"""
return self._get_emails_list(self.cc, exclude_displayname=exclude_displayname)
def bcc_list(self, exclude_displayname = True):
"""Returns bcc list.
"""
return self._get_emails_list(self.bcc, exclude_displayname=exclude_displayname)
def get_attachments(self):
attachments = frappe.get_all(
"File",
fields=["name", "file_name", "file_url", "is_private"],
filters = {"attached_to_name": self.name, "attached_to_doctype": self.DOCTYPE}
)
return attachments
def notify_change(self, action):
frappe.publish_realtime('update_docinfo_for_{}_{}'.format(self.reference_doctype, self.reference_name), {
'doc': self.as_dict(),
@ -149,6 +190,23 @@ class Communication(Document):
self.email_status = "Spam"
@classmethod
def find(cls, name, ignore_error=False):
try:
return frappe.get_doc(cls.DOCTYPE, name)
except frappe.DoesNotExistError:
if ignore_error:
return
raise
@classmethod
def find_one_by_filters(cls, *, order_by=None, **kwargs):
name = frappe.db.get_value(cls.DOCTYPE, kwargs, order_by=order_by)
return cls.find(name) if name else None
def update_db(self, **kwargs):
frappe.db.set_value(self.DOCTYPE, self.name, kwargs)
def set_sender_full_name(self):
if not self.sender_full_name and self.sender:
if self.sender == "Administrator":
@ -180,36 +238,6 @@ class Communication(Document):
if not self.sender_full_name:
self.sender_full_name = sender_email
def send(self, print_html=None, print_format=None, attachments=None,
send_me_a_copy=False, recipients=None):
"""Send communication via Email.
:param print_html: Send given value as HTML attachment.
:param print_format: Attach print format of parent document."""
self.send_me_a_copy = send_me_a_copy
self.notify(print_html, print_format, attachments, recipients)
def notify(self, print_html=None, print_format=None, attachments=None,
recipients=None, cc=None, bcc=None,fetched_from_email_account=False):
"""Calls a delayed task 'sendmail' that enqueus email in Email Queue queue
:param print_html: Send given value as HTML attachment
:param print_format: Attach print format of parent document
:param attachments: A list of filenames that should be attached when sending this email
:param recipients: Email recipients
:param cc: Send email as CC to
:param fetched_from_email_account: True when pulling email, the notification shouldn't go to the main recipient
"""
notify(self, print_html, print_format, attachments, recipients, cc, bcc,
fetched_from_email_account)
def _notify(self, print_html=None, print_format=None, attachments=None,
recipients=None, cc=None, bcc=None):
_notify(self, print_html, print_format, attachments, recipients, cc, bcc)
def bot_reply(self):
if self.comment_type == 'Bot' and self.communication_type == 'Chat':
reply = BotReply().get_reply(self.content)
@ -485,4 +513,5 @@ def set_avg_response_time(parent, communication):
response_times.append(response_time)
if response_times:
avg_response_time = sum(response_times) / len(response_times)
parent.db_set("avg_response_time", avg_response_time)
parent.db_set("avg_response_time", avg_response_time)

View file

@ -1,9 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
from __future__ import unicode_literals, absolute_import
from six.moves import range
from six import string_types
import frappe
import json
from email.utils import formataddr
@ -16,6 +13,11 @@ import time
from frappe import _
from frappe.utils.background_jobs import enqueue
OUTGOING_EMAIL_ACCOUNT_MISSING = _("""
Unable to send mail because of a missing email account.
Please setup default Email Account from Setup > Email > Email Account
""")
@frappe.whitelist()
def make(doctype=None, name=None, content=None, subject=None, sent_or_received = "Sent",
sender=None, sender_full_name=None, recipients=None, communication_medium="Email", send_email=False,
@ -39,7 +41,6 @@ def make(doctype=None, name=None, content=None, subject=None, sent_or_received =
:param send_me_a_copy: Send a copy to the sender (default **False**).
:param email_template: Template which is used to compose mail .
"""
is_error_report = (doctype=="User" and name==frappe.session.user and subject=="Error Report")
send_me_a_copy = cint(send_me_a_copy)
@ -77,22 +78,24 @@ def make(doctype=None, name=None, content=None, subject=None, sent_or_received =
comm.save(ignore_permissions=True)
if isinstance(attachments, string_types):
if isinstance(attachments, str):
attachments = json.loads(attachments)
# if not committed, delayed task doesn't find the communication
if attachments:
add_attachments(comm.name, attachments)
frappe.db.commit()
if cint(send_email):
frappe.flags.print_letterhead = cint(print_letterhead)
comm.send(print_html, print_format, attachments, send_me_a_copy=send_me_a_copy)
if not comm.get_outgoing_email_account():
frappe.throw(msg=OUTGOING_EMAIL_ACCOUNT_MISSING, exc=frappe.OutgoingEmailError)
comm.send_email(print_html=print_html, print_format=print_format,
send_me_a_copy=send_me_a_copy, print_letterhead=print_letterhead)
emails_not_sent_to = comm.exclude_emails_list(include_sender=send_me_a_copy)
return {
"name": comm.name,
"emails_not_sent_to": ", ".join(comm.emails_not_sent_to) if hasattr(comm, "emails_not_sent_to") else None
"emails_not_sent_to": ", ".join(emails_not_sent_to or [])
}
def validate_email(doc):
@ -113,164 +116,6 @@ def validate_email(doc):
# validate sender
def notify(doc, print_html=None, print_format=None, attachments=None,
recipients=None, cc=None, bcc=None, fetched_from_email_account=False):
"""Calls a delayed task 'sendmail' that enqueus email in Email Queue queue
:param print_html: Send given value as HTML attachment
:param print_format: Attach print format of parent document
:param attachments: A list of filenames that should be attached when sending this email
:param recipients: Email recipients
:param cc: Send email as CC to
:param bcc: Send email as BCC to
:param fetched_from_email_account: True when pulling email, the notification shouldn't go to the main recipient
"""
recipients, cc, bcc = get_recipients_cc_and_bcc(doc, recipients, cc, bcc,
fetched_from_email_account=fetched_from_email_account)
if not recipients and not cc:
return
doc.emails_not_sent_to = set(doc.all_email_addresses) - set(doc.sent_email_addresses)
if frappe.flags.in_test:
# for test cases, run synchronously
doc._notify(print_html=print_html, print_format=print_format, attachments=attachments,
recipients=recipients, cc=cc, bcc=None)
else:
enqueue(sendmail, queue="default", timeout=300, event="sendmail",
communication_name=doc.name,
print_html=print_html, print_format=print_format, attachments=attachments,
recipients=recipients, cc=cc, bcc=bcc, lang=frappe.local.lang,
session=frappe.local.session, print_letterhead=frappe.flags.print_letterhead)
def _notify(doc, print_html=None, print_format=None, attachments=None,
recipients=None, cc=None, bcc=None):
prepare_to_notify(doc, print_html, print_format, attachments)
if doc.outgoing_email_account.send_unsubscribe_message:
unsubscribe_message = _("Leave this conversation")
else:
unsubscribe_message = ""
frappe.sendmail(
recipients=(recipients or []),
cc=(cc or []),
bcc=(bcc or []),
expose_recipients="header",
sender=doc.sender,
reply_to=doc.incoming_email_account,
subject=doc.subject,
content=doc.content,
reference_doctype=doc.reference_doctype,
reference_name=doc.reference_name,
attachments=doc.attachments,
message_id=doc.message_id,
unsubscribe_message=unsubscribe_message,
delayed=True,
communication=doc.name,
read_receipt=doc.read_receipt,
is_notification=True if doc.sent_or_received =="Received" else False,
print_letterhead=frappe.flags.print_letterhead
)
def get_recipients_cc_and_bcc(doc, recipients, cc, bcc, fetched_from_email_account=False):
doc.all_email_addresses = []
doc.sent_email_addresses = []
doc.previous_email_sender = None
if not recipients:
recipients = get_recipients(doc, fetched_from_email_account=fetched_from_email_account)
if not cc:
cc = get_cc(doc, recipients, fetched_from_email_account=fetched_from_email_account)
if not bcc:
bcc = get_bcc(doc, recipients, fetched_from_email_account=fetched_from_email_account)
if fetched_from_email_account:
# email was already sent to the original recipient by the sender's email service
original_recipients, recipients = recipients, []
# send email to the sender of the previous email in the thread which this email is a reply to
#provides erratic results and can send external
#if doc.previous_email_sender:
# recipients.append(doc.previous_email_sender)
# cc that was received in the email
original_cc = split_emails(doc.cc)
# don't cc to people who already received the mail from sender's email service
cc = list(set(cc) - set(original_cc) - set(original_recipients))
remove_administrator_from_email_list(cc)
original_bcc = split_emails(doc.bcc)
bcc = list(set(bcc) - set(original_bcc) - set(original_recipients))
remove_administrator_from_email_list(bcc)
remove_administrator_from_email_list(recipients)
return recipients, cc, bcc
def remove_administrator_from_email_list(email_list):
administrator_email = list(filter(lambda emails: "Administrator" in emails, email_list))
if administrator_email:
email_list.remove(administrator_email[0])
def prepare_to_notify(doc, print_html=None, print_format=None, attachments=None):
"""Prepare to make multipart MIME Email
:param print_html: Send given value as HTML attachment.
:param print_format: Attach print format of parent document."""
view_link = frappe.utils.cint(frappe.db.get_value("System Settings", "System Settings", "attach_view_link"))
if print_format and view_link:
doc.content += get_attach_link(doc, print_format)
set_incoming_outgoing_accounts(doc)
if not doc.sender:
doc.sender = doc.outgoing_email_account.email_id
if not doc.sender_full_name:
doc.sender_full_name = doc.outgoing_email_account.name or _("Notification")
if doc.sender:
# combine for sending to get the format 'Jane <jane@example.com>'
doc.sender = get_formatted_email(doc.sender_full_name, mail=doc.sender)
doc.attachments = []
if print_html or print_format:
doc.attachments.append({"print_format_attachment":1, "doctype":doc.reference_doctype,
"name":doc.reference_name, "print_format":print_format, "html":print_html})
if attachments:
if isinstance(attachments, string_types):
attachments = json.loads(attachments)
for a in attachments:
if isinstance(a, string_types):
# is it a filename?
try:
# check for both filename and file id
file_id = frappe.db.get_list('File', or_filters={'file_name': a, 'name': a}, limit=1)
if not file_id:
frappe.throw(_("Unable to find attachment {0}").format(a))
file_id = file_id[0]['name']
_file = frappe.get_doc("File", file_id)
_file.get_content()
# these attachments will be attached on-demand
# and won't be stored in the message
doc.attachments.append({"fid": file_id})
except IOError:
frappe.throw(_("Unable to find attachment {0}").format(a))
else:
doc.attachments.append(a)
def set_incoming_outgoing_accounts(doc):
from frappe.email.doctype.email_account.email_account import EmailAccount
incoming_email_account = EmailAccount.find_incoming(
@ -283,82 +128,13 @@ def set_incoming_outgoing_accounts(doc):
if doc.sent_or_received == "Sent":
doc.db_set("email_account", doc.outgoing_email_account.name)
def get_recipients(doc, fetched_from_email_account=False):
"""Build a list of email addresses for To"""
# [EDGE CASE] doc.recipients can be None when an email is sent as BCC
recipients = split_emails(doc.recipients)
#if fetched_from_email_account and doc.in_reply_to:
# add sender of previous reply
#doc.previous_email_sender = frappe.db.get_value("Communication", doc.in_reply_to, "sender")
#recipients.append(doc.previous_email_sender)
if recipients:
recipients = filter_email_list(doc, recipients, [])
return recipients
def get_cc(doc, recipients=None, fetched_from_email_account=False):
"""Build a list of email addresses for CC"""
# get a copy of CC list
cc = split_emails(doc.cc)
if doc.reference_doctype and doc.reference_name:
if fetched_from_email_account:
# if it is a fetched email, add follows to CC
cc.append(get_owner_email(doc))
cc += get_assignees(doc)
if getattr(doc, "send_me_a_copy", False) and doc.sender not in cc:
cc.append(doc.sender)
if cc:
# exclude unfollows, recipients and unsubscribes
exclude = [] #added to remove account check
exclude += [d[0] for d in frappe.db.get_all("User", ["email"], {"thread_notify": 0}, as_list=True)]
exclude += [(parse_addr(email)[1] or "").lower() for email in recipients]
if fetched_from_email_account:
# exclude sender when pulling email
exclude += [parse_addr(doc.sender)[1]]
if doc.reference_doctype and doc.reference_name:
exclude += [d[0] for d in frappe.db.get_all("Email Unsubscribe", ["email"],
{"reference_doctype": doc.reference_doctype, "reference_name": doc.reference_name}, as_list=True)]
cc = filter_email_list(doc, cc, exclude, is_cc=True)
return cc
def get_bcc(doc, recipients=None, fetched_from_email_account=False):
"""Build a list of email addresses for BCC"""
bcc = split_emails(doc.bcc)
if bcc:
exclude = []
exclude += [d[0] for d in frappe.db.get_all("User", ["email"], {"thread_notify": 0}, as_list=True)]
exclude += [(parse_addr(email)[1] or "").lower() for email in recipients]
if fetched_from_email_account:
# exclude sender when pulling email
exclude += [parse_addr(doc.sender)[1]]
if doc.reference_doctype and doc.reference_name:
exclude += [d[0] for d in frappe.db.get_all("Email Unsubscribe", ["email"],
{"reference_doctype": doc.reference_doctype, "reference_name": doc.reference_name}, as_list=True)]
bcc = filter_email_list(doc, bcc, exclude, is_bcc=True)
return bcc
def add_attachments(name, attachments):
'''Add attachments to the given Communication'''
# loop through attachments
for a in attachments:
if isinstance(a, string_types):
if isinstance(a, str):
attach = frappe.db.get_value("File", {"name":a},
["file_name", "file_url", "is_private"], as_dict=1)
# save attachments to new doc
_file = frappe.get_doc({
"doctype": "File",
@ -370,103 +146,6 @@ def add_attachments(name, attachments):
})
_file.save(ignore_permissions=True)
def filter_email_list(doc, email_list, exclude, is_cc=False, is_bcc=False):
# temp variables
filtered = []
email_address_list = []
for email in list(set(email_list)):
email_address = (parse_addr(email)[1] or "").lower()
if not email_address:
continue
# this will be used to eventually find email addresses that aren't sent to
doc.all_email_addresses.append(email_address)
if (email in exclude) or (email_address in exclude):
continue
if is_cc:
is_user_enabled = frappe.db.get_value("User", email_address, "enabled")
if is_user_enabled==0:
# don't send to disabled users
continue
if is_bcc:
is_user_enabled = frappe.db.get_value("User", email_address, "enabled")
if is_user_enabled==0:
continue
# make sure of case-insensitive uniqueness of email address
if email_address not in email_address_list:
# append the full email i.e. "Human <human@example.com>"
filtered.append(email)
email_address_list.append(email_address)
doc.sent_email_addresses.extend(email_address_list)
return filtered
def get_owner_email(doc):
owner = get_parent_doc(doc).owner
return get_formatted_email(owner) or owner
def get_assignees(doc):
return [( get_formatted_email(d.owner) or d.owner ) for d in
frappe.db.get_all("ToDo", filters={
"reference_type": doc.reference_doctype,
"reference_name": doc.reference_name,
"status": "Open"
}, fields=["owner"])
]
def get_attach_link(doc, print_format):
"""Returns public link for the attachment via `templates/emails/print_link.html`."""
return frappe.get_template("templates/emails/print_link.html").render({
"url": get_url(),
"doctype": doc.reference_doctype,
"name": doc.reference_name,
"print_format": print_format,
"key": get_parent_doc(doc).get_signature()
})
def sendmail(communication_name, print_html=None, print_format=None, attachments=None,
recipients=None, cc=None, bcc=None, lang=None, session=None, print_letterhead=None):
try:
if lang:
frappe.local.lang = lang
if session:
# hack to enable access to private files in PDF
session['data'] = frappe._dict(session['data'])
frappe.local.session.update(session)
if print_letterhead:
frappe.flags.print_letterhead = print_letterhead
# upto 3 retries
for i in range(3):
try:
communication = frappe.get_doc("Communication", communication_name)
communication._notify(print_html=print_html, print_format=print_format, attachments=attachments,
recipients=recipients, cc=cc, bcc=bcc)
except frappe.db.InternalError as e:
# deadlock, try again
if frappe.db.is_deadlocked(e):
frappe.db.rollback()
time.sleep(1)
continue
else:
raise
else:
break
except:
traceback = frappe.log_error("frappe.core.doctype.communication.email.sendmail")
raise
@frappe.whitelist(allow_guest=True)
def mark_email_as_seen(name=None):
try:

View file

@ -0,0 +1,297 @@
import frappe
from frappe import _
from frappe.core.utils import get_parent_doc
from frappe.utils import parse_addr, get_formatted_email, get_url
from frappe.email.doctype.email_account.email_account import EmailAccount
class CommunicationEmailMixin:
"""Mixin class to handle communication mails.
"""
def is_email_communication(self):
return self.communication_type=="Communication" and self.communication_medium == "Email"
def get_owner(self):
"""Get owner of the communication docs parent.
"""
parent_doc = get_parent_doc(self)
return parent_doc.owner if parent_doc else None
def get_all_email_addresses(self, exclude_displayname=False):
"""Get all Email addresses mentioned in the doc along with display name.
"""
return self.to_list(exclude_displayname=exclude_displayname) + \
self.cc_list(exclude_displayname=exclude_displayname) + \
self.bcc_list(exclude_displayname=exclude_displayname)
def get_email_with_displayname(self, email_address):
"""Returns email address after adding displayname.
"""
display_name, email = parse_addr(email_address)
if display_name and display_name != email:
return email_address
# emailid to emailid with display name map.
email_map = {parse_addr(email)[1]: email for email in self.get_all_email_addresses()}
return email_map.get(email, email)
def mail_recipients(self, is_inbound_mail_communcation=False):
"""Build to(recipient) list to send an email.
"""
# Incase of inbound mail, recipients already received the mail, no need to send again.
if is_inbound_mail_communcation:
return []
if hasattr(self, '_final_recipients'):
return self._final_recipients
to = self.to_list()
self._final_recipients = list(filter(lambda id: id != 'Administrator', to))
return self._final_recipients
def get_mail_recipients_with_displayname(self, is_inbound_mail_communcation=False):
"""Build to(recipient) list to send an email including displayname in email.
"""
to_list = self.mail_recipients(is_inbound_mail_communcation=is_inbound_mail_communcation)
return [self.get_email_with_displayname(email) for email in to_list]
def mail_cc(self, is_inbound_mail_communcation=False, include_sender = False):
"""Build cc list to send an email.
* if email copy is requested by sender, then add sender to CC.
* If this doc is created through inbound mail, then add doc owner to cc list
* remove all the thread_notify disabled users.
* Make sure that all users enabled in the system
* Remove admin from email list
* FixMe: Removed adding TODO owners to cc list. Check if that is needed.
"""
if hasattr(self, '_final_cc'):
return self._final_cc
cc = self.cc_list()
# Need to inform parent document owner incase communication is created through inbound mail
if include_sender:
cc.append(self.sender_mailid)
if is_inbound_mail_communcation:
cc.append(self.get_owner())
cc = set(cc) - {self.sender_mailid}
cc = set(cc) - set(self.filter_thread_notification_disbled_users(cc))
cc = cc - set(self.mail_recipients(is_inbound_mail_communcation=is_inbound_mail_communcation))
cc = cc - set(self.filter_disabled_users(cc))
# # Incase of inbound mail, to and cc already received the mail, no need to send again.
if is_inbound_mail_communcation:
cc = cc - set(self.cc_list() + self.to_list())
self._final_cc = list(filter(lambda id: id != 'Administrator', cc))
return self._final_cc
def get_mail_cc_with_displayname(self, is_inbound_mail_communcation=False, include_sender = False):
cc_list = self.mail_cc(is_inbound_mail_communcation=False, include_sender = False)
return [self.get_email_with_displayname(email) for email in cc_list]
def mail_bcc(self, is_inbound_mail_communcation=False):
"""
* Thread_notify check
* Email unsubscribe list
* User must be enabled in the system
* remove_administrator_from_email_list
"""
if hasattr(self, '_final_bcc'):
return self._final_bcc
bcc = set(self.bcc_list())
if is_inbound_mail_communcation:
bcc = bcc - {self.sender_mailid}
bcc = bcc - set(self.filter_thread_notification_disbled_users(bcc))
bcc = bcc - set(self.mail_recipients(is_inbound_mail_communcation=is_inbound_mail_communcation))
bcc = bcc - set(self.filter_disabled_users(bcc))
# Incase of inbound mail, to and cc & bcc already received the mail, no need to send again.
if is_inbound_mail_communcation:
bcc = bcc - set(self.bcc_list() + self.to_list())
self._final_bcc = list(filter(lambda id: id != 'Administrator', bcc))
return self._final_bcc
def get_mail_bcc_with_displayname(self, is_inbound_mail_communcation=False):
bcc_list = self.mail_bcc(is_inbound_mail_communcation=is_inbound_mail_communcation)
return [self.get_email_with_displayname(email) for email in bcc_list]
def mail_sender(self):
email_account = self.get_outgoing_email_account()
if not self.sender_mailid and email_account:
return email_account.email_id
return self.sender_mailid
def mail_sender_fullname(self):
email_account = self.get_outgoing_email_account()
if not self.sender_full_name:
return (email_account and email_account.name) or _("Notification")
return self.sender_full_name
def get_mail_sender_with_displayname(self):
return get_formatted_email(self.mail_sender_fullname(), mail=self.mail_sender())
def get_content(self, print_format=None):
if print_format:
return self.content + self.get_attach_link(print_format)
return self.content
def get_attach_link(self, print_format):
"""Returns public link for the attachment via `templates/emails/print_link.html`."""
return frappe.get_template("templates/emails/print_link.html").render({
"url": get_url(),
"doctype": self.reference_doctype,
"name": self.reference_name,
"print_format": print_format,
"key": get_parent_doc(self).get_signature()
})
def get_outgoing_email_account(self):
if not hasattr(self, '_outgoing_email_account'):
if self.email_account:
self._outgoing_email_account = EmailAccount.find(self.email_account)
else:
self._outgoing_email_account = EmailAccount.find_outgoing(
match_by_email=self.sender_mailid,
match_by_doctype=self.reference_doctype
)
if self.sent_or_received == "Sent" and self._outgoing_email_account:
self.db_set("email_account", self._outgoing_email_account.name)
return self._outgoing_email_account
def get_incoming_email_account(self):
if not hasattr(self, '_incoming_email_account'):
self._incoming_email_account = EmailAccount.find_incoming(
match_by_email=self.sender_mailid,
match_by_doctype=self.reference_doctype
)
return self._incoming_email_account
def mail_attachments(self, print_format=None, print_html=None):
final_attachments = []
if print_format and print_html:
d = {'print_format': print_format, 'print_html': print_html, 'print_format_attachment': 1,
'doctype': self.reference_doctype, 'name': self.reference_name}
final_attachments.append(d)
for a in self.get_attachments() or []:
final_attachments.append({"fid": a['name']})
return final_attachments
def get_unsubscribe_message(self):
email_account = self.get_outgoing_email_account()
if email_account and email_account.send_unsubscribe_message:
return _("Leave this conversation")
return ''
def exclude_emails_list(self, is_inbound_mail_communcation=False, include_sender=False):
"""List of mail id's excluded while sending mail.
"""
all_ids = self.get_all_email_addresses(exclude_displayname=True)
final_ids = self.mail_recipients(is_inbound_mail_communcation = is_inbound_mail_communcation) + \
self.mail_bcc(is_inbound_mail_communcation = is_inbound_mail_communcation) + \
self.mail_cc(is_inbound_mail_communcation = is_inbound_mail_communcation, include_sender=include_sender)
return set(all_ids) - set(final_ids)
@staticmethod
def filter_thread_notification_disbled_users(emails):
"""Filter users based on notifications for email threads setting is disabled.
"""
if not emails:
return []
disabled_users = frappe.db.sql_list("""
SELECT
email
FROM
`tabUser`
where
email in %(emails)s
and
thread_notify=0
""", {'emails': tuple(emails)})
return disabled_users
@staticmethod
def filter_disabled_users(emails):
"""
"""
if not emails:
return []
disabled_users = frappe.db.sql_list("""
SELECT
email
FROM
`tabUser`
where
email in %(emails)s
and
enabled=0
""", {'emails': tuple(emails)})
return disabled_users
def sendmail_input_dict(self, print_html=None, print_format=None,
send_me_a_copy=None, print_letterhead=None, is_inbound_mail_communcation=None):
outgoing_email_account = self.get_outgoing_email_account()
if not outgoing_email_account:
return {}
recipients = self.get_mail_recipients_with_displayname(
is_inbound_mail_communcation=is_inbound_mail_communcation
)
cc = self.get_mail_cc_with_displayname(
is_inbound_mail_communcation=is_inbound_mail_communcation,
include_sender = send_me_a_copy
)
bcc = self.get_mail_bcc_with_displayname(
is_inbound_mail_communcation=is_inbound_mail_communcation
)
if not (recipients or cc):
return {}
final_attachments = self.mail_attachments(print_format=print_format, print_html=print_html)
incoming_email_account = self.get_incoming_email_account()
return {
"recipients": recipients,
"cc": cc,
"bcc": bcc,
"expose_recipients": "header",
"sender": self.get_mail_sender_with_displayname(),
"reply_to": incoming_email_account and incoming_email_account.email_id,
"subject": self.subject,
"content": self.get_content(print_format=print_format),
"reference_doctype": self.reference_doctype,
"reference_name": self.reference_name,
"attachments": final_attachments,
"message_id": self.message_id,
"unsubscribe_message": self.get_unsubscribe_message(),
"delayed": True,
"communication": self.name,
"read_receipt": self.read_receipt,
"is_notification": (self.sent_or_received =="Received" and True) or False,
"print_letterhead": print_letterhead
}
def send_email(self, print_html=None, print_format=None,
send_me_a_copy=None, print_letterhead=None, is_inbound_mail_communcation=None):
input_dict = self.sendmail_input_dict(
print_html=print_html,
print_format=print_format,
send_me_a_copy=send_me_a_copy,
print_letterhead=print_letterhead,
is_inbound_mail_communcation=is_inbound_mail_communcation
)
if input_dict:
frappe.sendmail(**input_dict)

View file

@ -1,12 +1,12 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
import unittest
from urllib.parse import quote
import frappe
import unittest
from six.moves.urllib.parse import quote
test_records = frappe.get_test_records('Communication')
from frappe.email.doctype.email_queue.email_queue import EmailQueue
test_records = frappe.get_test_records('Communication')
class TestCommunication(unittest.TestCase):
@ -201,6 +201,70 @@ class TestCommunication(unittest.TestCase):
self.assertIn(("Note", note.name), doc_links)
class TestCommunicationEmailMixin(unittest.TestCase):
def new_communication(self, recipients=None, cc=None, bcc=None):
recipients = ', '.join(recipients or [])
cc = ', '.join(cc or [])
bcc = ', '.join(bcc or [])
comm = frappe.get_doc({
"doctype": "Communication",
"communication_type": "Communication",
"communication_medium": "Email",
"content": "Test content",
"recipients": recipients,
"cc": cc,
"bcc": bcc
}).insert(ignore_permissions=True)
return comm
def new_user(self, email, **user_data):
user_data.setdefault('first_name', 'first_name')
user = frappe.new_doc('User')
user.email = email
user.update(user_data)
user.insert(ignore_permissions=True, ignore_if_duplicate=True)
return user
def test_recipients(self):
to_list = ['to@test.com', 'receiver <to+1@test.com>', 'to@test.com']
comm = self.new_communication(recipients = to_list)
res = comm.get_mail_recipients_with_displayname()
self.assertCountEqual(res, ['to@test.com', 'receiver <to+1@test.com>'])
comm.delete()
def test_cc(self):
to_list = ['to@test.com']
cc_list = ['cc+1@test.com', 'cc <cc+2@test.com>', 'to@test.com']
user = self.new_user(email='cc+1@test.com', thread_notify=0)
comm = self.new_communication(recipients=to_list, cc=cc_list)
res = comm.get_mail_cc_with_displayname()
self.assertCountEqual(res, ['cc <cc+2@test.com>'])
user.delete()
comm.delete()
def test_bcc(self):
bcc_list = ['bcc+1@test.com', 'cc <bcc+2@test.com>', ]
user = self.new_user(email='bcc+2@test.com', enabled=0)
comm = self.new_communication(bcc=bcc_list)
res = comm.get_mail_bcc_with_displayname()
self.assertCountEqual(res, ['bcc+1@test.com'])
user.delete()
comm.delete()
def test_sendmail(self):
to_list = ['to <to@test.com>']
cc_list = ['cc <cc+1@test.com>', 'cc <cc+2@test.com>']
comm = self.new_communication(recipients=to_list, cc=cc_list)
comm.send_email()
doc = EmailQueue.find_one_by_filters(communication=comm.name)
mail_receivers = [each.recipient for each in doc.recipients]
self.assertIsNotNone(doc)
self.assertCountEqual(to_list+cc_list, mail_receivers)
doc.delete()
comm.delete()
def create_email_account():
frappe.delete_doc_if_exists("Email Account", "_Test Comm Account 1")
@ -231,4 +295,4 @@ def create_email_account():
"enable_automatic_linking": 1
}).insert(ignore_permissions=True)
return email_account
return email_account

View file

@ -2,7 +2,6 @@
# Copyright (c) 2019, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document

View file

@ -2,7 +2,6 @@
# Copyright (c) 2015, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document

View file

@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and Contributors
# See license.txt
from __future__ import unicode_literals
import frappe
import unittest

View file

@ -2,7 +2,6 @@
# Copyright (c) 2015, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document

View file

@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and Contributors
# See license.txt
from __future__ import unicode_literals
import frappe
import unittest

View file

@ -2,7 +2,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
from frappe.model.document import Document
class DataExport(Document):

View file

@ -1,8 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
from __future__ import unicode_literals
import frappe
from frappe import _
import frappe.permissions
@ -10,7 +8,6 @@ import re, csv, os
from frappe.utils.csvutils import UnicodeWriter
from frappe.utils import cstr, formatdate, format_datetime, parse_json, cint, format_duration
from frappe.core.doctype.data_import_legacy.importer import get_data_keys
from six import string_types
from frappe.core.doctype.access_log.access_log import make_access_log
reflags = {
@ -57,7 +54,7 @@ class DataExporter:
self.docs_to_export = {}
if self.doctype:
if isinstance(self.doctype, string_types):
if isinstance(self.doctype, str):
self.doctype = [self.doctype]
if len(self.doctype) > 1:

View file

@ -91,7 +91,7 @@ frappe.ui.form.on('Data Import', {
if (frm.doc.status.includes('Success')) {
frm.add_custom_button(
__('Go to {0} List', [frm.doc.reference_doctype]),
__('Go to {0} List', [__(frm.doc.reference_doctype)]),
() => frappe.set_route('List', frm.doc.reference_doctype)
);
}

View file

@ -1,7 +1,6 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
from __future__ import unicode_literals
import os
import io
import frappe
@ -450,7 +449,7 @@ class ImportFile:
for row in data_without_first_row:
row_values = row.get_values(parent_column_indexes)
# if the row is blank, it's a child row doc
if all([v in INVALID_VALUES for v in row_values]):
if all(v in INVALID_VALUES for v in row_values):
rows.append(row)
continue
# if we encounter a row which has values in parent columns,
@ -607,7 +606,7 @@ class Row:
if df.fieldtype == "Select":
select_options = get_select_options(df)
if select_options and value not in select_options:
options_string = ", ".join([frappe.bold(d) for d in select_options])
options_string = ", ".join(frappe.bold(d) for d in select_options)
msg = _("Value must be one of {0}").format(options_string)
self.warnings.append(
{"row": self.row_number, "field": df_as_json(df), "message": msg,}
@ -903,7 +902,7 @@ class Column:
if self.df.fieldtype == "Link":
# find all values that dont exist
values = list(set([cstr(v) for v in self.column_values[1:] if v]))
values = list({cstr(v) for v in self.column_values[1:] if v})
exists = [
d.name for d in frappe.db.get_all(self.df.options, filters={"name": ("in", values)})
]
@ -936,11 +935,11 @@ class Column:
elif self.df.fieldtype == "Select":
options = get_select_options(self.df)
if options:
values = list(set([cstr(v) for v in self.column_values[1:] if v]))
invalid = list(set(values) - set(options))
values = {cstr(v) for v in self.column_values[1:] if v}
invalid = values - set(options)
if invalid:
valid_values = ", ".join([frappe.bold(o) for o in options])
invalid_values = ", ".join([frappe.bold(i) for i in invalid])
valid_values = ", ".join(frappe.bold(o) for o in options)
invalid_values = ", ".join(frappe.bold(i) for i in invalid)
self.warnings.append(
{
"col": self.column_number,

View file

@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and Contributors
# See license.txt
from __future__ import unicode_literals
# import frappe
import unittest

View file

@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and Contributors
# See license.txt
from __future__ import unicode_literals
import unittest
import frappe
from frappe.core.doctype.data_import.exporter import Exporter

View file

@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and Contributors
# See license.txt
from __future__ import unicode_literals
import unittest
import frappe
from frappe.core.doctype.data_import.importer import Importer
@ -64,9 +62,9 @@ class TestImporter(unittest.TestCase):
data_import.reload()
import_log = frappe.parse_json(data_import.import_log)
self.assertEqual(import_log[0]['row_indexes'], [2,3])
expected_error = "Error: <b>Child 1 of DocType for Import</b> Row #1: Value missing for: Child Title"
expected_error = "Error: <strong>Child 1 of DocType for Import</strong> Row #1: Value missing for: Child Title"
self.assertEqual(frappe.parse_json(import_log[0]['messages'][0])['message'], expected_error)
expected_error = "Error: <b>Child 1 of DocType for Import</b> Row #2: Value missing for: Child Title"
expected_error = "Error: <strong>Child 1 of DocType for Import</strong> Row #2: Value missing for: Child Title"
self.assertEqual(frappe.parse_json(import_log[0]['messages'][1])['message'], expected_error)
self.assertEqual(import_log[1]['row_indexes'], [4])

View file

@ -3,9 +3,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
from __future__ import unicode_literals, print_function
from six.moves import range
import requests
import frappe, json
import frappe.permissions
@ -16,7 +13,6 @@ from frappe.utils.csvutils import getlink
from frappe.utils.dateutils import parse_date
from frappe.utils import cint, cstr, flt, getdate, get_datetime, get_url, get_absolute_url, duration_to_seconds
from six import string_types
@frappe.whitelist()
@ -42,7 +38,7 @@ def upload(rows = None, submit_after_import=None, ignore_encoding_errors=False,
frappe.cache().hdel("lang", user)
frappe.set_user_lang(user)
if data_import_doc and isinstance(data_import_doc, string_types):
if data_import_doc and isinstance(data_import_doc, str):
data_import_doc = frappe.get_doc("Data Import Legacy", data_import_doc)
if data_import_doc and from_data_import == "Yes":
no_email = data_import_doc.no_email
@ -152,7 +148,7 @@ def upload(rows = None, submit_after_import=None, ignore_encoding_errors=False,
elif fieldtype in ("Float", "Currency", "Percent"):
d[fieldname] = flt(d[fieldname])
elif fieldtype == "Date":
if d[fieldname] and isinstance(d[fieldname], string_types):
if d[fieldname] and isinstance(d[fieldname], str):
d[fieldname] = getdate(parse_date(d[fieldname]))
elif fieldtype == "Datetime":
if d[fieldname]:
@ -181,7 +177,7 @@ def upload(rows = None, submit_after_import=None, ignore_encoding_errors=False,
if d.get("name") and d["name"].startswith('"'):
d["name"] = d["name"][1:-1]
if sum([0 if not val else 1 for val in d.values()]):
if sum(0 if not val else 1 for val in d.values()):
d['doctype'] = dt
if dt == doctype:
doc.update(d)
@ -537,6 +533,6 @@ def get_parent_field(doctype, parenttype):
def delete_child_rows(rows, doctype):
"""delete child rows for all parents"""
for p in list(set([r[1] for r in rows])):
for p in list(set(r[1] for r in rows)):
if p:
frappe.db.sql("""delete from `tab{0}` where parent=%s""".format(doctype), p)

View file

@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and Contributors
# See license.txt
from __future__ import unicode_literals
# import frappe
import unittest

View file

@ -1,4 +1,3 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
from __future__ import unicode_literals

View file

@ -1,7 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document

View file

@ -2,7 +2,6 @@
# Copyright (c) 2015, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
import json
from frappe.desk.doctype.bulk_update.bulk_update import show_progress

View file

@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and Contributors
# See license.txt
from __future__ import unicode_literals
import frappe
import unittest

View file

@ -1,4 +1,3 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
from __future__ import unicode_literals

View file

@ -1,8 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document

View file

@ -1,4 +1,3 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
from __future__ import unicode_literals

View file

@ -1,7 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document

View file

@ -1,7 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
from frappe import _

View file

@ -1,7 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
import frappe
import frappe.share
import unittest

View file

@ -1,4 +1,3 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
from __future__ import unicode_literals

View file

@ -33,11 +33,11 @@ frappe.ui.form.on('DocType', {
if (!frm.is_new() && !frm.doc.istable) {
if (frm.doc.issingle) {
frm.add_custom_button(__('Go to {0}', [frm.doc.name]), () => {
frm.add_custom_button(__('Go to {0}', [__(frm.doc.name)]), () => {
window.open(`/app/${frappe.router.slug(frm.doc.name)}`);
});
} else {
frm.add_custom_button(__('Go to {0} List', [frm.doc.name]), () => {
frm.add_custom_button(__('Go to {0} List', [__(frm.doc.name)]), () => {
window.open(`/app/${frappe.router.slug(frm.doc.name)}`);
});
}

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