Merge branch 'develop' of https://github.com/frappe/frappe into feat-desktop-cards-reorder

This commit is contained in:
Faris Ansari 2019-07-18 12:13:30 +05:30
commit 4cbd74a48d
482 changed files with 234088 additions and 231176 deletions

View file

@ -58,8 +58,6 @@
"frappe": true,
"Vue": true,
"__": true,
"_p": true,
"_f": true,
"repl": true,
"Class": true,
"locals": true,

13
.mergify.yml Normal file
View file

@ -0,0 +1,13 @@
pull_request_rules:
- name: Automatic merge on CI success and review
conditions:
- status-success=Codacy/PR Quality Review
- status-success=Semantic Pull Request
- status-success=continuous-integration/travis-ci/pr
- status-success=security/snyk - package.json (frappe)
- status-success=security/snyk - requirements.txt (frappe)
- label!=don't-merge
- "#approved-reviews-by>=1"
actions:
merge:
method: merge

14
.snyk
View file

@ -2,8 +2,16 @@
version: v1.13.3
# ignores vulnerabilities until expiry date; change duration by modifying expiry date
ignore:
SNYK-JS-AWESOMPLETE-174474:
- awesomplete:
reason: No patch available
expires: '2019-06-11T14:12:04.995Z'
'npm:mem:20180117':
- showdown > yargs > os-locale > mem:
reason: None given
expires: '2019-04-01T10:08:52.588Z'
patch: {}
reason: No patch available
expires: '2019-06-11T14:12:04.995Z'
# patches apply the minimum changes required to fix a vulnerability
patch:
'npm:extend:20180424':
- superagent > extend:
patched: '2019-05-09T10:14:19.246Z'

View file

@ -1,4 +1,5 @@
{
"baseUrl": "http://test_site_ui:8000",
"projectId": "92odwv"
"projectId": "92odwv",
"adminPassword": "admin"
}

View file

@ -1,17 +1,18 @@
context('Awesome Bar', () => {
before(() => {
cy.visit('/login');
cy.login('Administrator', 'qwe');
cy.login();
cy.visit('/desk');
});
beforeEach(() => {
cy.get('.navbar-home').click();
cy.get('.navbar-header .navbar-home').click();
});
it('navigates to doctype list', () => {
cy.get('#navbar-search')
.type('todo{downarrow}{enter}', { delay: 100 });
cy.get('#navbar-search').type('todo', { delay: 200 });
cy.get('#navbar-search + ul').should('be.visible');
cy.get('#navbar-search').type('{downarrow}{enter}', { delay: 100 });
cy.get('h1').should('contain', 'To Do');
@ -20,7 +21,7 @@ context('Awesome Bar', () => {
it('find text in doctype list', () => {
cy.get('#navbar-search')
.type('test in todo{downarrow}{enter}', { delay: 100 });
.type('test in todo{downarrow}{enter}', { delay: 200 });
cy.get('h1').should('contain', 'To Do');
@ -31,14 +32,14 @@ context('Awesome Bar', () => {
it('navigates to new form', () => {
cy.get('#navbar-search')
.type('new blog post{downarrow}{enter}', { delay: 100 });
.type('new blog post{downarrow}{enter}', { delay: 200 });
cy.get('.title-text:visible').should('have.text', 'New Blog Post 1');
});
it('calculates math expressions', () => {
cy.get('#navbar-search')
.type('55 + 32{downarrow}{enter}', { delay: 100 });
.type('55 + 32{downarrow}{enter}', { delay: 200 });
cy.get('.modal-title').should('contain', 'Result');
cy.get('.msgprint').should('contain', '55 + 32 = 87');

View file

@ -0,0 +1,75 @@
context('Control Link', () => {
beforeEach(() => {
cy.login();
cy.visit('/desk');
cy.create_records({
doctype: 'ToDo',
description: 'this is a test todo for link'
}).as('todos');
});
function get_dialog_with_link() {
return cy.dialog({
title: 'Link',
fields: [
{
'label': 'Select ToDo',
'fieldname': 'link',
'fieldtype': 'Link',
'options': 'ToDo'
}
]
});
}
it('should set the valid value', () => {
get_dialog_with_link().as('dialog');
cy.server();
cy.route('POST', '/api/method/frappe.desk.search.search_link').as('search_link');
cy.get('.frappe-control[data-fieldname=link] input')
.focus()
.type('todo for li')
.type('n', { delay: 600 })
.type('k', { delay: 700 });
cy.wait('@search_link');
cy.get('.frappe-control[data-fieldname=link] ul').should('be.visible');
cy.get('.frappe-control[data-fieldname=link] input').type('{downarrow}{enter}', { delay: 100 });
cy.get('.frappe-control[data-fieldname=link] input').blur();
cy.get('@dialog').then(dialog => {
cy.get('@todos').then(todos => {
let value = dialog.get_value('link');
expect(value).to.eq(todos[0]);
});
});
});
it.only('should unset invalid value', () => {
get_dialog_with_link().as('dialog');
cy.server();
cy.route('GET', '/api/method/frappe.desk.form.utils.validate_link*').as('validate_link');
cy.get('.frappe-control[data-fieldname=link] input')
.type('invalid value', { delay: 100 })
.blur();
cy.wait('@validate_link');
cy.get('.frappe-control[data-fieldname=link] input').should('have.value', '');
});
it('should route to form on arrow click', () => {
get_dialog_with_link().as('dialog');
cy.server();
cy.route('GET', '/api/method/frappe.desk.form.utils.validate_link*').as('validate_link');
cy.get('@todos').then(todos => {
cy.get('.frappe-control[data-fieldname=link] input').type(todos[0]).blur();
cy.wait('@validate_link');
cy.get('.frappe-control[data-fieldname=link] input').focus();
cy.get('.frappe-control[data-fieldname=link] .link-btn').click();
cy.location('hash').should('eq', `#Form/ToDo/${todos[0]}`);
});
});
});

View file

@ -1,14 +1,21 @@
context('Rating Control', () => {
beforeEach(() => {
cy.login('Administrator', 'qwe');
context('Control Rating', () => {
before(() => {
cy.login();
cy.visit('/desk');
});
function get_dialog_with_rating() {
return cy.dialog({
title: 'Rating',
fields: [{
'fieldname': 'rate',
'fieldtype': 'Rating',
}]
});
}
it('click on the star rating to record value', () => {
cy.visit('/desk');
cy.dialog('Rating', [{
'fieldname': 'rate',
'fieldtype': 'Rating',
}]).as('dialog');
get_dialog_with_rating().as('dialog');
cy.get('div.rating')
.children('i.fa')
@ -18,15 +25,13 @@ context('Rating Control', () => {
cy.get('@dialog').then(dialog => {
var value = dialog.get_value('rate');
expect(value).to.equal(1);
dialog.hide();
});
});
it('hover on the star', () => {
cy.visit('/desk');
cy.dialog('Rating', [{
'fieldname': 'rate',
'fieldtype': 'Rating',
}]);
get_dialog_with_rating();
cy.get('div.rating')
.children('i.fa')
.first()

View file

@ -1,6 +1,6 @@
context('FileUploader', () => {
before(() => {
cy.login('Administrator', 'qwe');
cy.login();
cy.visit('/desk');
});

View file

@ -1,6 +1,6 @@
context('Form', () => {
before(() => {
cy.login('Administrator', 'qwe');
cy.login();
cy.visit('/desk');
});

View file

@ -0,0 +1,33 @@
context('List View', () => {
before(() => {
cy.login();
cy.visit('/desk');
cy.window().its('frappe').then(frappe => {
frappe.call("frappe.tests.ui_test_helpers.setup_workflow");
});
cy.clear_cache();
});
it('enables "Actions" button', () => {
const actions = ['Approve', 'Reject', 'Edit', 'Assign To', 'Print','Delete'];
cy.go_to_list('ToDo');
cy.get('.level-item.list-row-checkbox.hidden-xs').click({ multiple: true, force: true });
cy.get('.btn.btn-primary.btn-sm.dropdown-toggle').contains('Actions').should('be.visible').click();
cy.get('.dropdown-menu li:visible').should('have.length', 6).each((el, index) => {
cy.wrap(el).contains(actions[index]);
}).then((elements) => {
cy.server();
cy.route({
method: 'POST',
url:'api/method/frappe.model.workflow.bulk_workflow_approval'
}).as('bulk-approval');
cy.route({
method: 'GET',
url:'api/method/frappe.desk.reportview.get*'
}).as('update-list');
cy.wrap(elements).contains('Approve').click();
cy.wait(['@bulk-approval', '@update-list']);
cy.get('.list-row-container:visible').should('contain', 'Approved');
});
});
});

View file

@ -1,12 +1,12 @@
context('List View Settings', () => {
beforeEach(() => {
cy.login('Administrator', 'qwe');
cy.login();
cy.visit('/desk');
});
it('Default settings', () => {
cy.visit('/desk#List/DocType/List');
cy.get('.list-count').should('contain', "20 of");
cy.get('.sidebar-stat').should('contain', "No Tags");
cy.get('.sidebar-stat').should('contain', "Tags");
});
it('disable count and sidebar stats then verify', () => {
cy.visit('/desk#List/DocType/List');
@ -14,13 +14,13 @@ context('List View Settings', () => {
cy.get('button').contains('Menu').click();
cy.get('.dropdown-menu li').filter(':visible').contains('Settings').click();
cy.get('.modal-dialog').should('contain', 'Settings');
cy.get('input[data-fieldname="disable_count"]').check({force: true});
cy.get('input[data-fieldname="disable_sidebar_stats"]').check({force: true});
cy.get('button').filter(':visible').contains('Save').click();
cy.reload();
cy.get('.list-count').should('be.empty');
cy.get('.list-sidebar .sidebar-stat').should('not.exist');

View file

@ -23,7 +23,7 @@ context('Login', () => {
it('logs in using correct credentials', () => {
cy.get('#login_email').type('Administrator');
cy.get('#login_password').type('qwe');
cy.get('#login_password').type(Cypress.config('adminPassword'));
cy.get('.btn-login').click();
cy.location('pathname').should('eq', '/desk');

View file

@ -1,6 +1,6 @@
context('Form', () => {
before(() => {
cy.login('Administrator', 'qwe');
cy.login();
cy.visit('/desk');
});

View file

@ -1,6 +1,6 @@
context('Recorder', () => {
before(() => {
cy.login('Administrator', 'qwe');
cy.login();
});
it('Navigate to Recorder', () => {

View file

@ -1,13 +1,13 @@
context('Relative Timeframe', () => {
beforeEach(() => {
cy.login('Administrator', 'qwe');
cy.login();
cy.visit('/desk');
});
before(() => {
cy.login('Administrator', 'qwe');
cy.login();
cy.visit('/desk');
cy.window().its('frappe').then(frappe => {
frappe.call("frappe.tests.test_utils.create_todo_records");
frappe.call("frappe.tests.ui_test_helpers.create_todo_records");
});
});
it('set relative filter for Previous and check list', () => {

View file

@ -1,6 +1,6 @@
context('Table MultiSelect', () => {
beforeEach(() => {
cy.login('Administrator', 'qwe');
cy.login();
});
let name = 'table multiselect' + Math.random().toString().slice(2, 8);

View file

@ -25,6 +25,12 @@ import 'cypress-file-upload';
// -- This is will overwrite an existing command --
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... });
Cypress.Commands.add('login', (email, password) => {
if (!email) {
email = 'Administrator';
}
if (!password) {
password = Cypress.config('adminPassword');
}
cy.request({
url: '/api/method/login',
method: 'POST',
@ -35,6 +41,29 @@ Cypress.Commands.add('login', (email, password) => {
});
});
Cypress.Commands.add('call', (method, args) => {
return cy.window().its('frappe.csrf_token').then(csrf_token => {
return cy.request({
url: `/api/method/${method}`,
method: 'POST',
body: args,
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'X-Frappe-CSRF-Token': csrf_token
}
}).then(res => {
expect(res.status).eq(200);
return res.body;
});
});
});
Cypress.Commands.add('create_records', (doc) => {
return cy.call('frappe.tests.ui_test_helpers.create_if_not_exists', { doc })
.then(r => r.message);
});
Cypress.Commands.add('fill_field', (fieldname, value, fieldtype='Data') => {
let selector = `.form-control[data-fieldname="${fieldname}"]`;
@ -66,15 +95,15 @@ Cypress.Commands.add('go_to_list', (doctype) => {
cy.visit(`/desk#List/${doctype}/List`);
});
Cypress.Commands.add('dialog', (title, fields) => {
cy.window().then(win => {
var d = new win.frappe.ui.Dialog({
title: title,
fields: fields,
primary_action: function(){
d.hide();
}
});
Cypress.Commands.add('clear_cache', () => {
cy.window().its('frappe').then(frappe => {
frappe.ui.toolbar.clear_cache();
});
});
Cypress.Commands.add('dialog', (opts) => {
return cy.window().then(win => {
var d = new win.frappe.ui.Dialog(opts);
d.show();
return d;
});

View file

@ -23,7 +23,7 @@ if sys.version[0] == '2':
reload(sys)
sys.setdefaultencoding("utf-8")
__version__ = '11.1.21'
__version__ = '11.1.36'
__title__ = "Frappe Framework"
local = Local()
@ -339,6 +339,10 @@ def msgprint(msg, title=None, raise_exception=0, as_table=False, indicator=None,
out.alert = 1
message_log.append(json.dumps(out))
if raise_exception and hasattr(raise_exception, '__name__'):
local.response['exc_type'] = raise_exception.__name__
_raise_exception()
def clear_messages():

View file

@ -11,7 +11,6 @@ from frappe import _
from six.moves.urllib.parse import urlparse, urlencode
import base64
def handle():
"""
Handler for `/api` methods
@ -181,4 +180,4 @@ def validate_api_key_secret(api_key, api_secret):
user_secret = frappe.utils.password.get_decrypted_password ("User", user, fieldname='api_secret')
if api_secret == user_secret:
frappe.set_user(user)
frappe.local.form_dict = form_dict
frappe.local.form_dict = form_dict

View file

@ -109,7 +109,8 @@ def init_request(request):
raise NotFound
if frappe.local.conf.get('maintenance_mode'):
raise frappe.SessionStopped
frappe.connect()
raise frappe.SessionStopped('Session Stopped')
make_form_dict(request)

View file

@ -8,7 +8,7 @@ from frappe import _
import frappe
import frappe.database
import frappe.utils
from frappe.utils import cint, flt, get_datetime, datetime
from frappe.utils import cint, flt, get_datetime, datetime, date_diff, today
import frappe.utils.user
from frappe import conf
from frappe.sessions import Session, clear_sessions, delete_session
@ -124,6 +124,12 @@ class LoginManager:
frappe.clear_cache(user = frappe.form_dict.get('usr'))
user, pwd = get_cached_user_pass()
self.authenticate(user=user, pwd=pwd)
if self.force_user_to_reset_password():
doc = frappe.get_doc("User", self.user)
frappe.local.response["redirect_to"] = doc.reset_password(send_email=False, password_expired=True)
frappe.local.response["message"] = "Password Reset"
return False
if should_run_2fa(self.user):
authenticate_for_2factor(self.user)
if not confirm_otp_token(self):
@ -209,6 +215,22 @@ class LoginManager:
self.check_if_enabled(user)
self.user = self.check_password(user, pwd)
def force_user_to_reset_password(self):
if not self.user:
return
reset_pwd_after_days = cint(frappe.db.get_single_value("System Settings",
"force_user_to_reset_password"))
if reset_pwd_after_days:
last_password_reset_date = frappe.db.get_value("User",
self.user, "last_password_reset_date") or today()
last_pwd_reset_days = date_diff(today(), last_password_reset_date)
if last_pwd_reset_days > reset_pwd_after_days:
return True
def check_if_enabled(self, user):
"""raise exception if user not enabled"""
doc = frappe.get_doc("System Settings")

View file

@ -40,7 +40,8 @@ class AssignmentRule(Document):
doctype = doc.get('doctype'),
name = doc.get('name'),
description = frappe.render_template(self.description, doc),
assignment_rule = self.name
assignment_rule = self.name,
notify = True
))
# set for reference in round robin

View file

@ -0,0 +1,104 @@
// Copyright (c) 2018, Frappe Technologies and contributors
// For license information, please see license.txt
frappe.provide("frappe.auto_repeat");
frappe.ui.form.on('Auto Repeat', {
setup: function(frm) {
frm.fields_dict['reference_doctype'].get_query = function() {
return {
query: "frappe.automation.doctype.auto_repeat.auto_repeat.get_auto_repeat_doctypes"
};
};
frm.fields_dict['reference_document'].get_query = function() {
return {
filters: {
"auto_repeat": ''
}
};
};
frm.fields_dict['print_format'].get_query = function() {
return {
filters: {
"doc_type": frm.doc.reference_doctype
}
};
};
},
refresh: function(frm) {
// auto repeat message
if (frm.is_new()) {
let customize_form_link = `<a href="#Form/Customize Form">${__('Customize Form')}</a>`;
frm.dashboard.set_headline(__('To configure Auto Repeat, enable "Allow Auto Repeat" from {0}.', [customize_form_link]));
}
// view document button
if (!frm.is_dirty()) {
let label = __('View {0}', [__(frm.doc.reference_doctype)]);
frm.add_custom_button(label, () =>
frappe.set_route("List", frm.doc.reference_doctype, { auto_repeat: frm.doc.name })
);
}
// auto repeat schedule
frappe.auto_repeat.render_schedule(frm);
},
template: function(frm) {
if (frm.doc.template) {
frappe.model.with_doc("Email Template", frm.doc.template, () => {
let email_template = frappe.get_doc("Email Template", frm.doc.template);
frm.set_value("subject", email_template.subject);
frm.set_value("message", email_template.response);
frm.refresh_field("subject");
frm.refresh_field("message");
});
}
},
get_contacts: function(frm) {
frm.call('fetch_linked_contacts');
},
preview_message: function(frm) {
if (frm.doc.message) {
frappe.call({
method: "frappe.automation.doctype.auto_repeat.auto_repeat.generate_message_preview",
args: {
reference_dt: frm.doc.reference_doctype,
reference_doc: frm.doc.reference_document,
subject: frm.doc.subject,
message: frm.doc.message
},
callback: function(r) {
if (r.message) {
frappe.msgprint(r.message.message, r.message.subject)
}
}
});
} else {
frappe.msgprint(__("Please setup a message first"), __("Message not setup"))
}
}
});
frappe.auto_repeat.render_schedule = function(frm) {
if (!frm.is_dirty() && frm.doc.status !== 'Disabled') {
frappe.call({
method: "get_auto_repeat_schedule",
doc: frm.doc
}).done((r) => {
frm.dashboard.wrapper.empty();
frm.dashboard.add_section(
frappe.render_template("auto_repeat_schedule", {
schedule_details : r.message || []
})
);
frm.dashboard.show();
});
} else {
frm.dashboard.hide();
}
};

View file

@ -0,0 +1,239 @@
{
"allow_import": 1,
"allow_rename": 1,
"autoname": "format:AUT-AR-{#####}",
"creation": "2018-03-09 11:22:31.192349",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"section_break_1",
"disabled",
"section_break_3",
"reference_doctype",
"reference_document",
"column_break_5",
"start_date",
"end_date",
"section_break_10",
"frequency",
"repeat_on_day",
"repeat_on_last_day",
"column_break_12",
"next_schedule_date",
"notification",
"notify_by_email",
"recipients",
"get_contacts",
"template",
"subject",
"message",
"preview_message",
"print_format",
"status"
],
"fields": [
{
"fieldname": "section_break_1",
"fieldtype": "Section Break"
},
{
"fieldname": "reference_doctype",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Reference Document Type",
"options": "DocType",
"reqd": 1
},
{
"fieldname": "reference_document",
"fieldtype": "Dynamic Link",
"label": "Reference Document",
"no_copy": 1,
"options": "reference_doctype",
"reqd": 1
},
{
"fieldname": "column_break_5",
"fieldtype": "Column Break"
},
{
"default": "Today",
"fieldname": "start_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Start Date",
"reqd": 1
},
{
"fieldname": "end_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "End Date"
},
{
"default": "0",
"fieldname": "disabled",
"fieldtype": "Check",
"label": "Disabled",
"no_copy": 1
},
{
"fieldname": "section_break_10",
"fieldtype": "Section Break"
},
{
"fieldname": "frequency",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Frequency",
"options": "\nDaily\nWeekly\nMonthly\nQuarterly\nHalf-yearly\nYearly",
"reqd": 1
},
{
"fieldname": "column_break_12",
"fieldtype": "Column Break"
},
{
"depends_on": "eval: in_list([\"Monthly\", \"Quarterly\", \"Half-yearly\", \"Yearly\"], doc.frequency) && !doc.repeat_on_last_day\n",
"fieldname": "repeat_on_day",
"fieldtype": "Int",
"label": "Repeat on Day"
},
{
"fieldname": "next_schedule_date",
"fieldtype": "Date",
"label": "Next Schedule Date",
"no_copy": 1,
"print_hide": 1,
"read_only": 1,
"search_index": 1
},
{
"collapsible": 1,
"fieldname": "notification",
"fieldtype": "Section Break",
"label": "Notification"
},
{
"default": "0",
"fieldname": "notify_by_email",
"fieldtype": "Check",
"label": "Notify by Email"
},
{
"depends_on": "notify_by_email",
"fieldname": "recipients",
"fieldtype": "Small Text",
"label": "Recipients"
},
{
"depends_on": "eval: doc.notify_by_email && doc.reference_doctype && doc.reference_document",
"fieldname": "get_contacts",
"fieldtype": "Button",
"label": "Get Contacts"
},
{
"depends_on": "eval: doc.notify_by_email",
"fieldname": "template",
"fieldtype": "Link",
"label": "Template",
"options": "Email Template"
},
{
"depends_on": "eval: doc.notify_by_email",
"description": "To add dynamic subject, use jinja tags like\n\n<div><pre><code>New {{ doc.doctype }} #{{ doc.name }}</code></pre></div>",
"fieldname": "subject",
"fieldtype": "Data",
"label": "Subject"
},
{
"default": "Please find attached {{ doc.doctype }} #{{ doc.name }}",
"depends_on": "eval: doc.notify_by_email",
"fieldname": "message",
"fieldtype": "Text",
"label": "Message"
},
{
"depends_on": "eval: doc.notify_by_email && doc.reference_doctype && doc.reference_document",
"fieldname": "preview_message",
"fieldtype": "Button",
"label": "Preview Message"
},
{
"depends_on": "notify_by_email",
"fieldname": "print_format",
"fieldtype": "Link",
"label": "Print Format",
"options": "Print Format"
},
{
"fieldname": "status",
"fieldtype": "Select",
"hidden": 1,
"in_list_view": 1,
"label": "Status",
"options": "\nActive\nDisabled\nCompleted",
"read_only": 1
},
{
"fieldname": "section_break_3",
"fieldtype": "Section Break"
},
{
"default": "0",
"depends_on": "eval:doc.frequency === 'Monthly'",
"fieldname": "repeat_on_last_day",
"fieldtype": "Check",
"label": "Repeat on Last Day of the Month"
}
],
"modified": "2019-07-17 11:30:51.412317",
"modified_by": "Administrator",
"module": "Automation",
"name": "Auto Repeat",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts User",
"share": 1,
"write": 1
}
],
"search_fields": "reference_document",
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "reference_document",
"track_changes": 1
}

View file

@ -0,0 +1,374 @@
# -*- coding: utf-8 -*-
# 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 frappe.desk.form import assign_to
from frappe.utils.jinja import validate_template
from dateutil.relativedelta import relativedelta
from frappe.utils.user import get_system_managers
from frappe.utils import cstr, getdate, split_emails, add_days, today, get_last_day, get_first_day
from frappe.model.document import Document
from frappe.core.doctype.communication.email import make
from frappe.utils.background_jobs import get_jobs
month_map = {'Monthly': 1, 'Quarterly': 3, 'Half-yearly': 6, 'Yearly': 12}
class AutoRepeat(Document):
def validate(self):
self.update_status()
self.validate_reference_doctype()
self.validate_dates()
self.validate_email_id()
self.set_dates()
self.update_auto_repeat_id()
self.unlink_if_applicable()
validate_template(self.subject or "")
validate_template(self.message or "")
def before_insert(self):
if not frappe.flags.in_test:
start_date = self.start_date
today_date = today()
if start_date <= today_date:
start_date = today_date
def after_save(self):
frappe.get_doc(self.reference_doctype, self.reference_document).notify_update()
def on_trash(self):
frappe.db.set_value(self.reference_doctype, self.reference_document, {
'auto_repeat': self.name
}, 'auto_repeat', '')
def set_dates(self):
if self.disabled:
self.next_schedule_date = None
else:
self.next_schedule_date = get_next_schedule_date(self.start_date, self.frequency, self.repeat_on_day, self.repeat_on_last_day, self.end_date)
def unlink_if_applicable(self):
if self.status == 'Completed' or self.disabled:
frappe.db.set_value(self.reference_doctype, self.reference_document, 'auto_repeat', '')
def validate_reference_doctype(self):
if not frappe.flags.in_test:
if not frappe.get_meta(self.reference_doctype).allow_auto_repeat:
frappe.throw(_("Enable Allow Auto Repeat for the doctype {0} in Customize Form").format(self.reference_doctype))
def validate_dates(self):
self.validate_from_to_dates('start_date', 'end_date')
if self.end_date == self.start_date:
frappe.throw(_('{0} should not be same as {1}').format(frappe.bold('End Date'), frappe.bold('Start Date')))
def validate_email_id(self):
if self.notify_by_email:
if self.recipients:
email_list = split_emails(self.recipients.replace("\n", ""))
from frappe.utils import validate_email_address
for email in email_list:
if not validate_email_address(email):
frappe.throw(_("{0} is an invalid email address in 'Recipients'").format(email))
else:
frappe.throw(_("'Recipients' not specified"))
def update_auto_repeat_id(self):
#check if document is already on auto repeat
auto_repeat = frappe.db.get_value(self.reference_doctype, self.reference_document, "auto_repeat")
if auto_repeat and auto_repeat != self.name:
frappe.throw(_("The {0} is already on auto repeat {1}").format(self.reference_document, auto_repeat))
else:
frappe.db.set_value(self.reference_doctype, self.reference_document, "auto_repeat", self.name)
def update_status(self):
if self.disabled:
self.status = "Disabled"
elif self.is_completed():
self.status = "Completed"
else:
self.status = "Active"
def is_completed(self):
return self.end_date and getdate(self.end_date) < getdate(today())
def get_auto_repeat_schedule(self):
schedule_details = []
start_date = getdate(self.start_date)
end_date = getdate(self.end_date)
today = frappe.utils.datetime.date.today()
if start_date < today:
start_date = today
if not self.end_date:
start_date = get_next_schedule_date(start_date, self.frequency, self.repeat_on_day, self.repeat_on_last_day)
row = {
"reference_document": self.reference_document,
"frequency": self.frequency,
"next_scheduled_date": start_date
}
schedule_details.append(row)
start_date = get_next_schedule_date(start_date, self.frequency, self.repeat_on_day, self.repeat_on_last_day)
if self.end_date:
start_date = get_next_schedule_date(start_date, self.frequency, self.repeat_on_day, self.repeat_on_last_day)
while (getdate(start_date) < getdate(end_date)):
row = {
"reference_document" : self.reference_document,
"frequency" : self.frequency,
"next_scheduled_date" : start_date
}
schedule_details.append(row)
start_date = get_next_schedule_date(start_date, self.frequency, self.repeat_on_day, self.repeat_on_last_day, end_date)
return schedule_details
def create_documents(self):
try:
new_doc = self.make_new_document()
if self.notify_by_email and self.recipients:
self.send_notification(new_doc)
except Exception:
error_log = frappe.log_error(frappe.get_traceback(), _("Auto Repeat Document Creation Failure"))
self.disable_auto_repeat()
if self.reference_document and not frappe.flags.in_test:
self.notify_error_to_user(error_log)
def make_new_document(self):
reference_doc = frappe.get_doc(self.reference_doctype, self.reference_document)
new_doc = frappe.copy_doc(reference_doc, ignore_no_copy = False)
self.update_doc(new_doc, reference_doc)
new_doc.insert(ignore_permissions = True)
return new_doc
def update_doc(self, new_doc, reference_doc):
new_doc.docstatus = 0
if new_doc.meta.get_field('set_posting_time'):
new_doc.set('set_posting_time', 1)
if new_doc.meta.get_field('auto_repeat'):
new_doc.set('auto_repeat', self.name)
for fieldname in ['naming_series', 'ignore_pricing_rule', 'posting_time', 'select_print_heading', 'remarks', 'owner']:
if new_doc.meta.get_field(fieldname):
new_doc.set(fieldname, reference_doc.get(fieldname))
for data in new_doc.meta.fields:
if data.fieldtype == 'Date' and data.reqd:
new_doc.set(data.fieldname, self.next_schedule_date)
self.set_auto_repeat_period(new_doc)
auto_repeat_doc = frappe.get_doc('Auto Repeat', self.name)
#for any action that needs to take place after the recurring document creation
#on recurring method of that doctype is triggered
new_doc.run_method('on_recurring', reference_doc = reference_doc, auto_repeat_doc = auto_repeat_doc)
def set_auto_repeat_period(self, new_doc):
mcount = month_map.get(self.frequency)
if mcount and new_doc.meta.get_field('from_date') and new_doc.meta.get_field('to_date'):
last_ref_doc = frappe.db.get_all(doctype = self.reference_doctype,
fields = ['name', 'from_date', 'to_date'],
filters = [
['auto_repeat', '=', self.name],
['docstatus', '<', 2],
],
order_by = 'creation desc',
limit = 1)
if not last_ref_doc:
return
from_date = get_next_date(last_ref_doc[0].from_date, mcount)
if (cstr(get_first_day(last_ref_doc[0].from_date)) == cstr(last_ref_doc[0].from_date)) and \
(cstr(get_last_day(last_ref_doc[0].to_date)) == cstr(last_ref_doc[0].to_date)):
to_date = get_last_day(get_next_date(last_ref_doc[0].to_date, mcount))
else:
to_date = get_next_date(last_ref_doc[0].to_date, mcount)
new_doc.set('from_date', from_date)
new_doc.set('to_date', to_date)
def send_notification(self, new_doc):
"""Notify concerned people about recurring document generation"""
subject = self.subject or ''
message = self.message or ''
if not self.subject:
subject = _("New {0}: {1}").format(new_doc.doctype, new_doc.name)
elif "{" in self.subject:
subject = frappe.render_template(self.subject, {'doc': new_doc})
if not self.message:
message = _("Please find attached {0}: {1}").format(new_doc.doctype, new_doc.name)
elif "{" in self.message:
message = frappe.render_template(self.message, {'doc': new_doc})
print_format = self.print_format or 'Standard'
attachments = [frappe.attach_print(new_doc.doctype, new_doc.name,
file_name=new_doc.name, print_format=print_format)]
make(doctype=new_doc.doctype, name=new_doc.name, recipients=self.recipients,
subject=subject, content=message, attachments=attachments, send_email=1)
def fetch_linked_contacts(self):
if self.reference_doctype and self.reference_document:
res = frappe.db.get_all('Contact',
fields=['email_id'],
filters=[
['Dynamic Link', 'link_doctype', '=', self.reference_doctype],
['Dynamic Link', 'link_name', '=', self.reference_document]
])
email_ids = list(set([d.email_id for d in res]))
if not email_ids:
frappe.msgprint(_('No contacts linked to document'), alert=True)
else:
self.recipients = ', '.join(email_ids)
def disable_auto_repeat(self):
frappe.db.set_value('Auto Repeat', self.name, 'disabled', 1)
def notify_error_to_user(self, error_log):
recipients = get_system_managers(only_name=True) + self.owner
subject = _("Auto Repeat Document Creation Failed")
form_link = frappe.utils.get_link_to_form(self.reference_doctype, self.reference_document)
auto_repeat_failed_for = _('Auto Repeat failed for {0}').format(form_link)
error_log_link =frappe.utils.get_link_to_form(error_log.reference_doctype, error_log.reference_document)
error_log_message = _('Check the Error Log for more information: {0}').format(error_log_link)
frappe.sendmail(
recipients=recipients,
subject=subject,
template="auto_repeat_fail",
args={
'auto_repeat_failed_for': auto_repeat_failed_for,
'error_log_message': error_log_message
},
header=[subject, 'red']
)
def get_next_schedule_date(start_date, frequency, repeat_on_day, repeat_on_last_day = False, end_date = None):
month_count = month_map.get(frequency)
if month_count and repeat_on_last_day:
next_date = get_next_date(start_date, month_count, 31)
elif month_count and repeat_on_day:
next_date = get_next_date(start_date, month_count, repeat_on_day)
elif month_count:
next_date = get_next_date(start_date, month_count)
else:
days = 7 if frequency == 'Weekly' else 1
next_date = add_days(start_date, days)
return next_date
def get_next_date(dt, mcount, day=None):
dt = getdate(dt)
dt += relativedelta(months=mcount, day=day)
return dt
#called through hooks
def make_auto_repeat_entry():
enqueued_method = 'frappe.automation.doctype.auto_repeat.auto_repeat.create_repeated_entries'
jobs = get_jobs()
if not jobs or enqueued_method not in jobs[frappe.local.site]:
date = getdate(today())
data = get_auto_repeat_entries(date)
frappe.enqueue(enqueued_method, data=data)
def create_repeated_entries(data):
for d in data:
doc = frappe.get_doc('Auto Repeat', d.name)
current_date = getdate(today())
schedule_date = getdate(doc.next_schedule_date)
while schedule_date <= current_date and not doc.disabled:
doc.create_documents()
schedule_date = get_next_schedule_date(schedule_date, doc.frequency, doc.repeat_on_day, doc.repeat_on_last_day, doc.end_date)
if schedule_date and not doc.disabled:
frappe.db.set_value('Auto Repeat', doc.name, 'next_schedule_date', schedule_date)
def get_auto_repeat_entries(date=None):
if not date:
date = getdate(today())
return frappe.db.get_all('Auto Repeat', filters=[
['next_schedule_date', '<=', date],
['status', '=', 'Active']
])
#called through hooks
def set_auto_repeat_as_completed():
auto_repeat = frappe.get_all("Auto Repeat", filters = {'status': ['!=', 'Disabled']})
for entry in auto_repeat:
doc = frappe.get_doc("Auto Repeat", entry.name)
if doc.is_completed():
doc.status = 'Completed'
doc.save()
@frappe.whitelist()
def make_auto_repeat(doctype, docname, frequency, start_date, end_date = None):
doc = frappe.new_doc('Auto Repeat')
doc.reference_doctype = doctype
doc.reference_document = docname
doc.frequency = frequency
doc.start_date = start_date
if end_date:
doc.end_date = end_date
doc.save()
return doc
#method for reference_doctype filter
def get_auto_repeat_doctypes(doctype, txt, searchfield, start, page_len, filters):
res = frappe.db.get_all('Property Setter', {
'property': 'allow_auto_repeat',
'value': '1',
}, ['doc_type'])
docs = [r.doc_type for r in res]
res = frappe.db.get_all('DocType', {
'allow_auto_repeat': 1,
}, ['name'])
docs += [r.name for r in res]
docs = set(list(docs))
return [[d] for d in docs]
@frappe.whitelist()
def update_reference(docname, reference):
result = ""
try:
frappe.db.set_value("Auto Repeat", docname, "reference_document", reference)
result = "success"
except Exception as e:
result = "error"
raise e
return result
@frappe.whitelist()
def generate_message_preview(reference_dt, reference_doc, message=None, subject=None):
doc = frappe.get_doc(reference_dt, reference_doc)
subject_preview = _("Please add a subject to your email")
msg_preview = frappe.render_template(message, {'doc': doc})
if subject:
subject_preview = frappe.render_template(subject, {'doc': doc})
return {'message': msg_preview, 'subject': subject_preview}

View file

@ -0,0 +1,11 @@
frappe.listview_settings['Auto Repeat'] = {
add_fields: ["next_schedule_date"],
get_indicator: function(doc) {
var colors = {
"Active": "green",
"Disabled": "red",
"Completed": "blue",
};
return [__(doc.status), colors[doc.status], "status,=," + doc.status];
}
};

View file

@ -11,10 +11,9 @@
{% for(var i=0; i < schedule_details.length; i++) { %}
<tr>
<td>{{ schedule_details[i].reference_document }}</td>
<td> {{ schedule_details[i].frequency }} </td>
<td> {{ schedule_details[i].next_scheduled_date }} </td>
<td> {{ __(schedule_details[i].frequency) }} </td>
<td> {{ frappe.datetime.str_to_user(schedule_details[i].next_scheduled_date) }} </td>
</tr>
{% } %}
</tbody>
</table>

View file

@ -7,20 +7,19 @@ import unittest
import frappe
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
from frappe.desk.doctype.auto_repeat.auto_repeat import get_auto_repeat_entries, create_repeated_entries, disable_auto_repeat
from frappe.utils import today, add_days, getdate
from frappe.automation.doctype.auto_repeat.auto_repeat import get_auto_repeat_entries, create_repeated_entries
from frappe.utils import today, add_days, getdate, add_months
def add_custom_fields():
df = dict(
fieldname='auto_repeat', label='Auto Repeat', fieldtype='Link', insert_after='sender',
options='Auto Repeat')
options='Auto Repeat', hidden=1, print_hide=1, read_only=1)
create_custom_field('ToDo', df)
class TestAutoRepeat(unittest.TestCase):
def setUp(self):
if not frappe.db.sql("SELECT `name` FROM `tabCustom Field` WHERE `name`='auto_repeat'"):
if not frappe.db.sql("SELECT `fieldname` FROM `tabCustom Field` WHERE `fieldname`='auto_repeat' and `dt`=%s", "Todo"):
add_custom_fields()
def test_daily_auto_repeat(self):
@ -29,8 +28,8 @@ class TestAutoRepeat(unittest.TestCase):
doc = make_auto_repeat(reference_document=todo.name)
self.assertEqual(doc.next_schedule_date, today())
for data in get_auto_repeat_entries(today()):
create_repeated_entries(data)
data = get_auto_repeat_entries(getdate(today()))
create_repeated_entries(data)
frappe.db.commit()
todo = frappe.get_doc(doc.reference_doctype, doc.reference_document)
@ -44,15 +43,18 @@ class TestAutoRepeat(unittest.TestCase):
self.assertEqual(todo.get('description'), new_todo.get('description'))
def test_monthly_auto_repeat(self):
start_date = '2018-01-01'
end_date = '2018-12-31'
start_date = today()
end_date = add_months(start_date, 12)
todo = frappe.get_doc(
dict(doctype='ToDo', description='test recurring todo', assigned_by='Administrator')).insert()
self.monthly_auto_repeat('ToDo', todo.name, start_date, end_date)
#test without end_date
todo = frappe.get_doc(dict(doctype='ToDo', description='test recurring todo without end_date', assigned_by='Administrator')).insert()
self.monthly_auto_repeat('ToDo', todo.name, start_date)
def monthly_auto_repeat(self, doctype, docname, start_date, end_date):
def monthly_auto_repeat(self, doctype, docname, start_date, end_date = None):
def get_months(start, end):
diff = (12 * end.year + end.month) - (12 * start.year + start.month)
return diff + 1
@ -61,10 +63,10 @@ class TestAutoRepeat(unittest.TestCase):
reference_doctype=doctype, frequency='Monthly', reference_document=docname, start_date=start_date,
end_date=end_date)
disable_auto_repeat(doc)
doc.disable_auto_repeat()
for data in get_auto_repeat_entries(today()):
create_repeated_entries(data)
data = get_auto_repeat_entries(getdate(today()))
create_repeated_entries(data)
docnames = frappe.get_all(doc.reference_doctype, {'auto_repeat': doc.name})
self.assertEqual(len(docnames), 1)
@ -72,8 +74,8 @@ class TestAutoRepeat(unittest.TestCase):
doc.db_set('disabled', 0)
months = get_months(getdate(start_date), getdate(today()))
for data in get_auto_repeat_entries(today()):
create_repeated_entries(data)
data = get_auto_repeat_entries(getdate(today()))
create_repeated_entries(data)
docnames = frappe.get_all(doc.reference_doctype, {'auto_repeat': doc.name})
self.assertEqual(len(docnames), months)
@ -84,8 +86,8 @@ class TestAutoRepeat(unittest.TestCase):
doc = make_auto_repeat(reference_document=todo.name, notify=1, recipients="test@domain.com", subject="New ToDo",
message="A new ToDo has just been created for you")
for data in get_auto_repeat_entries(today()):
create_repeated_entries(data)
data = get_auto_repeat_entries(getdate(today()))
create_repeated_entries(data)
frappe.db.commit()
new_todo = frappe.db.get_value('ToDo',
@ -100,18 +102,14 @@ def make_auto_repeat(**args):
doc = frappe.get_doc({
'doctype': 'Auto Repeat',
'reference_doctype': args.reference_doctype or 'ToDo',
'reference_document': args.reference_document or frappe.db.get_value('ToDo', {'docstatus': 1}, 'name'),
'reference_document': args.reference_document or frappe.db.get_value('ToDo', 'name'),
'frequency': args.frequency or 'Daily',
'start_date': args.start_date or add_days(today(), -1),
'end_date': args.end_date or add_days(today(), 1),
'submit_on_creation': args.submit_on_creation or 0,
'end_date': args.end_date or "",
'notify_by_email': args.notify or 0,
'recipients': args.recipients or "",
'subject': args.subject or "",
'message': args.message or ""
}).insert(ignore_permissions=True)
if not args.do_not_submit:
doc.submit()
return doc

View file

@ -72,7 +72,7 @@ def get_bootinfo():
bootinfo.lang = text_type(bootinfo.lang)
bootinfo.versions = {k: v['version'] for k, v in get_versions().items()}
bootinfo.error_report_email = frappe.get_hooks("error_report_email")
bootinfo.error_report_email = frappe.conf.error_report_email
bootinfo.calendars = sorted(frappe.get_hooks("calendars"))
bootinfo.treeviews = frappe.get_hooks("treeviews") or []
bootinfo.lang_dict = get_lang_dict()

View file

@ -361,6 +361,10 @@ def attach_file(filename=None, filedata=None, doctype=None, docname=None, folder
return _file.as_dict()
@frappe.whitelist()
def get_hooks(hook, app_name=None):
return frappe.get_hooks(hook, app_name)
def check_parent_permission(parent, child_doctype):
if parent:
# User may pass fake parent and get the information from the child table

View file

@ -72,12 +72,11 @@ def call_command(cmd, context):
def get_commands():
# prevent circular imports
from .docs import commands as doc_commands
from .scheduler import commands as scheduler_commands
from .site import commands as site_commands
from .translate import commands as translate_commands
from .utils import commands as utils_commands
return list(set(doc_commands + scheduler_commands + site_commands + translate_commands + utils_commands))
return list(set(scheduler_commands + site_commands + translate_commands + utils_commands))
commands = get_commands()

View file

@ -1,57 +0,0 @@
from __future__ import unicode_literals, absolute_import
import click
import os, shutil
import frappe
from frappe.commands import pass_context
@click.command('build-docs')
@pass_context
@click.argument('app')
@click.option('--docs-version', default='current')
@click.option('--target', default=None)
@click.option('--local', default=False, is_flag=True, help='Run app locally')
@click.option('--watch', default=False, is_flag=True, help='Watch for changes and rewrite')
def build_docs(context, app, docs_version="current", target=None, local=False, watch=False):
"Setup docs in target folder of target app"
from frappe.utils import watch as start_watch
from frappe.utils.setup_docs import add_breadcrumbs_tag
for site in context.sites:
_build_docs_once(site, app, docs_version, target, local)
if watch:
def trigger_make(source_path, event_type):
if "/docs/user/" in source_path:
# user file
target_path = frappe.get_app_path(target, 'www', 'docs', 'user',
os.path.relpath(source_path, start=frappe.get_app_path(app, 'docs', 'user')))
shutil.copy(source_path, target_path)
add_breadcrumbs_tag(target_path)
if source_path.endswith('/docs/index.md'):
target_path = frappe.get_app_path(target, 'www', 'docs', 'index.md')
shutil.copy(source_path, target_path)
apps_path = frappe.get_app_path(app)
start_watch(apps_path, handler=trigger_make)
def _build_docs_once(site, app, docs_version, target, local, only_content_updated=False):
from frappe.utils.setup_docs import setup_docs
try:
frappe.init(site=site)
frappe.connect()
make = setup_docs(app, target)
if not only_content_updated:
make.build(docs_version)
#make.make_docs(target, local)
finally:
frappe.destroy()
commands = [
build_docs,
]

View file

@ -157,16 +157,17 @@ def _reinstall(site, admin_password=None, mariadb_root_username=None, mariadb_ro
admin_password=admin_password)
@click.command('install-app')
@click.argument('app')
@click.argument('apps', nargs=-1)
@pass_context
def install_app(context, app):
"Install a new app to site"
def install_app(context, apps):
"Install a new app to site, supports multiple apps"
from frappe.installer import install_app as _install_app
for site in context.sites:
frappe.init(site=site)
frappe.connect()
try:
_install_app(app, verbose=context.verbose)
for app in apps:
_install_app(app, verbose=context.verbose)
finally:
frappe.destroy()
@ -229,6 +230,7 @@ def migrate(context, rebuild_website=False):
finally:
frappe.destroy()
print("Compiling Python Files...")
compileall.compile_dir('../apps', quiet=1, rx=re.compile('.*node_modules.*'))
@click.command('run-patch')
@ -562,10 +564,7 @@ def browse(context, site):
site = site.lower()
if site in frappe.utils.get_sites():
webbrowser.open('http://{site}:{port}'.format(
site=site,
port=frappe.get_conf(site).webserver_port
), new=2)
webbrowser.open(frappe.utils.get_site_url(site), new=2)
else:
click.echo("\nSite named \033[1m{}\033[0m doesn't exist\n".format(site))

View file

@ -441,7 +441,7 @@ def run_tests(context, app=None, module=None, doctype=None, test=(),
if coverage:
# Generate coverage report only for app that is being tested
source_path = os.path.join(get_bench_path(), 'apps', app or 'frappe')
cov = Coverage(source=[source_path], omit=['*.html', '*.js', '*.css'])
cov = Coverage(source=[source_path], omit=['*.html', '*.js', '*.xml', '*.css', '*/doctype/*/*_dashboard.py', '*/patches/*'])
cov.start()
ret = frappe.test_runner.main(app, module, doctype, context.verbose, tests=tests,
@ -459,26 +459,26 @@ def run_tests(context, app=None, module=None, doctype=None, test=(),
sys.exit(ret)
@click.command('run-ui-tests')
@click.option('--app', help="App to run tests on, leave blank for all apps")
@click.option('--test', help="Path to the specific test you want to run")
@click.option('--test-list', help="Path to the txt file with the list of test cases")
@click.option('--profile', is_flag=True, default=False)
@click.argument('app')
@click.option('--headless', is_flag=True, help="Run UI Test in headless mode")
@pass_context
def run_ui_tests(context, app=None, test=False, test_list=False, profile=False):
def run_ui_tests(context, app, headless=False):
"Run UI tests"
import frappe.test_runner
site = get_site(context)
frappe.init(site=site)
frappe.connect()
app_base_path = os.path.abspath(os.path.join(frappe.get_app_path(app), '..'))
site_url = frappe.utils.get_site_url(site)
admin_password = frappe.get_conf(site).admin_password
ret = frappe.test_runner.run_ui_tests(app=app, test=test, test_list=test_list, verbose=context.verbose,
profile=profile)
if len(ret.failures) == 0 and len(ret.errors) == 0:
ret = 0
# override baseUrl using env variable
site_env = 'CYPRESS_baseUrl={}'.format(site_url)
password_env = 'CYPRESS_adminPassword={}'.format(admin_password) if admin_password else ''
if os.environ.get('CI'):
sys.exit(ret)
# run for headless mode
run_or_open = 'run' if headless else 'open'
command = '{site_env} {password_env} yarn run cypress {run_or_open}'
formatted_command = command.format(site_env=site_env, password_env=password_env, run_or_open=run_or_open)
frappe.commands.popen(formatted_command, cwd=app_base_path)
@click.command('run-setup-wizard-ui-test')
@click.option('--app', help="App to run tests on, leave blank for all apps")

View file

@ -89,8 +89,8 @@ def get_data():
"items": [
{
"type": "doctype",
"name": "Google Maps Settings",
"description": _("Google Maps integration"),
"name": "Google Settings",
"description": _("Google API Settings."),
},
{
"type": "doctype",
@ -111,6 +111,11 @@ def get_data():
"type": "doctype",
"name": "GSuite Templates",
"description": _("Google GSuite Templates to integration with DocTypes"),
},
{
"type": "doctype",
"name": "Google Contacts",
"description": _("Google Contacts Integration."),
}
]
}

View file

@ -46,8 +46,8 @@ def get_data():
},
{
"type": "doctype",
"name": "Blog Settings",
"description": _("Write titles and introductions to your blog."),
"name": "Blogger",
"description": _("A user who posts blogs."),
},
{
"type": "doctype",

File diff suppressed because it is too large Load diff

View file

@ -161,3 +161,16 @@ def contact_query(doctype, txt, searchfield, start, page_len, filters):
'link_name': link_name,
'link_doctype': link_doctype
})
def get_contact_with_phone_number(number):
if not number: return
contacts = frappe.get_all('Contact', or_filters={
'phone': ['like', '%{}'.format(number)],
'mobile_no': ['like', '%{}'.format(number)]
}, limit=1)
contact = contacts[0].name if contacts else None
return contact

View file

@ -4,7 +4,7 @@
from __future__ import unicode_literals
from six import iteritems
import frappe
from frappe import _
field_map = {
"Contact": [ "first_name", "last_name", "phone", "mobile_no", "email_id", "is_primary_contact" ],
@ -94,6 +94,9 @@ def get_reference_details(reference_doctype, doctype, reference_list, reference_
for d in records:
temp_records.append(d[1:])
if not reference_list:
frappe.throw(_("No records present in {0}".format(reference_doctype)))
reference_details[reference_list[0]][frappe.scrub(doctype)] = temp_records
return reference_details

View file

@ -6,7 +6,7 @@ from __future__ import unicode_literals
from frappe import _
from frappe.utils import get_fullname, now
from frappe.model.document import Document
from frappe.core.utils import get_parent_doc, set_timeline_doc
from frappe.core.utils import set_timeline_doc
import frappe
class ActivityLog(Document):

View file

@ -67,7 +67,7 @@ def get_feed_match_conditions(user=None, doctype='Comment'):
user_permissions = frappe.permissions.get_user_permissions(user)
can_read = frappe.get_user().get_can_read()
can_read_doctypes = ["'{}'".format(doctype) for doctype in
can_read_doctypes = ["'{}'".format(dt) for dt in
list(set(can_read) - set(list(user_permissions)))]
if can_read_doctypes:

View file

@ -48,10 +48,10 @@ class TestComment(unittest.TestCase):
add_comment('pleez vizits my site http://mysite.com', 'test@test.com', 'bad commentor',
'Blog Post', test_blog.name, test_blog.route)
self.assertEqual(frappe.get_all('Comment', fields = ['*'], filters = dict(
self.assertEqual(len(frappe.get_all('Comment', fields = ['*'], filters = dict(
reference_doctype = test_blog.doctype,
reference_name = test_blog.name
))[0].published, 0)
))), 0)

View file

@ -1,7 +1,7 @@
{
"allow_import": 1,
"creation": "2013-01-29 10:47:14",
"description": "Keep a track of all communications",
"description": "Keeps track of all communications",
"doctype": "DocType",
"document_type": "Setup",
"engine": "InnoDB",
@ -41,14 +41,11 @@
"user",
"column_break_27",
"email_template",
"link_doctype",
"link_name",
"timeline_doctype",
"timeline_name",
"timeline_label",
"unread_notification_sent",
"seen",
"_user_tags",
"timeline_links_sections",
"timeline_links",
"email_inbox",
"message_id",
"uid",
@ -204,6 +201,7 @@
"label": "Date"
},
{
"default": "0",
"fieldname": "read_receipt",
"fieldtype": "Check",
"label": "Sent Read Receipt",
@ -220,6 +218,7 @@
"read_only": 1
},
{
"default": "0",
"fieldname": "read_by_recipient",
"fieldtype": "Check",
"label": "Read by Recipient",
@ -284,39 +283,6 @@
"fieldname": "column_break_27",
"fieldtype": "Column Break"
},
{
"fieldname": "link_doctype",
"fieldtype": "Link",
"label": "Link DocType",
"options": "DocType",
"read_only": 1
},
{
"fieldname": "link_name",
"fieldtype": "Dynamic Link",
"label": "Link Name",
"options": "link_doctype",
"read_only": 1
},
{
"fieldname": "timeline_doctype",
"fieldtype": "Link",
"label": "Timeline DocType",
"options": "DocType",
"read_only": 1
},
{
"fieldname": "timeline_name",
"fieldtype": "Dynamic Link",
"label": "Timeline Name",
"options": "timeline_doctype",
"read_only": 1
},
{
"fieldname": "timeline_label",
"fieldtype": "Data",
"label": "Timeline field Name"
},
{
"default": "0",
"fieldname": "unread_notification_sent",
@ -325,6 +291,7 @@
"read_only": 1
},
{
"default": "0",
"fieldname": "seen",
"fieldtype": "Check",
"label": "Seen",
@ -368,6 +335,7 @@
"options": "Open\nSpam\nTrash"
},
{
"default": "0",
"fieldname": "has_attachment",
"fieldtype": "Check",
"hidden": 1,
@ -398,11 +366,24 @@
"label": "Email Template",
"options": "Email Template",
"read_only": 1
},
{
"collapsible": 1,
"fieldname": "timeline_links_sections",
"fieldtype": "Section Break",
"label": "Timeline Links"
},
{
"fieldname": "timeline_links",
"fieldtype": "Table",
"label": "Timeline Links",
"options": "Communication Link",
"permlevel": 2
}
],
"icon": "fa fa-comment",
"idx": 1,
"modified": "2019-05-04 15:36:35.818714",
"modified": "2019-05-21 09:48:24.892143",
"modified_by": "Administrator",
"module": "Core",
"name": "Communication",
@ -428,6 +409,18 @@
"role": "System Manager",
"share": 1
},
{
"delete": 1,
"email": 1,
"export": 1,
"permlevel": 2,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"delete": 1,
"email": 1,
@ -437,6 +430,7 @@
}
],
"search_fields": "subject",
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "subject",
"track_changes": 1,

View file

@ -8,11 +8,12 @@ from frappe.model.document import Document
from frappe.utils import validate_email_address, get_fullname, strip_html, cstr
from frappe.core.doctype.communication.email import (validate_email,
notify, _notify, update_parent_mins_to_first_response)
from frappe.core.utils import get_parent_doc, set_timeline_doc
from frappe.core.utils import get_parent_doc
from frappe.utils.bot import BotReply
from frappe.utils import parse_addr
from frappe.core.doctype.comment.comment import update_comment_in_doc
from email.utils import parseaddr
from six.moves.urllib.parse import unquote
from collections import Counter
exclude_from_linked_with = True
@ -55,10 +56,15 @@ class Communication(Document):
self.sent_or_received = "Sent"
self.set_status()
self.set_sender_full_name()
validate_email(self)
set_timeline_doc(self)
if self.communication_medium == "Email":
self.parse_email_for_timeline_links()
self.set_timeline_links()
self.deduplicate_timeline_links()
self.set_sender_full_name()
def validate_reference(self):
if self.reference_doctype and self.reference_name:
@ -79,6 +85,7 @@ class Communication(Document):
circular_linking = True
break
doc = get_parent_doc(doc)
if circular_linking:
frappe.throw(_("Please make sure the Reference Communication Docs are not circularly linked."), frappe.CircularLinkingError)
@ -154,7 +161,7 @@ class Communication(Document):
if sender_name == sender_email:
sender_name = None
self.sender = sender_email
self.sender_full_name = sender_name or get_fullname(frappe.session.user) if frappe.session.user!='Administrator' else None
self.sender_full_name = sender_name or frappe.db.exists("Contact", {"email_id": sender_email}) or sender_email
def send(self, print_html=None, print_format=None, attachments=None,
send_me_a_copy=False, recipients=None):
@ -231,26 +238,69 @@ class Communication(Document):
if commit:
frappe.db.commit()
def parse_email_for_timeline_links(self):
parse_email(self, [self.recipients, self.cc, self.bcc])
# Timeline Links
def set_timeline_links(self):
contacts = get_contacts([self.sender, self.recipients, self.cc, self.bcc])
for contact_name in contacts:
self.add_link('Contact', contact_name)
#link contact's dynamic links to communication
add_contact_links_to_communication(self, contact_name)
def deduplicate_timeline_links(self):
if self.timeline_links:
links, duplicate = [], False
for l in self.timeline_links:
t = (l.link_doctype, l.link_name)
if not t in links:
links.append(t)
else:
duplicate = True
if duplicate:
del self.timeline_links[:] # make it python 2 compatible as list.clear() is python 3 only
for l in links:
self.add_link(link_doctype=l[0], link_name=l[1])
def add_link(self, link_doctype, link_name, autosave=False):
self.append("timeline_links",
{
"link_doctype": link_doctype,
"link_name": link_name
}
)
if autosave:
self.save(ignore_permissions=True)
def get_links(self):
return self.timeline_links
def remove_link(self, link_doctype, link_name, autosave=False, ignore_permissions=True):
for l in self.timeline_links:
if l.link_doctype == link_doctype and l.link_name == link_name:
self.timeline_links.remove(l)
if autosave:
self.save(ignore_permissions=ignore_permissions)
def on_doctype_update():
"""Add indexes in `tabCommunication`"""
frappe.db.add_index("Communication", ["reference_doctype", "reference_name"])
frappe.db.add_index("Communication", ["timeline_doctype", "timeline_name"])
frappe.db.add_index("Communication", ["link_doctype", "link_name"])
frappe.db.add_index("Communication", ["status", "communication_type"])
def has_permission(doc, ptype, user):
if ptype=="read":
if (doc.reference_doctype == "Communication" and doc.reference_name == doc.name) \
or (doc.timeline_doctype == "Communication" and doc.timeline_name == doc.name):
return
if doc.reference_doctype == "Communication" and doc.reference_name == doc.name:
return
if doc.reference_doctype and doc.reference_name:
if frappe.has_permission(doc.reference_doctype, ptype="read", doc=doc.reference_name):
return True
if doc.timeline_doctype and doc.timeline_name:
if frappe.has_permission(doc.timeline_doctype, ptype="read", doc=doc.timeline_name):
return True
def get_permission_query_conditions_for_communication(user):
if not user: user = frappe.session.user
@ -265,8 +315,76 @@ def get_permission_query_conditions_for_communication(user):
distinct=True, order_by="idx")
if not accounts:
return """tabCommunication.communication_medium!='Email'"""
return """`tabCommunication`.communication_medium!='Email'"""
email_accounts = [ '"%s"'%account.get("email_account") for account in accounts ]
return """tabCommunication.email_account in ({email_accounts})"""\
return """`tabCommunication`.email_account in ({email_accounts})"""\
.format(email_accounts=','.join(email_accounts))
def get_contacts(email_strings):
email_addrs = []
for email_string in email_strings:
if email_string:
for email in email_string.split(","):
parsed_email = parseaddr(email)[1]
if parsed_email:
email_addrs.append(parsed_email)
contacts = []
for email in email_addrs:
email = get_email_without_link(email)
contact_name = frappe.db.get_value('Contact', {'email_id': email})
if not contact_name:
contact = frappe.get_doc({
"doctype": "Contact",
"first_name": frappe.unscrub(email.split("@")[0]),
"email_id": email
}).insert(ignore_permissions=True)
contact_name = contact.name
contacts.append(contact_name)
return contacts
def add_contact_links_to_communication(communication, contact_name):
contact_links = frappe.get_list("Dynamic Link", filters={
"parenttype": "Contact",
"parent": contact_name
}, fields=["link_doctype", "link_name"])
if contact_links:
for contact_link in contact_links:
communication.add_link(contact_link.link_doctype, contact_link.link_name)
def parse_email(communication, email_strings):
"""
Parse email to add timeline links.
When automatic email linking is enabled, an email from email_strings can contain
a doctype and docname ie in the format `admin+doctype+docname@example.com`,
the email is parsed and doctype and docname is extracted and timeline link is added.
"""
delimiter = "+"
for email_string in email_strings:
if email_string:
for email in email_string.split(","):
if delimiter in email:
email = email.split("@")[0]
doctype = unquote(email.split(delimiter)[1])
docname = unquote(email.split(delimiter)[2])
if doctype and docname and frappe.db.exists(doctype, docname):
communication.add_link(doctype, docname)
def get_email_without_link(email):
"""
returns email address without doctype links
returns admin@example.com for email admin+doctype+docname@example.com
"""
email_id = email.split("@")[0].split("+")[0]
email_host = email.split("@")[1]
return "{0}@{1}".format(email_id, email_host)

View file

@ -71,12 +71,9 @@ def make(doctype=None, name=None, content=None, subject=None, sent_or_received =
"message_id":get_message_id().strip(" <>"),
"read_receipt":read_receipt,
"has_attachment": 1 if attachments else 0
})
comm.insert(ignore_permissions=True)
}).insert(ignore_permissions=True)
if not doctype:
# if no reference given, then send it against the communication
comm.db_set(dict(reference_doctype='Communication', reference_name=comm.name))
comm.save(ignore_permissions=True)
if isinstance(attachments, string_types):
attachments = json.loads(attachments)
@ -557,5 +554,4 @@ def mark_email_as_seen(name=None):
frappe.response["type"] = 'binary'
frappe.response["filename"] = "imaginary_pixel.png"
frappe.response["filecontent"] = buffered_obj.getvalue()
frappe.response["filecontent"] = buffered_obj.getvalue()

View file

@ -4,6 +4,7 @@ from __future__ import unicode_literals
import frappe
import unittest
from six.moves.urllib.parse import quote
test_records = frappe.get_test_records('Communication')
@ -44,28 +45,153 @@ class TestCommunication(unittest.TestCase):
self.assertFalse(frappe.utils.parse_addr(x)[0])
def test_circular_linking(self):
content = "This was created to test circular linking"
a = frappe.get_doc({
"doctype": "Communication",
"communication_type": "Communication",
"content": content,
}).insert()
"content": "This was created to test circular linking: Communication A",
}).insert(ignore_permissions=True)
b = frappe.get_doc({
"doctype": "Communication",
"communication_type": "Communication",
"content": content,
"content": "This was created to test circular linking: Communication B",
"reference_doctype": "Communication",
"reference_name": a.name
}).insert()
}).insert(ignore_permissions=True)
c = frappe.get_doc({
"doctype": "Communication",
"communication_type": "Communication",
"content": content,
"content": "This was created to test circular linking: Communication C",
"reference_doctype": "Communication",
"reference_name": b.name
}).insert()
}).insert(ignore_permissions=True)
a = frappe.get_doc("Communication", a.name)
a.reference_doctype = "Communication"
a.reference_name = c.name
self.assertRaises(frappe.CircularLinkingError, a.save)
def test_deduplication_timeline_links(self):
frappe.delete_doc_if_exists("Note", "deduplication timeline links")
note = frappe.get_doc({
"doctype": "Note",
"title": "deduplication timeline links",
"content": "deduplication timeline links"
}).insert(ignore_permissions=True)
comm = frappe.get_doc({
"doctype": "Communication",
"communication_type": "Communication",
"content": "Deduplication of Links",
"communication_medium": "Email"
}).insert(ignore_permissions=True)
#adding same link twice
comm.add_link(link_doctype="Note", link_name=note.name, autosave=True)
comm.add_link(link_doctype="Note", link_name=note.name, autosave=True)
comm = frappe.get_doc("Communication", comm.name)
self.assertNotEqual(2, len(comm.timeline_links))
def test_contacts_attached(self):
contact_sender = frappe.get_doc({
"doctype": "Contact",
"first_name": frappe.generate_hash(length=10),
"email_id": "comm_sender@example.com"
}).insert(ignore_permissions=True)
contact_recipient = frappe.get_doc({
"doctype": "Contact",
"first_name": frappe.generate_hash(length=10),
"email_id": "comm_recipient@example.com"
}).insert(ignore_permissions=True)
contact_cc = frappe.get_doc({
"doctype": "Contact",
"first_name": frappe.generate_hash(length=10),
"email_id": "comm_cc@example.com"
}).insert(ignore_permissions=True)
comm = frappe.get_doc({
"doctype": "Communication",
"communication_medium": "Email",
"subject": "Contacts Attached Test",
"sender": "comm_sender@example.com",
"recipients": "comm_recipient@example.com",
"cc": "comm_cc@example.com"
}).insert(ignore_permissions=True)
comm = frappe.get_doc("Communication", comm.name)
contact_links = []
for timeline_link in comm.timeline_links:
contact_links.append(timeline_link.link_name)
self.assertIn(contact_sender.name, contact_links)
self.assertIn(contact_recipient.name, contact_links)
self.assertIn(contact_cc.name, contact_links)
def test_get_communication_data(self):
from frappe.desk.form.load import get_communication_data
frappe.delete_doc_if_exists("Note", "get communication data")
note = frappe.get_doc({
"doctype": "Note",
"title": "get communication data",
"content": "get communication data"
}).insert(ignore_permissions=True)
comm_note_1 = frappe.get_doc({
"doctype": "Communication",
"communication_type": "Communication",
"content": "Test Get Communication Data 1",
"communication_medium": "Email"
}).insert(ignore_permissions=True)
comm_note_1.add_link(link_doctype="Note", link_name=note.name, autosave=True)
comm_note_2 = frappe.get_doc({
"doctype": "Communication",
"communication_type": "Communication",
"content": "Test Get Communication Data 2",
"communication_medium": "Email"
}).insert(ignore_permissions=True)
comm_note_2.add_link(link_doctype="Note", link_name=note.name, autosave=True)
comms = get_communication_data("Note", note.name, as_dict=True)
data = []
for comm in comms:
data.append(comm.name)
self.assertIn(comm_note_1.name, data)
self.assertIn(comm_note_2.name, data)
def test_link_in_email(self):
frappe.delete_doc_if_exists("Note", "test document link in email")
note = frappe.get_doc({
"doctype": "Note",
"title": "test document link in email",
"content": "test document link in email"
}).insert(ignore_permissions=True)
comm = frappe.get_doc({
"doctype": "Communication",
"communication_medium": "Email",
"subject": "Document Link in Email",
"sender": "comm_sender@example.com",
"recipients": "comm_recipient+{0}+{1}@example.com".format(quote("Note"), quote(note.name)),
}).insert(ignore_permissions=True)
doc_links = []
for timeline_link in comm.timeline_links:
doc_links.append((timeline_link.link_doctype, timeline_link.link_name))
self.assertIn(("Note", note.name), doc_links)

View file

@ -0,0 +1,47 @@
{
"creation": "2019-05-21 09:47:23.043960",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"link_doctype",
"link_name",
"link_title"
],
"fields": [
{
"fieldname": "link_doctype",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Link DocType",
"options": "DocType",
"reqd": 1
},
{
"fieldname": "link_name",
"fieldtype": "Dynamic Link",
"in_list_view": 1,
"label": "Link Name",
"options": "link_doctype",
"reqd": 1
},
{
"fieldname": "link_title",
"fieldtype": "Read Only",
"in_list_view": 1,
"label": "Link Title",
"read_only": 1
}
],
"istable": 1,
"modified": "2019-05-21 09:47:23.043960",
"modified_by": "Administrator",
"module": "Core",
"name": "Communication Link",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View file

@ -0,0 +1,13 @@
# -*- coding: utf-8 -*-
# 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
class CommunicationLink(Document):
pass
def on_doctype_update():
frappe.db.add_index("Communication Link", ["link_doctype", "link_name"])

View file

@ -108,7 +108,7 @@
"no_copy": 0,
"oldfieldname": "fieldtype",
"oldfieldtype": "Select",
"options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRead Only\nSection Break\nSelect\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nSignature",
"options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRead Only\nRating\nSection Break\nSelect\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nSignature",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
@ -1710,7 +1710,7 @@
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2019-04-08 12:19:53.415372",
"modified": "2019-05-28 12:19:53.415372",
"modified_by": "Administrator",
"module": "Core",
"name": "DocField",

View file

@ -22,9 +22,15 @@ frappe.ui.form.on('DocType', {
}
if (!frm.is_new() && !frm.doc.istable) {
frm.add_custom_button(__('Go to {0} List', [frm.doc.name]), () => {
frappe.set_route('List', frm.doc.name, 'List');
});
if (frm.doc.issingle) {
frm.add_custom_button(__('Go to {0}', [frm.doc.name]), () => {
frappe.set_route('Form', frm.doc.name);
});
} else {
frm.add_custom_button(__('Go to {0} List', [frm.doc.name]), () => {
frappe.set_route('List', frm.doc.name, 'List');
});
}
}
if(!frappe.boot.developer_mode && !frm.doc.custom) {

File diff suppressed because it is too large Load diff

View file

@ -13,6 +13,7 @@ from frappe.utils import now, cint
from frappe.model import no_value_fields, default_fields, data_fieldtypes, table_fields
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
from frappe.desk.notifications import delete_notification_count_for
from frappe.modules import make_boilerplate, get_doc_path
from frappe.database.schema import validate_column_name, validate_column_length
@ -23,6 +24,14 @@ import frappe.website.render
import json
class InvalidFieldNameError(frappe.ValidationError): pass
class UniqueFieldnameError(frappe.ValidationError): pass
class IllegalMandatoryError(frappe.ValidationError): pass
class DoctypeLinkError(frappe.ValidationError): pass
class WrongOptionsDoctypeLinkError(frappe.ValidationError): pass
class HiddenAndMandatoryWithoutDefaultError(frappe.ValidationError): pass
class NonUniqueError(frappe.ValidationError): pass
class CannotIndexedError(frappe.ValidationError): pass
class CannotCreateStandardDoctypeError(frappe.ValidationError): pass
form_grid_templates = {
"fields": "templates/form_grid/fields.html"
@ -39,7 +48,8 @@ class DocType(Document):
- Validate series
- Check fieldnames (duplication etc)
- Clear permission table for child tables
- Add `amended_from` and `amended_by` if Amendable"""
- Add `amended_from` and `amended_by` if Amendable
- Add custom field `auto_repeat` if Repeatable"""
self.check_developer_mode()
@ -68,6 +78,7 @@ class DocType(Document):
validate_permissions(self)
self.make_amendable()
self.make_repeatable()
self.validate_website()
if not self.is_new():
@ -101,7 +112,7 @@ class DocType(Document):
return
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."))
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'''
@ -121,7 +132,7 @@ class DocType(Document):
link_fieldname, source_fieldname = df.fetch_from.split('.', 1)
link_df = new_meta.get_field(link_fieldname)
if frappe.conf.db_type == 'postgres':
if frappe.db.db_type == 'postgres':
update_query = '''
UPDATE `tab{doctype}`
SET `{fieldname}` = source.`{source_fieldname}`
@ -195,6 +206,9 @@ class DocType(Document):
d.fieldname = d.fieldname + '_column'
else:
d.fieldname = d.fieldtype.lower().replace(" ","_") + "_" + str(d.idx)
else:
if d.fieldname in restricted:
frappe.throw(_("Fieldname {0} is restricted").format(d.fieldname), InvalidFieldNameError)
d.fieldname = re.sub('''['",./%@()<>{}]''', '', d.fieldname)
@ -373,7 +387,7 @@ class DocType(Document):
os.path.join(new_path, fname.replace(frappe.scrub(old), frappe.scrub(new)))])
self.rename_inside_controller(new, old, new_path)
frappe.msgprint('Renamed files and replaced code in controllers, please check!')
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'):
@ -515,6 +529,14 @@ 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:
if not frappe.db.exists('Custom Field', {'fieldname': 'auto_repeat', 'dt': self.name}):
insert_after = self.fields[len(self.fields) - 1].fieldname
df = dict(fieldname='auto_repeat', label='Auto Repeat', fieldtype='Link', options='Auto Repeat', insert_after=insert_after, read_only=1, no_copy=1, print_hide=1)
create_custom_field(self.name, df)
def get_max_idx(self):
"""Returns the highest `idx`"""
max_idx = frappe.db.sql("""select max(idx) from `tabDocField` where parent = %s""",
@ -542,7 +564,6 @@ def validate_fields_for_doctype(doctype):
# this is separate because it is also called via custom field
def validate_fields(meta):
"""Validate doctype fields. Checks
1. There are no illegal characters in fieldnames
2. If fieldnames are unique.
3. Validate column length.
@ -562,38 +583,38 @@ def validate_fields(meta):
def check_illegal_characters(fieldname):
validate_column_name(fieldname)
def check_unique_fieldname(fieldname):
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(_("Fieldname {0} appears multiple times in rows {1}").format(fieldname, ", ".join(duplicates)))
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(d):
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(_("Field {0} of type {1} cannot be mandatory").format(d.label, d.fieldtype))
frappe.throw(_("{0}: Field {1} of type {2} cannot be mandatory").format(docname, d.label, d.fieldtype), IllegalMandatoryError)
def check_link_table_options(d):
def check_link_table_options(docname, d):
if d.fieldtype in ("Link",) + table_fields:
if not d.options:
frappe.throw(_("Options required for Link or Table type field {0} in row {1}").format(d.label, d.idx))
frappe.throw(_("{0}: Options required for Link or Table type field {1} in row {2}").format(docname, d.label, d.idx), DoctypeLinkError)
if d.options=="[Select]" or d.options==d.parent:
return
if d.options != d.parent:
options = frappe.db.get_value("DocType", d.options, "name")
if not options:
frappe.throw(_("Options must be a valid DocType for field {0} in row {1}").format(d.label, d.idx))
frappe.throw(_("{0}: Options must be a valid DocType for field {1} in row {2}").format(docname, d.label, d.idx), WrongOptionsDoctypeLinkError)
elif not (options == d.options):
frappe.throw(_("Options {0} must be the same as doctype name {1} for the field {2}")
.format(d.options, options, d.label))
frappe.throw(_("{0}: Options {1} must be the same as doctype name {2} for the field {3}", DoctypeLinkError)
.format(docname, d.options, options, d.label))
else:
# fix case
d.options = options
def check_hidden_and_mandatory(d):
def check_hidden_and_mandatory(docname, d):
if d.hidden and d.reqd and not d.default:
frappe.throw(_("Field {0} in row {1} cannot be hidden and mandatory without default").format(d.label, d.idx))
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:
@ -616,7 +637,9 @@ def validate_fields(meta):
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 d.default and d.default not in ('0', '1'):
if d.fieldtype == "Check" and not d.default:
d.default = '0'
if d.fieldtype == "Check" and d.default not in ('0', '1'):
frappe.throw(_("Default for 'Check' type of field must be either '0' or '1'"))
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))
@ -625,14 +648,14 @@ def validate_fields(meta):
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(d):
def check_unique_and_text(docname, d):
if meta.issingle:
d.unique = 0
d.search_index = 0
if getattr(d, "unique", False):
if d.fieldtype not in ("Data", "Link", "Read Only"):
frappe.throw(_("Fieldtype {0} for {1} cannot be unique").format(d.fieldtype, d.label))
frappe.throw(_("{0}: Fieldtype {1} for {2} cannot be unique").format(docname, d.fieldtype, d.label), NonUniqueError)
if not d.get("__islocal") and frappe.db.has_column(d.parent, d.fieldname):
has_non_unique_values = frappe.db.sql("""select `{fieldname}`, count(*)
@ -641,10 +664,10 @@ def validate_fields(meta):
doctype=d.parent, fieldname=d.fieldname))
if has_non_unique_values and has_non_unique_values[0][0]:
frappe.throw(_("Field '{0}' cannot be set as Unique as it has non-unique values").format(d.label))
frappe.throw(_("{0}: Field '{1}' cannot be set as Unique as it has non-unique values").format(docname, d.label), NonUniqueError)
if d.search_index and d.fieldtype in ("Text", "Long Text", "Small Text", "Code", "Text Editor"):
frappe.throw(_("Fieldtype {0} for {1} cannot be indexed").format(d.fieldtype, d.label))
frappe.throw(_("{0}:Fieldtype {1} for {2} cannot be indexed").format(docname, d.fieldtype, d.label), CannotIndexedError)
def check_fold(fields):
fold_exists = False
@ -790,21 +813,20 @@ def validate_fields(meta):
for d in fields:
if not d.permlevel: d.permlevel = 0
if d.fieldtype not in table_fields: d.allow_bulk_edit = 0
if d.fieldtype == "Barcode": d.ignore_xss_filter = 1
if not d.fieldname:
d.fieldname = d.fieldname.lower()
check_illegal_characters(d.fieldname)
check_unique_fieldname(d.fieldname)
check_unique_fieldname(meta.get("name"), d.fieldname)
check_fieldname_length(d.fieldname)
check_illegal_mandatory(d)
check_link_table_options(d)
check_illegal_mandatory(meta.get("name"), d)
check_link_table_options(meta.get("name"), d)
check_dynamic_link_options(d)
check_hidden_and_mandatory(d)
check_hidden_and_mandatory(meta.get("name"), d)
check_in_list_view(d)
check_in_global_search(d)
check_illegal_default(d)
check_unique_and_text(d)
check_unique_and_text(meta.get("name"), d)
check_illegal_depends_on_conditions(d)
check_table_multiselect_option(d)
scrub_options_in_select(d)

View file

@ -5,6 +5,8 @@ from __future__ import unicode_literals
import frappe
import unittest
from frappe.core.doctype.doctype.doctype import UniqueFieldnameError, IllegalMandatoryError, DoctypeLinkError, WrongOptionsDoctypeLinkError,\
HiddenAndMandatoryWithoutDefaultError, CannotIndexedError, InvalidFieldNameError, CannotCreateStandardDoctypeError
# test_records = frappe.get_test_records('DocType')
@ -226,3 +228,65 @@ class TestDocType(unittest.TestCase):
raise
finally:
frappe.flags.allow_doctype_export = 0
def test_unique_field_name_for_two_fields(self):
doc = self.new_doctype('Test Unique Field')
field_1 = doc.append('fields', {})
field_1.fieldname = 'some_fieldname_1'
field_1.fieldtype = 'Data'
field_2 = doc.append('fields', {})
field_2.fieldname = 'some_fieldname_1'
field_2.fieldtype = 'Data'
self.assertRaises(UniqueFieldnameError, doc.insert)
def test_fieldname_is_not_name(self):
doc = self.new_doctype('Test Name Field')
field_1 = doc.append('fields', {})
field_1.label = 'Name'
field_1.fieldtype = 'Data'
doc.insert()
self.assertEqual(doc.fields[1].fieldname, "name1")
doc.fields[1].fieldname = 'name'
self.assertRaises(InvalidFieldNameError, doc.save)
def test_illegal_mandatory_validation(self):
doc = self.new_doctype('Test Illegal mandatory')
field_1 = doc.append('fields', {})
field_1.fieldname = 'some_fieldname_1'
field_1.fieldtype = 'Section Break'
field_1.reqd = 1
self.assertRaises(IllegalMandatoryError, doc.insert)
def test_link_with_wrong_and_no_options(self):
doc = self.new_doctype('Test link')
field_1 = doc.append('fields', {})
field_1.fieldname = 'some_fieldname_1'
field_1.fieldtype = 'Link'
self.assertRaises(DoctypeLinkError, doc.insert)
field_1.options = 'wrongdoctype'
self.assertRaises(WrongOptionsDoctypeLinkError, doc.insert)
def test_hidden_and_mandatory_without_default(self):
doc = self.new_doctype('Test hidden and mandatory')
field_1 = doc.append('fields', {})
field_1.fieldname = 'some_fieldname_1'
field_1.fieldtype = 'Data'
field_1.reqd = 1
field_1.hidden = 1
self.assertRaises(HiddenAndMandatoryWithoutDefaultError, doc.insert)
def test_field_can_not_be_indexed_validation(self):
doc = self.new_doctype('Test index')
field_1 = doc.append('fields', {})
field_1.fieldname = 'some_fieldname_1'
field_1.fieldtype = 'Long Text'
field_1.search_index = 1
self.assertRaises(CannotIndexedError, doc.insert)

View file

@ -1,95 +1,54 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"autoname": "field:domain",
"beta": 0,
"creation": "2017-05-03 15:07:39.752820",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"autoname": "field:domain",
"creation": "2017-05-03 15:07:39.752820",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"domain"
],
"fields": [
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "domain",
"fieldtype": "Data",
"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": "Domain",
"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": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
"fieldname": "domain",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Domain",
"reqd": 1,
"unique": 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": 0,
"max_attachments": 0,
"modified": "2017-09-15 12:26:21.827149",
"modified_by": "Administrator",
"module": "Core",
"name": "Domain",
"name_case": "",
"owner": "makarand@erpnext.com",
],
"modified": "2019-06-30 13:24:13.732202",
"modified_by": "Administrator",
"module": "Core",
"name": "Domain",
"owner": "makarand@erpnext.com",
"permissions": [
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 1,
"delete": 0,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "Administrator",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"create": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Administrator",
"share": 1,
"write": 1
},
{
"create": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"search_fields": "domain",
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "domain",
"track_changes": 0,
"track_seen": 0
],
"quick_entry": 1,
"search_fields": "domain",
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "domain"
}

View file

@ -1,125 +1,47 @@
{
"allow_copy": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2017-01-13 04:55:18.835023",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"creation": "2017-01-13 04:55:18.835023",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"link_doctype",
"link_name",
"link_title"
],
"fields": [
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "link_doctype",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Link DocType",
"length": 0,
"no_copy": 0,
"options": "DocType",
"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": "link_doctype",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Link DocType",
"options": "DocType",
"reqd": 1
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "link_name",
"fieldtype": "Dynamic Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Link Name",
"length": 0,
"no_copy": 0,
"options": "link_doctype",
"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": "link_name",
"fieldtype": "Dynamic Link",
"in_list_view": 1,
"label": "Link Name",
"options": "link_doctype",
"reqd": 1
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "link_title",
"fieldtype": "Read Only",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Link Title",
"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
"fieldname": "link_title",
"fieldtype": "Read Only",
"in_list_view": 1,
"label": "Link Title",
"read_only": 1
}
],
"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": "2017-01-17 14:25:49.140730",
"modified_by": "Administrator",
"module": "Core",
"name": "Dynamic Link",
"name_case": "",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0
],
"istable": 1,
"modified": "2019-05-16 19:54:31.400026",
"modified_by": "Administrator",
"module": "Core",
"name": "Dynamic Link",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View file

@ -2,6 +2,7 @@
# MIT License. See license.txt
from __future__ import unicode_literals
from frappe import _
"""
record of files
@ -446,7 +447,7 @@ class File(NestedSet):
def validate_url(self, df=None):
if self.file_url:
if not self.file_url.startswith(("http://", "https://", "/files/", "/private/files/")):
frappe.throw("URL must start with 'http://' or 'https://'")
frappe.throw(_("URL must start with 'http://' or 'https://'"))
return
self.file_url = unquote(self.file_url)

File diff suppressed because it is too large Load diff

View file

@ -9,16 +9,22 @@ frappe.ui.form.on('Role Permission for Page and Report', {
refresh: function(frm) {
frm.disable_save();
frm.role_area.hide();
frm.add_custom_button(__("Reset to defaults"), function() {
frm.trigger("reset_roles");
});
frm.add_custom_button(__("Update"), function() {
frm.trigger("update_report_page_data");
}).addClass('btn-primary');
frm.events.add_custom_buttons(frm);
},
add_custom_buttons: function(frm) {
frm.clear_custom_buttons();
if(frm.doc.set_role_for && frm.doc[frappe.model.scrub(frm.doc.set_role_for)]) {
frm.add_custom_button(__("Reset to defaults"), function() {
frm.trigger("reset_roles");
});
frm.add_custom_button(__("Update"), function() {
frm.trigger("update_report_page_data");
}).addClass('btn-primary');
}
},
onload: function(frm) {
if(!frm.roles_editor) {
frm.role_area = $('<div style="min-height: 300px">')
@ -48,14 +54,20 @@ frappe.ui.form.on('Role Permission for Page and Report', {
},
page: function(frm) {
frm.events.add_custom_buttons(frm);
if(frm.doc.page) {
frm.trigger("set_report_page_data");
} else {
frm.trigger("set_role_for");
}
},
report: function(frm){
frm.events.add_custom_buttons(frm);
if(frm.doc.report) {
frm.trigger("set_report_page_data");
} else {
frm.trigger("set_role_for");
}
},
@ -107,7 +119,7 @@ frappe.ui.form.on('Role Permission for Page and Report', {
if(!frm.doc.set_role_for){
frappe.throw(__("Mandatory field: set role for"))
}
if(frm.doc.set_role_for && !frm.doc[frm.doc.set_role_for.toLocaleLowerCase()]) {
frappe.throw(__("Mandatory field: {0}", [frm.doc.set_role_for]))
}

View file

@ -15,7 +15,7 @@ frappe.ui.form.on('Success Action', {
validate: (frm) => {
const checked_actions = frm.action_multicheck.get_checked_options();
if (checked_actions.length < 2) {
frappe.msgprint('Select atleast 2 actions');
frappe.msgprint(__('Select atleast 2 actions'));
} else {
return true;
}

View file

@ -1,5 +1,6 @@
{
"allow_copy": 0,
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
@ -18,6 +19,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "localization",
"fieldtype": "Section Break",
"hidden": 0,
@ -49,6 +51,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "country",
"fieldtype": "Link",
"hidden": 0,
@ -82,6 +85,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "language",
"fieldtype": "Link",
"hidden": 0,
@ -114,6 +118,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "column_break_3",
"fieldtype": "Column Break",
"hidden": 0,
@ -145,6 +150,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "time_zone",
"fieldtype": "Select",
"hidden": 0,
@ -176,6 +182,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "is_first_startup",
"fieldtype": "Check",
"hidden": 1,
@ -208,6 +215,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "setup_complete",
"fieldtype": "Check",
"hidden": 1,
@ -240,6 +248,7 @@
"bold": 0,
"collapsible": 1,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "date_and_number_format",
"fieldtype": "Section Break",
"hidden": 0,
@ -271,6 +280,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "date_format",
"fieldtype": "Select",
"hidden": 0,
@ -303,6 +313,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "column_break_7",
"fieldtype": "Column Break",
"hidden": 0,
@ -334,6 +345,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "number_format",
"fieldtype": "Select",
"hidden": 0,
@ -366,6 +378,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "float_precision",
"fieldtype": "Select",
"hidden": 0,
@ -399,6 +412,7 @@
"collapsible": 0,
"columns": 0,
"description": "If not set, the currency precision will depend on number format",
"fetch_if_empty": 0,
"fieldname": "currency_precision",
"fieldtype": "Select",
"hidden": 0,
@ -432,6 +446,7 @@
"bold": 0,
"collapsible": 1,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "sec_backup_limit",
"fieldtype": "Section Break",
"hidden": 0,
@ -466,6 +481,7 @@
"columns": 0,
"default": "3",
"description": "Older backups will be automatically deleted",
"fetch_if_empty": 0,
"fieldname": "backup_limit",
"fieldtype": "Int",
"hidden": 0,
@ -498,6 +514,7 @@
"bold": 0,
"collapsible": 1,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "background_workers",
"fieldtype": "Section Break",
"hidden": 0,
@ -531,6 +548,7 @@
"collapsible": 0,
"columns": 0,
"description": "Run scheduled jobs only if checked",
"fetch_if_empty": 0,
"fieldname": "enable_scheduler",
"fieldtype": "Check",
"hidden": 1,
@ -562,6 +580,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "scheduler_last_event",
"fieldtype": "Data",
"hidden": 1,
@ -594,6 +613,7 @@
"bold": 0,
"collapsible": 1,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "permissions",
"fieldtype": "Section Break",
"hidden": 0,
@ -628,6 +648,7 @@
"columns": 0,
"default": "0",
"description": "If Apply Strict User Permission is checked and User Permission is defined for a DocType for a User, then all the documents where value of the link is blank, will not be shown to that User",
"fetch_if_empty": 0,
"fieldname": "apply_strict_user_permissions",
"fieldtype": "Check",
"hidden": 0,
@ -660,6 +681,7 @@
"bold": 0,
"collapsible": 1,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "security",
"fieldtype": "Section Break",
"hidden": 0,
@ -693,6 +715,7 @@
"columns": 0,
"default": "06:00",
"description": "Session Expiry in Hours e.g. 06:00",
"fetch_if_empty": 0,
"fieldname": "session_expiry",
"fieldtype": "Data",
"hidden": 0,
@ -727,6 +750,7 @@
"columns": 0,
"default": "720:00",
"description": "In Hours",
"fetch_if_empty": 0,
"fieldname": "session_expiry_mobile",
"fieldtype": "Data",
"hidden": 0,
@ -759,75 +783,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "1",
"description": "If enabled, the password strength will be enforced based on the Minimum Password Score value. A value of 2 being medium strong and 4 being very strong.",
"fieldname": "enable_password_policy",
"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 Password Policy",
"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
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "2",
"depends_on": "eval:doc.enable_password_policy==1",
"fieldname": "minimum_password_score",
"fieldtype": "Select",
"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": "Minimum Password Score",
"length": 0,
"no_copy": 0,
"options": "2\n3\n4",
"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
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "column_break_13",
"fieldtype": "Column Break",
"hidden": 0,
@ -860,6 +816,7 @@
"collapsible": 0,
"columns": 0,
"description": "Note: Multiple sessions will be allowed in case of mobile device",
"fetch_if_empty": 0,
"fieldname": "deny_multiple_sessions",
"fieldtype": "Check",
"hidden": 0,
@ -894,6 +851,7 @@
"columns": 0,
"default": "0",
"description": "User can login using Email id or Mobile number",
"fetch_if_empty": 0,
"fieldname": "allow_login_using_mobile_number",
"fieldtype": "Check",
"hidden": 0,
@ -928,6 +886,7 @@
"columns": 0,
"default": "0",
"description": "User can login using Email id or User Name",
"fetch_if_empty": 0,
"fieldname": "allow_login_using_user_name",
"fieldtype": "Check",
"hidden": 0,
@ -962,6 +921,7 @@
"columns": 0,
"default": "1",
"description": "",
"fetch_if_empty": 0,
"fieldname": "allow_error_traceback",
"fieldtype": "Check",
"hidden": 0,
@ -994,6 +954,177 @@
"bold": 0,
"collapsible": 1,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "password_settings",
"fieldtype": "Section 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,
"label": "Password",
"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
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "In Days",
"fetch_if_empty": 0,
"fieldname": "force_user_to_reset_password",
"fieldtype": "Int",
"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": "Force User to Reset Password",
"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
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "column_break_31",
"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
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "1",
"description": "If enabled, the password strength will be enforced based on the Minimum Password Score value. A value of 2 being medium strong and 4 being very strong.",
"fetch_if_empty": 0,
"fieldname": "enable_password_policy",
"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 Password Policy",
"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
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "2",
"depends_on": "eval:doc.enable_password_policy==1",
"fetch_if_empty": 0,
"fieldname": "minimum_password_score",
"fieldtype": "Select",
"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": "Minimum Password Score",
"length": 0,
"no_copy": 0,
"options": "2\n3\n4",
"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
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 1,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "brute_force_security",
"fieldtype": "Section Break",
"hidden": 0,
@ -1026,6 +1157,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "allow_consecutive_login_attempts",
"fieldtype": "Int",
"hidden": 0,
@ -1058,6 +1190,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "column_break_34",
"fieldtype": "Column Break",
"hidden": 0,
@ -1091,6 +1224,7 @@
"columns": 0,
"default": "60",
"description": "In seconds",
"fetch_if_empty": 0,
"fieldname": "allow_login_after_fail",
"fieldtype": "Int",
"hidden": 0,
@ -1123,6 +1257,7 @@
"bold": 0,
"collapsible": 1,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "two_factor_authentication",
"fieldtype": "Section Break",
"hidden": 0,
@ -1155,6 +1290,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "enable_two_factor_auth",
"fieldtype": "Check",
"hidden": 0,
@ -1190,6 +1326,7 @@
"default": "0",
"depends_on": "enable_two_factor_auth",
"description": "If enabled, users who login from Restricted IP Address, won't be prompted for Two Factor Auth",
"fetch_if_empty": 0,
"fieldname": "bypass_2fa_for_retricted_ip_users",
"fieldtype": "Check",
"hidden": 0,
@ -1224,6 +1361,7 @@
"columns": 0,
"depends_on": "enable_two_factor_auth",
"description": "If enabled, all users can login from any IP Address using Two Factor Auth. This can also be set only for specific user(s) in User Page",
"fetch_if_empty": 0,
"fieldname": "bypass_restrict_ip_check_if_2fa_enabled",
"fieldtype": "Check",
"hidden": 0,
@ -1259,6 +1397,7 @@
"default": "OTP App",
"depends_on": "",
"description": "Choose authentication method to be used by all users",
"fetch_if_empty": 0,
"fieldname": "two_factor_method",
"fieldtype": "Select",
"hidden": 0,
@ -1294,6 +1433,7 @@
"columns": 0,
"depends_on": "eval:doc.two_factor_method == \"OTP App\"",
"description": "Time in seconds to retain QR code image on server. Min:<strong>240</strong>",
"fetch_if_empty": 0,
"fieldname": "lifespan_qrcode_image",
"fieldtype": "Int",
"hidden": 0,
@ -1328,6 +1468,7 @@
"columns": 0,
"default": "Frappe Framework",
"depends_on": "enable_two_factor_auth",
"fetch_if_empty": 0,
"fieldname": "otp_issuer_name",
"fieldtype": "Data",
"hidden": 0,
@ -1361,6 +1502,7 @@
"bold": 0,
"collapsible": 1,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "email",
"fieldtype": "Section Break",
"hidden": 0,
@ -1394,6 +1536,7 @@
"collapsible": 0,
"columns": 0,
"description": "Your organization name and address for the email footer.",
"fetch_if_empty": 0,
"fieldname": "email_footer_address",
"fieldtype": "Small Text",
"hidden": 0,
@ -1426,6 +1569,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "column_break_18",
"fieldtype": "Column Break",
"hidden": 0,
@ -1457,6 +1601,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "disable_standard_email_footer",
"fieldtype": "Check",
"hidden": 0,
@ -1489,6 +1634,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "hide_footer_in_auto_email_reports",
"fieldtype": "Check",
"hidden": 0,
@ -1521,6 +1667,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "chat",
"fieldtype": "Section Break",
"hidden": 0,
@ -1554,6 +1701,7 @@
"collapsible": 0,
"columns": 0,
"default": "1",
"fetch_if_empty": 0,
"fieldname": "enable_chat",
"fieldtype": "Check",
"hidden": 0,
@ -1587,6 +1735,7 @@
"collapsible": 0,
"columns": 0,
"default": "1",
"fetch_if_empty": 0,
"fieldname": "use_socketio_to_upload_file",
"fieldtype": "Check",
"hidden": 0,
@ -1624,7 +1773,7 @@
"issingle": 1,
"istable": 0,
"max_attachments": 0,
"modified": "2019-01-30 11:02:41.011412",
"modified": "2019-04-16 13:26:09.247487",
"modified_by": "Administrator",
"module": "Core",
"name": "System Settings",

View file

@ -7,7 +7,7 @@ from frappe import _
from frappe.model.document import Document
from frappe.model import no_value_fields
from frappe.translate import set_default_language
from frappe.utils import cint
from frappe.utils import cint, today
from frappe.utils.momentjs import get_all_timezones
from frappe.twofactor import toggle_two_factor_auth
@ -35,6 +35,11 @@ class SystemSettings(Document):
self.bypass_2fa_for_retricted_ip_users = 0
self.bypass_restrict_ip_check_if_2fa_enabled = 0
frappe.flags.update_last_reset_password_date = False
if (self.force_user_to_reset_password and
not cint(frappe.db.get_single_value("System Settings", "force_user_to_reset_password"))):
frappe.flags.update_last_reset_password_date = True
def on_update(self):
for df in self.meta.get("fields"):
if df.fieldtype not in no_value_fields:
@ -47,6 +52,16 @@ class SystemSettings(Document):
frappe.cache().delete_value('time_zone')
frappe.local.system_settings = {}
if frappe.flags.update_last_reset_password_date:
update_last_reset_password_date()
def update_last_reset_password_date():
frappe.db.sql(""" UPDATE `tabUser`
SET
last_password_reset_date = %s
WHERE
last_password_reset_date is null or last_password_reset_date = ''""", today())
@frappe.whitelist()
def load():
if not "System Manager" in frappe.get_roles():

View file

@ -3,5 +3,19 @@
frappe.ui.form.on('Translation', {
refresh: function(frm) {
if(frm.is_new() || !(["Saved", "Deleted"].includes(frm.doc.status))) return;
frm.add_custom_button('Contribute', function() {
frappe.call({
method: 'frappe.core.doctype.translation.translation.contribute_translation',
args: {
"language": frm.doc.language,
"contributor": frm.doc.owner,
"source_name": frm.doc.source_name,
"target_name": frm.doc.target_name,
"doc_name": frm.doc.name
}
});
}).addClass('btn-primary');
}
});

View file

@ -1,203 +1,91 @@
{
"allow_copy": 0,
"allow_import": 1,
"allow_rename": 0,
"autoname": "hash",
"beta": 0,
"creation": "2016-02-17 12:21:16.175465",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "Setup",
"editable_grid": 0,
"engine": "InnoDB",
"allow_import": 1,
"autoname": "hash",
"creation": "2016-02-17 12:21:16.175465",
"doctype": "DocType",
"document_type": "Setup",
"engine": "InnoDB",
"field_order": [
"language",
"section_break_4",
"source_name",
"column_break_6",
"target_name",
"section_break_6",
"status",
"contributed_translation_doctype_name"
],
"fields": [
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "language",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"in_standard_filter": 1,
"label": "Language",
"length": 0,
"no_copy": 0,
"options": "Language",
"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": 1,
"set_only_once": 0,
"unique": 0
},
"fieldname": "language",
"fieldtype": "Link",
"in_standard_filter": 1,
"label": "Language",
"options": "Language",
"search_index": 1
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "section_break_4",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 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,
"unique": 0
},
"fieldname": "section_break_4",
"fieldtype": "Section Break"
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "If your data is in HTML, please copy paste the exact HTML code with the tags.",
"fieldname": "source_name",
"fieldtype": "Code",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Source Text",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"description": "If your data is in HTML, please copy paste the exact HTML code with the tags.",
"fieldname": "source_name",
"fieldtype": "Code",
"label": "Source Text"
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_6",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 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,
"unique": 0
},
"fieldname": "column_break_6",
"fieldtype": "Column Break"
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "target_name",
"fieldtype": "Code",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Translated Text",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
"fieldname": "target_name",
"fieldtype": "Code",
"in_list_view": 1,
"label": "Translated Text"
},
{
"fieldname": "section_break_6",
"fieldtype": "Section Break"
},
{
"default": "Saved",
"depends_on": "eval: !doc.__islocal",
"fieldname": "status",
"fieldtype": "Select",
"label": "Status",
"options": "Saved\nContributed\nVerified\nPR sent\nDeleted",
"read_only": 1
},
{
"fieldname": "contributed_translation_doctype_name",
"fieldtype": "Data",
"hidden": 1,
"label": "Contributed Translation Doctype Name",
"read_only": 1
}
],
"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": "2016-12-29 14:39:48.571006",
"modified_by": "Administrator",
"module": "Core",
"name": "Translation",
"name_case": "",
"owner": "Administrator",
],
"modified": "2019-06-18 19:03:38.640990",
"modified_by": "Administrator",
"module": "Core",
"name": "Translation",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"is_custom": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "source_name",
"track_changes": 1,
"track_seen": 0
],
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "source_name",
"track_changes": 1
}

View file

@ -7,6 +7,8 @@ import frappe
from frappe.model.document import Document
from frappe.translate import clear_cache
from frappe.utils import strip_html_tags, is_html
from frappe.integrations.utils import make_post_request
import json
class Translation(Document):
def validate(self):
@ -21,3 +23,39 @@ class Translation(Document):
def on_trash(self):
clear_cache()
def onload(self):
if self.contributed_translation_doctype_name:
data = {"data": json.dumps({
"doc_name": self.contributed_translation_doctype_name
})}
try:
response = make_post_request(url=frappe.get_hooks("translation_contribution_status")[0], data=data)
except Exception:
frappe.msgprint("Something went wrong. Please check error log for more details")
if response.get("message").get("message") == "Contributed Translation has been deleted":
self.status = "Deleted"
self.contributed_translation_doctype_name = ""
self.save()
else:
self.status = response.get("message").get("status")
self.save()
@frappe.whitelist()
def contribute_translation(language, contributor, source_name, target_name, doc_name):
data = {"data": json.dumps({
"language": language,
"contributor": contributor,
"source_name": source_name,
"target_name": target_name,
"posting_date": frappe.utils.nowdate()
})}
try:
response = make_post_request(url=frappe.get_hooks("translation_contribution_url")[0], data=data)
except Exception:
frappe.msgprint("Something went wrong while contributing translation. Please check error log for more details")
if response.get("message").get("message") == "Already exists":
frappe.msgprint("Translation already exists")
elif response.get("message").get("message") == "Added to contribution list":
frappe.set_value("Translation", doc_name, "contributed_translation_doctype_name", response.get("message").get("doc_name"))
frappe.msgprint("Translation successfully contributed")

View file

@ -268,21 +268,38 @@ class TestUser(unittest.TestCase):
self.assertEqual(result['feedback']['password_policy_validation_passed'], True)
def test_comment_mentions(self):
user_name = "@test.comment@example.com"
self.assertEqual(extract_mentions(user_name)[0], "test.comment@example.com")
user_name = "@test.comment@test-example.com"
self.assertEqual(extract_mentions(user_name)[0], "test.comment@test-example.com")
user_name = "Testing comment, @test-user please check."
self.assertEqual(extract_mentions(user_name)[0], "test-user")
user_name = "Testing comment, @test.user@example.com please check."
self.assertEqual(extract_mentions(user_name)[0], "test.user@example.com")
user_name = "<div>@test_user@example.com and @test.again@example1.com</div><div>This is a test.</div>"
self.assertEqual(extract_mentions(user_name)[0], "test_user@example.com")
self.assertEqual(extract_mentions(user_name)[1], "test.again@example1.com")
user_name = "<div>@user@example.com</a> Test @test-comment@xyz.com</div><div>Test for comment mentions @test@abc.com</div>"
self.assertEqual(extract_mentions(user_name)[0], "user@example.com")
self.assertEqual(extract_mentions(user_name)[1], "test-comment@xyz.com")
self.assertEqual(extract_mentions(user_name)[2], "test@abc.com")
comment = '''
<span class="mention" data-id="test.comment@example.com" data-value="Test" data-denotation-char="@">
<span><span class="ql-mention-denotation-char">@</span>Test</span>
</span>
'''
self.assertEqual(extract_mentions(comment)[0], "test.comment@example.com")
comment = '''
<div>
Testing comment,
<span class="mention" data-id="test.comment@example.com" data-value="Test" data-denotation-char="@">
<span><span class="ql-mention-denotation-char">@</span>Test</span>
</span>
please check
</div>
'''
self.assertEqual(extract_mentions(comment)[0], "test.comment@example.com")
comment = '''
<div>
Testing comment for
<span class="mention" data-id="test_user@example.com" data-value="Test" data-denotation-char="@">
<span><span class="ql-mention-denotation-char">@</span>Test</span>
</span>
and
<span class="mention" data-id="test.again@example1.com" data-value="Test" data-denotation-char="@">
<span><span class="ql-mention-denotation-char">@</span>Test</span>
</span>
please check
</div>
'''
self.assertEqual(extract_mentions(comment)[0], "test_user@example.com")
self.assertEqual(extract_mentions(comment)[1], "test.again@example1.com")
def delete_contact(user):
frappe.db.sql("DELETE FROM `tabContact` WHERE `email_id`= %s", user)

File diff suppressed because it is too large Load diff

View file

@ -4,11 +4,12 @@
from __future__ import unicode_literals, print_function
import frappe
from frappe.model.document import Document
from frappe.utils import cint, has_gravatar, format_datetime, now_datetime, get_formatted_email
from frappe.utils import cint, has_gravatar, format_datetime, now_datetime, get_formatted_email, today
from frappe import throw, msgprint, _
from frappe.utils.password import update_password as _update_password
from frappe.desk.notifications import clear_notifications
from frappe.utils.user import get_system_managers
from bs4 import BeautifulSoup
import frappe.permissions
import frappe.share
import re
@ -95,7 +96,7 @@ class User(Document):
clear_notifications(user=self.name)
frappe.clear_cache(user=self.name)
self.send_password_notification(self.__new_password)
create_contact(self)
create_contact(self, ignore_mandatory=True)
if self.name not in ('Administrator', 'Guest') and not self.user_image:
frappe.enqueue('frappe.core.doctype.user.user.update_gravatar', name=self.name)
@ -154,7 +155,7 @@ class User(Document):
if new_password and not self.flags.in_insert:
_update_password(user=self.name, pwd=new_password, logout_all_sessions=self.logout_all_sessions)
if self.send_password_update_notification:
if self.send_password_update_notification and self.enabled:
self.password_update_mail(new_password)
frappe.msgprint(_("New password emailed"))
@ -218,13 +219,17 @@ class User(Document):
def validate_reset_password(self):
pass
def reset_password(self, send_email=False):
def reset_password(self, send_email=False, password_expired=False):
from frappe.utils import random_string, get_url
key = random_string(32)
self.db_set("reset_password_key", key)
link = get_url("/update-password?key=" + key)
url = "/update-password?key=" + key
if password_expired:
url = "/update-password?key=" + key + '&password_expired=true'
link = get_url(url)
if send_email:
self.password_reset_mail(link)
@ -591,6 +596,9 @@ def update_password(new_password, logout_all_sessions=0, key=None, old_password=
frappe.local.login_manager.login_as(user)
frappe.db.set_value("User", user,
'last_password_reset_date', today())
if user_doc.user_type == "System User":
return "/desk"
else:
@ -936,11 +944,13 @@ def notify_admin_access_to_system_manager(login_manager=None):
)
def extract_mentions(txt):
"""Find all instances of @name in the string.
The mentions will be separated by non-word characters or may appear at the start of the string"""
txt = txt.replace("<div>", "<div> ")
txt = re.sub(r'(<[a-zA-Z\/][^>]*>)', '', txt)
return re.findall(r'(?:[^\w\.\-\@]|^)@([\w\.\-\@]*)', txt)
"""Find all instances of @mentions in the html."""
soup = BeautifulSoup(txt, 'html.parser')
emails = []
for mention in soup.find_all(class_='mention'):
email = mention['data-id']
emails.append(email)
return emails
def handle_password_test_fail(result):
suggestions = result['feedback']['suggestions'][0] if result['feedback']['suggestions'] else ''

View file

@ -8,6 +8,9 @@ import frappe
import unittest
class TestUserPermission(unittest.TestCase):
def setUp(self):
frappe.db.sql("DELETE FROM `tabUser Permission` WHERE `user`='test_bulk_creation_update@example.com'")
def test_default_user_permission_validation(self):
user = create_user('test_default_permission@example.com')
param = get_params(user, 'User', user.name, is_default=1)
@ -21,39 +24,69 @@ class TestUserPermission(unittest.TestCase):
''' Create User permission for User having access to all applicable Doctypes'''
user = create_user('test_bulk_creation_update@example.com')
param = get_params(user, 'User', user.name)
created = add_user_permissions(param)
self.assertEquals(created, 1)
is_created = add_user_permissions(param)
self.assertEquals(is_created, 1)
def test_for_apply_to_all_on_update_from_apply_all(self):
user = create_user('test_bulk_creation_update@example.com')
param = get_params(user, 'User', user.name)
# Initially create User Permission document with apply_to_all checked
is_created = add_user_permissions(param)
self.assertEquals(is_created, 1)
is_created = add_user_permissions(param)
# User Permission should not be changed
self.assertEquals(is_created, 0)
def test_for_applicable_on_update_from_apply_to_all(self):
''' Update User Permission from all to some applicable Doctypes'''
user = create_user('test_bulk_creation_update@example.com')
param = get_params(user, 'User', user.name , applicable = ["Chat Room", "Chat Message"])
create = add_user_permissions(param)
param = get_params(user,'User', user.name, applicable = ["Chat Room", "Chat Message"])
# Initially create User Permission document with apply_to_all checked
is_created = add_user_permissions(get_params(user, 'User', user.name))
self.assertEquals(is_created, 1)
is_created = add_user_permissions(param)
frappe.db.commit()
removed_apply_to_all = frappe.db.exists("User Permission", get_exists_param(user))
created_applicable_first = frappe.db.exists("User Permission", get_exists_param(user, applicable = "Chat Room"))
created_applicable_second = frappe.db.exists("User Permission", get_exists_param(user, applicable = "Chat Message"))
is_created_applicable_first = frappe.db.exists("User Permission", get_exists_param(user, applicable = "Chat Room"))
is_created_applicable_second = frappe.db.exists("User Permission", get_exists_param(user, applicable = "Chat Message"))
# Check that apply_to_all is removed
self.assertIsNone(removed_apply_to_all)
self.assertIsNotNone(created_applicable_first)
self.assertIsNotNone(created_applicable_second)
self.assertEquals(create, 1)
# Check that User Permissions for applicable is created
self.assertIsNotNone(is_created_applicable_first)
self.assertIsNotNone(is_created_applicable_second)
self.assertEquals(is_created, 1)
def test_for_apply_to_all_on_update_from_applicable(self):
''' Update User Permission from some to all applicable Doctypes'''
user = create_user('test_bulk_creation_update@example.com')
param = get_params(user, 'User', user.name)
created = add_user_permissions(param)
created_apply_to_all = frappe.db.exists("User Permission", get_exists_param(user))
# create User permissions that with applicable
is_created = add_user_permissions(get_params(user, 'User', user.name, applicable = ["Chat Room", "Chat Message"]))
self.assertEquals(is_created, 1)
is_created = add_user_permissions(param)
is_created_apply_to_all = frappe.db.exists("User Permission", get_exists_param(user))
removed_applicable_first = frappe.db.exists("User Permission", get_exists_param(user, applicable = "Chat Room"))
removed_applicable_second = frappe.db.exists("User Permission", get_exists_param(user, applicable = "Chat Message"))
# To check that a User permission with apply_to_all exists
self.assertIsNotNone(is_created_apply_to_all)
self.assertIsNotNone(created_apply_to_all)
# Check that all User Permission with applicable is removed
self.assertIsNone(removed_applicable_first)
self.assertIsNone(removed_applicable_second)
self.assertEquals(created, 1)
self.assertEquals(is_created, 1)
def create_user(email):
''' create user with role system manager '''

View file

@ -182,12 +182,17 @@ def add_user_permissions(data):
data = frappe._dict(data)
d = check_applicable_doc_perm(data.user, data.doctype, data.docname)
exists = frappe.db.exists("User Permission", {"user": data.user, "allow": data.doctype, "for_value": data.docname, "apply_to_all_doctypes": 1})
exists = frappe.db.exists("User Permission", {
"user": data.user,
"allow": data.doctype,
"for_value": data.docname,
"apply_to_all_doctypes": 1
})
if data.apply_to_all_doctypes == 1 and not exists:
remove_applicable(d, data.user, data.doctype, data.docname)
insert_user_perm(data.user, data.doctype, data.docname, data.is_default, apply_to_all = 1)
return 1
else:
elif len(data.applicable_doctypes) > 0 and data.apply_to_all_doctypes != 1:
remove_apply_to_all(data.user, data.doctype, data.docname)
update_applicable(d, data.applicable_doctypes, data.user, data.doctype, data.docname)
for applicable in data.applicable_doctypes :

View file

@ -166,7 +166,7 @@ frappe.listview_settings['User Permission'] = {
return data;
}
if(data.apply_to_all_doctypes == 0 && !("applicable_doctypes" in data)) {
frappe.throw("Please select applicable Doctypes");
frappe.throw(__("Please select applicable Doctypes"));
}
return data;
},

View file

@ -1,17 +1,20 @@
.chart-wrapper {
border: 1px solid #d1d8dd;
border-radius: 4px;
height: 340px;
height: 320px;
margin: 15px 0;
padding-left: 15px;
padding-right: 15px;
}
.chart-container {
padding: 15px;
margin-top: 30px;
}
.chart-container > .title {
.frappe-chart > text.title {
margin: 0px;
font-size: 14px;
font-size: 14px !important;
font-weight: bold;
}
.chart-loading-state {

View file

@ -4,6 +4,7 @@
frappe.provide('frappe.dashboards');
frappe.provide('frappe.dashboards.chart_sources');
frappe.pages['dashboard'].on_page_load = function(wrapper) {
var page = frappe.ui.make_app_page({
parent: wrapper,
@ -226,18 +227,18 @@ class DashboardChart {
"Bar": "bar",
};
let chart_args = {
title: this.chart_doc.chart_name.bold(),
title: this.chart_doc.chart_name,
data: this.data,
type: chart_type_map[this.chart_doc.type],
colors: [this.chart_doc.color || "light-blue"],
axisOptions: {
xIsSeries: this.chart_doc.timeseries
},
}
};
this.chart_container.find('.chart-loading-state').addClass('hide');
if(!this.chart) {
this.chart = new Chart(this.chart_container.find(".chart-wrapper")[0], chart_args);
this.chart = new frappe.Chart(this.chart_container.find(".chart-wrapper")[0], chart_args);
} else {
this.chart.update(this.data);
}

View file

@ -5,8 +5,6 @@ frappe.provide("frappe.customize_form");
frappe.ui.form.on("Customize Form", {
onload: function(frm) {
frappe.customize_form.add_fields_help(frm);
frm.set_query("doc_type", function() {
return {
translate_values: false,
@ -206,103 +204,3 @@ frappe.customize_form.clear_locals_and_refresh = function(frm) {
frm.refresh();
}
frappe.customize_form.add_fields_help = function(frm) {
$(frm.grids[0].parent).before(
'<div style="padding: 10px">\
<a id="fields_help" class="link_type">' + __("Help") + '</a>\
</div>');
$('#fields_help').click(function() {
var d = new frappe.ui.Dialog({
title: __('Help: Field Properties'),
width: 600
});
var help =
"<table cellspacing='25'>\
<tr>\
<td><b>" + __("Label") + "</b></td>\
<td>" + __("Set the display label for the field") + "</td>\
</tr>\
<tr>\
<td><b>" + __("Type") + "</b></td>\
<td>" + __("Change type of field. (Currently, Type change is \
allowed among 'Currency and Float')") + "</td>\
</tr>\
<tr>\
<td width='25%'><b>" + __("Options") + "</b></td>\
<td width='75%'>" + __("Specify the value of the field") + "</td>\
</tr>\
<tr>\
<td><b>" + __("Perm Level") + "</b></td>\
<td>\
" + __("Assign a permission level to the field.") + "<br />\
(" + __("Permissions can be managed via Setup &gt; Role Permissions Manager") + "\
</td>\
</tr>\
<tr>\
<td><b>" + __("Width") + "</b></td>\
<td>\
" + __("Width of the input box") + "<br />\
" + __("Example") + ": <i>120px</i>\
</td>\
</tr>\
<tr>\
<td><b>" + __("Reqd") + "</b></td>\
<td>" + __("Mark the field as Mandatory") + "</td>\
</tr>\
<tr>\
<td><b>" + __("In Filter") + "</b></td>\
<td>" + __("Use the field to filter records") + "</td>\
</tr>\
<tr>\
<td><b>" + __("Hidden") + "</b></td>\
<td>" + __("Hide field in form") + "</td>\
</tr>\
<tr>\
<td><b>" + __("Print Hide") + "</b></td>\
<td>" + __("Hide field in Standard Print Format") + "</td>\
</tr>\
<tr>\
<td><b>" + __("Report Hide") + "</b></td>\
<td>" + __("Hide field in Report Builder") + "</td>\
</tr>\
<tr>\
<td><b>" + __("Allow on Submit") + "</b></td>\
<td>" + __("Allow field to remain editable even after submission") + "</td>\
</tr>\
<tr>\
<td><b>" + __("Depends On") + "</b></td>\
<td>\
Show field if a condition is met<br />\
Example: <code>eval:doc.status=='Cancelled'</code>\
on a field like \"reason_for_cancellation\" will reveal \
\"Reason for Cancellation\" only if the record is Cancelled.\
</td>\
</tr>\
<tr>\
<td><b>" + __("Description") + "</b></td>\
<td>" + __("Show a description below the field") + "</td>\
</tr>\
<tr>\
<td><b>" + __("Default") + "</b></td>\
<td>" + __("Specify a default value") + "</td>\
</tr>\
<tr>\
<td></td>\
<td><a class='link_type' \
onclick='frappe.customize_form.fields_help_dialog.hide()'\
style='color:grey'>" + __("Press Esc to close") + "</a>\
</td>\
</tr>\
</table>"
$y(d.body, {padding: '32px', textAlign: 'center', lineHeight: '200%'});
$a(d.body, 'div', '', {textAlign: 'left'}, help);
d.show();
frappe.customize_form.fields_help_dialog = d;
});
}

View file

@ -4,6 +4,7 @@
"doctype": "DocType",
"document_type": "Document",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"doc_type",
"properties",
@ -16,6 +17,7 @@
"quick_entry",
"track_changes",
"track_views",
"allow_auto_repeat",
"image_view",
"column_break_5",
"title_field",
@ -59,17 +61,20 @@
"label": "Max Attachments"
},
{
"default": "0",
"fieldname": "allow_copy",
"fieldtype": "Check",
"label": "Hide Copy"
},
{
"default": "0",
"fieldname": "istable",
"fieldtype": "Check",
"label": "Is Table",
"read_only": 1
},
{
"default": "0",
"depends_on": "istable",
"fieldname": "editable_grid",
"fieldtype": "Check",
@ -82,11 +87,13 @@
"label": "Quick Entry"
},
{
"default": "0",
"fieldname": "track_changes",
"fieldtype": "Check",
"label": "Track Changes"
},
{
"default": "0",
"depends_on": "eval: doc.image_field",
"fieldname": "image_view",
"fieldtype": "Check",
@ -150,16 +157,23 @@
"options": "Customize Form Field"
},
{
"default": "0",
"fieldname": "track_views",
"fieldtype": "Check",
"label": "Track Views"
},
{
"default": "0",
"fieldname": "allow_auto_repeat",
"fieldtype": "Check",
"label": "Allow Auto Repeat"
}
],
"hide_toolbar": 1,
"icon": "fa fa-glass",
"idx": 1,
"issingle": 1,
"modified": "2019-05-13 18:54:40.610862",
"modified": "2019-07-01 22:50:50.372465",
"modified_by": "Administrator",
"module": "Custom",
"name": "Customize Form",
@ -177,6 +191,7 @@
],
"quick_entry": 1,
"search_fields": "doc_type",
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View file

@ -13,6 +13,7 @@ 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.custom.doctype.custom_field.custom_field import create_custom_field
from frappe.model.docfield import supports_translation
doctype_properties = {
@ -29,6 +30,7 @@ doctype_properties = {
'max_attachments': 'Int',
'track_changes': 'Check',
'track_views': 'Check',
'allow_auto_repeat': 'Check'
}
docfield_properties = {
@ -65,6 +67,7 @@ docfield_properties = {
'columns': 'Int',
'remember_last_selected_value': 'Check',
'allow_bulk_edit': 'Check',
'auto_repeat': 'Link'
}
allowed_fieldtype_change = (('Currency', 'Float', 'Percent'), ('Small Text', 'Data'),
@ -108,6 +111,13 @@ class CustomizeForm(Document):
translation = self.get_name_translation()
self.label = translation.target_name if translation else ''
#If allow_auto_repeat is set, add auto_repeat custom field.
if self.allow_auto_repeat:
if not frappe.db.exists('Custom Field', {'fieldname': 'auto_repeat', 'dt': self.doc_type}):
insert_after = self.fields[len(self.fields) - 1].fieldname
df = dict(fieldname='auto_repeat', label='Auto Repeat', fieldtype='Link', options='Auto Repeat', insert_after=insert_after, read_only=1, no_copy=1, print_hide=1)
create_custom_field(self.doc_type, df)
# NOTE doc is sent to clientside by run_method
def get_name_translation(self):
@ -151,6 +161,7 @@ class CustomizeForm(Document):
return
self.flags.update_db = False
self.flags.rebuild_doctype_for_global_search = False
self.set_property_setters()
self.update_custom_fields()
@ -165,6 +176,10 @@ class CustomizeForm(Document):
frappe.clear_cache(doctype=self.doc_type)
self.fetch_to_customize()
if self.flags.rebuild_doctype_for_global_search:
frappe.enqueue('frappe.utils.global_search.rebuild_for_doctype',
now=True, doctype=self.doc_type)
def set_property_setters(self):
meta = frappe.get_meta(self.doc_type)
# doctype property setters
@ -225,6 +240,10 @@ class CustomizeForm(Document):
frappe.msgprint(_("You can't set 'Translatable' for field {0}").format(df.label))
continue
elif (property == 'in_global_search' and
df.in_global_search != meta_df[0].get("in_global_search")):
self.flags.rebuild_doctype_for_global_search = True
self.make_property_setter(property=property, value=df.get(property),
property_type=docfield_properties[property], fieldname=df.fieldname)

View file

@ -7,6 +7,7 @@ from googleapiclient.errors import HttpError
import time
from datetime import datetime
from frappe.utils import add_days, add_years
from frappe.desk.doctype.event.event import has_permission
class CalendarConnector(BaseConnection):
def __init__(self, connector):
@ -64,24 +65,21 @@ class CalendarConnector(BaseConnection):
def insert(self, doctype, doc):
if doctype == 'Events':
from frappe.desk.doctype.event.event import has_permission
d = frappe.get_doc("Event", doc["name"])
if has_permission(d, self.account.name):
if doc["start_datetime"] >= datetime.now():
try:
doctype = "Event"
e = self.insert_events(doctype, doc)
return e
except Exception:
frappe.log_error(frappe.get_traceback(), "GCalendar Synchronization Error")
try:
doctype = "Event"
e = self.insert_events(doctype, doc)
return e
except Exception:
frappe.log_error(frappe.get_traceback(), "GCalendar Synchronization Error")
def update(self, doctype, doc, migration_id):
if doctype == 'Events':
from frappe.desk.doctype.event.event import has_permission
d = frappe.get_doc("Event", doc["name"])
if has_permission(d, self.account.name):
if doc["start_datetime"] >= datetime.now() and migration_id is not None:
if migration_id is not None:
try:
doctype = "Event"
return self.update_events(doctype, doc, migration_id)
@ -217,23 +215,23 @@ class CalendarConnector(BaseConnection):
day = []
if e.repeat_on == "Every Day":
if e.monday is not None:
if e.monday == 1:
day.append("MO")
if e.tuesday is not None:
if e.tuesday == 1:
day.append("TU")
if e.wednesday is not None:
if e.wednesday == 1:
day.append("WE")
if e.thursday is not None:
if e.thursday == 1:
day.append("TH")
if e.friday is not None:
if e.friday == 1:
day.append("FR")
if e.saturday is not None:
if e.saturday == 1:
day.append("SA")
if e.sunday is not None:
if e.sunday == 1:
day.append("SU")
day = "BYDAY=" + ",".join(str(d) for d in day)
frequency = "FREQ=DAILY"
frequency = "FREQ=WEEKLY"
elif e.repeat_on == "Every Week":
frequency = "FREQ=WEEKLY"

View file

@ -6,7 +6,7 @@ from __future__ import unicode_literals
import frappe, json, math
from frappe.model.document import Document
from frappe import _
from frappe.utils import get_source_value
from frappe.utils import get_source_value, cstr
class DataMigrationRun(Document):
def run(self):
@ -213,19 +213,19 @@ class DataMigrationRun(Document):
def get_deleted_local_data(self):
'''Fetch local deleted data using `frappe.get_all`. Used during Push'''
mapping = self.get_mapping(self.current_mapping)
or_filters = self.get_or_filters(mapping)
filters = dict(
deleted_doctype=mapping.local_doctype
)
filters = self.get_last_modified_condition()
filters.update({
"deleted_doctype": mapping.local_doctype
})
data = frappe.get_all('Deleted Document', fields=['data'],
filters=filters, or_filters=or_filters)
data = frappe.get_all('Deleted Document', fields=['name', 'data'],
filters=filters)
_data = []
for d in data:
doc = json.loads(d.data)
if doc.get(mapping.migration_id_field):
doc['_deleted_document_name'] = d.name
doc['_deleted_document_name'] = d["name"]
_data.append(doc)
return _data
@ -306,8 +306,8 @@ class DataMigrationRun(Document):
self.update_log('push_insert', 1)
# post process after insert
self.post_process_doc(local_doc=d, remote_doc=response_doc)
except Exception:
self.update_log('push_failed', d.name)
except Exception as e:
self.update_log('push_failed', {d.name: cstr(e)})
# update page_start
self.db_set('current_mapping_start',
@ -338,8 +338,8 @@ class DataMigrationRun(Document):
self.update_log('push_update', 1)
# post process after update
self.post_process_doc(local_doc=d, remote_doc=response_doc)
except Exception:
self.update_log('push_failed', d.name)
except Exception as e:
self.update_log('push_failed', {d.name: cstr(e)})
# update page_start
self.db_set('current_mapping_start',
@ -370,8 +370,8 @@ class DataMigrationRun(Document):
self.update_log('push_delete', 1)
# post process only when action is success
self.post_process_doc(local_doc=d, remote_doc=response_doc)
except Exception:
self.update_log('push_failed', d.name)
except Exception as e:
self.update_log('push_failed', {d.name: cstr(e)})
# update page_start
self.db_set('current_mapping_start',
@ -414,7 +414,7 @@ class DataMigrationRun(Document):
self.post_process_doc(remote_doc=d, local_doc=local_doc)
except Exception:
# failed, append to log
self.update_log('pull_failed', migration_id_value)
self.update_log('pull_failed', {migration_id_value: cstr(e)})
if len(data) < mapping.page_length:
# last page, done with pull

View file

@ -178,10 +178,11 @@ class Database(object):
frappe.errprint(("Execution time: {0} sec").format(round(time_end - time_start, 2)))
except Exception as e:
if(frappe.conf.db_type == 'postgres'):
if frappe.conf.db_type == 'postgres':
self.rollback()
if frappe.conf.db_type == 'mariadb' and self.is_syntax_error(e):
elif self.is_syntax_error(e):
# only for mariadb
frappe.errprint('Syntax error in query:')
frappe.errprint(query)
@ -552,6 +553,10 @@ class Database(object):
val = val[0][0] if val else None
df = frappe.get_meta(doctype).get_field(fieldname)
if not df:
frappe.throw(_('Invalid field name: {0}').format(frappe.bold(fieldname)), self.InvalidColumnName)
if df.fieldtype in frappe.model.numeric_fieldtypes:
val = cint(val)
@ -729,6 +734,7 @@ class Database(object):
def commit(self):
"""Commit current transaction. Calls SQL `COMMIT`."""
self.sql("commit")
frappe.local.rollback_observers = []
self.flush_realtime_log()
enqueue_jobs_after_commit()
@ -910,7 +916,7 @@ class Database(object):
return self.is_missing_column(e) or self.is_missing_table(e)
def multisql(self, sql_dict, values=(), **kwargs):
current_dialect = frappe.conf.db_type or 'mariadb'
current_dialect = frappe.db.db_type or 'mariadb'
query = sql_dict.get(current_dialect)
return self.sql(query, values, **kwargs)
@ -922,18 +928,32 @@ class Database(object):
conditions=conditions
), values)
else:
frappe.throw('No conditions provided')
frappe.throw(_('No conditions provided'))
def log_touched_tables(self, query, values=None):
if values:
query = frappe.safe_decode(self._cursor.mogrify(query, values))
if query.strip().lower().split()[0] in ('insert', 'delete', 'update', 'alter'):
# ([`\"']?) Captures ', " or ` at the begining of the table name (if provided)
# single_word_regex is designed to match following patterns
# `tabXxx`, tabXxx and "tabXxx"
# multi_word_regex is designed to match following patterns
# `tabXxx Xxx` and "tabXxx Xxx"
# ([`"]?) Captures " or ` at the begining of the table name (if provided)
# \1 matches the first captured group (quote character) at the end of the table name
# multi word table name must have surrounding quotes.
# (tab([A-Z]\w+)( [A-Z]\w+)*) Captures table names that start with "tab"
# and are continued with multiple words that start with a captital letter
# e.g. 'tabXxx' or 'tabXxx Xxx' or 'tabXxx Xxx Xxx' and so on
# \1 matches the first captured group (quote character) at the end of the table name
tables = [groups[1] for groups in re.findall(r'([`"\']?)(tab([A-Z]\w+)( [A-Z]\w+)*)\1', query)]
single_word_regex = r'([`"]?)(tab([A-Z]\w+))\1'
multi_word_regex = r'([`"])(tab([A-Z]\w+)( [A-Z]\w+)+)\1'
tables = []
for regex in (single_word_regex, multi_word_regex):
tables += [groups[1] for groups in re.findall(regex, query)]
if frappe.flags.touched_tables is None:
frappe.flags.touched_tables = set()
frappe.flags.touched_tables.update(tables)

View file

@ -25,6 +25,7 @@ class MariaDBDatabase(Database):
REGEX_CHARACTER = 'regexp'
def setup_type_map(self):
self.db_type = 'mariadb'
self.type_map = {
'Currency': ('decimal', '18,6'),
'Int': ('int', '11'),

View file

@ -37,6 +37,7 @@ CREATE TABLE `tabDocField` (
`unique` int(1) NOT NULL DEFAULT 0,
`no_copy` int(1) NOT NULL DEFAULT 0,
`allow_on_submit` int(1) NOT NULL DEFAULT 0,
`show_preview_popup` int(1) NOT NULL DEFAULT 0,
`trigger` varchar(255) DEFAULT NULL,
`collapsible_depends_on` text,
`depends_on` text,

View file

@ -30,6 +30,7 @@ class PostgresDatabase(Database):
REGEX_CHARACTER = '~'
def setup_type_map(self):
self.db_type = 'postgres'
self.type_map = {
'Currency': ('decimal', '18,6'),
'Int': ('bigint', None),
@ -110,13 +111,10 @@ class PostgresDatabase(Database):
def format_date(self, date):
if not date:
return '0001-01-01::DATE'
return '0001-01-01'
if isinstance(date, frappe.string_types):
if ':' not in date:
date = date + '::DATE'
else:
date = date.strftime('%Y-%m-%d') + '::DATE'
if not isinstance(date, frappe.string_types):
date = date.strftime('%Y-%m-%d')
return date

View file

@ -37,6 +37,7 @@ CREATE TABLE "tabDocField" (
"unique" smallint NOT NULL DEFAULT 0,
"no_copy" smallint NOT NULL DEFAULT 0,
"allow_on_submit" smallint NOT NULL DEFAULT 0,
"show_preview_popup" smallint NOT NULL DEFAULT 0,
"trigger" varchar(255) DEFAULT NULL,
"collapsible_depends_on" text,
"depends_on" text,

View file

@ -40,7 +40,20 @@ class PostgresTable(DBTable):
query.append("ADD COLUMN `{}` {}".format(col.fieldname, col.get_definition()))
for col in self.change_type:
query.append("ALTER COLUMN `{}` TYPE {}".format(col.fieldname, get_definition(col.fieldtype, precision=col.precision, length=col.length)))
using_clause = ""
if col.fieldtype in ("Datetime"):
# The USING option of SET DATA TYPE can actually specify any expression
# involving the old values of the row
# read more https://www.postgresql.org/docs/9.1/sql-altertable.html
using_clause = "USING {}::timestamp without time zone".format(col.fieldname)
elif col.fieldtype in ("Check"):
using_clause = "USING {}::smallint".format(col.fieldname)
query.append("ALTER COLUMN {0} TYPE {1} {2}".format(
col.fieldname,
get_definition(col.fieldtype, precision=col.precision, length=col.length),
using_clause)
)
for col in self.set_default:
if col.fieldname=="name":
@ -93,4 +106,4 @@ class PostgresTable(DBTable):
fieldname, self.table_name)))
raise e
else:
raise e
raise e

View file

@ -17,7 +17,7 @@ def setup_database(force, source_sql, verbose):
subprocess_env['PGPASSWORD'] = str(frappe.conf.db_password)
# bootstrap db
subprocess.check_output([
'psql', frappe.conf.db_name, '-h', 'localhost', '-U',
'psql', frappe.conf.db_name, '-h', frappe.conf.db_host, '-U',
frappe.conf.db_name, '-f',
os.path.join(os.path.dirname(__file__), 'framework_postgres.sql')
], env=subprocess_env)

View file

@ -1,143 +0,0 @@
// Copyright (c) 2018, Frappe Technologies and contributors
// For license information, please see license.txt
frappe.provide("frappe.auto_repeat");
frappe.ui.form.on('Auto Repeat', {
setup: function(frm) {
frm.fields_dict['reference_doctype'].get_query = function() {
return {
query: "frappe.desk.doctype.auto_repeat.auto_repeat.auto_repeat_doctype_query"
};
};
frm.fields_dict['reference_document'].get_query = function() {
return {
filters: {
"docstatus": 1,
"auto_repeat": ''
}
};
};
frm.fields_dict['print_format'].get_query = function() {
return {
filters: {
"doc_type": frm.doc.reference_doctype
}
};
};
},
refresh: function(frm) {
if(frm.doc.docstatus == 1) {
let label = __('View {0}', [__(frm.doc.reference_doctype)]);
frm.add_custom_button(__(label),
function() {
frappe.route_options = {
"auto_repeat": frm.doc.name,
};
frappe.set_route("List", frm.doc.reference_doctype);
}
);
if(frm.doc.status != 'Stopped') {
frm.add_custom_button(__("Stop"),
function() {
frm.events.stop_resume_auto_repeat(frm, "Stopped");
}
);
}
if(frm.doc.status == 'Stopped') {
frm.add_custom_button(__("Restart"),
function() {
frm.events.stop_resume_auto_repeat(frm, "Resumed");
}
);
}
if(frm.doc.docstatus!= 0 && !frm.doc.status.includes('Stopped', 'Cancelled') && frm.doc.next_schedule_date >= frappe.datetime.get_today()){
frappe.auto_repeat.render_schedule(frm);
}
}
},
stop_resume_auto_repeat: function(frm, status) {
frappe.call({
method: "frappe.desk.doctype.auto_repeat.auto_repeat.stop_resume_auto_repeat",
args: {
auto_repeat: frm.doc.name,
status: status
},
callback: function(r) {
if(r.message) {
frm.set_value("status", r.message);
frm.reload_doc();
}
}
});
},
template: function(frm) {
if (frm.doc.template) {
frappe.model.with_doc("Email Template", frm.doc.template, () => {
let email_template = frappe.get_doc("Email Template", frm.doc.template);
frm.set_value("subject", email_template.subject);
frm.set_value("message", email_template.response);
frm.refresh_field("subject");
frm.refresh_field("message");
});
}
},
get_contacts: function(frm) {
frappe.call({
method: "frappe.desk.doctype.auto_repeat.auto_repeat.get_contacts",
args: {
reference_doctype: frm.doc.reference_doctype,
reference_name: frm.doc.reference_document
},
callback: function(r) {
if(r.message) {
frm.set_value("recipients", r.message.join());
frm.refresh_field("recipients");
}
}
});
},
preview_message: function(frm) {
if (frm.doc.message) {
frappe.call({
method: "frappe.desk.doctype.auto_repeat.auto_repeat.generate_message_preview",
args: {
reference_dt: frm.doc.reference_doctype,
reference_doc: frm.doc.reference_document,
subject: frm.doc.subject,
message: frm.doc.message
},
callback: function(r) {
if(r.message) {
frappe.msgprint(r.message.message, r.message.subject)
}
}
});
} else {
frappe.msgprint(__("Please setup a message first"), __("Message not setup"))
}
}
});
frappe.auto_repeat.render_schedule = function(frm) {
frappe.call({
method: "get_auto_repeat_schedule",
doc: frm.doc
}).done((r) => {
var wrapper = $(frm.fields_dict["auto_repeat_schedule"].wrapper);
wrapper.html(frappe.render_template ("auto_repeat_schedule", {"schedule_details" : r.message || []} ));
});
frm.refresh_fields() ;
};

File diff suppressed because it is too large Load diff

View file

@ -1,408 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
import calendar
from frappe import _
from frappe.desk.form import assign_to
from frappe.utils.jinja import validate_template
from dateutil.relativedelta import relativedelta
from frappe.utils.user import get_system_managers
from frappe.utils import cstr, getdate, split_emails, add_days, today, get_last_day, get_first_day
from frappe.model.document import Document
from frappe.core.doctype.communication.email import make
from frappe.utils.background_jobs import get_jobs
month_map = {'Monthly': 1, 'Quarterly': 3, 'Half-yearly': 6, 'Yearly': 12}
class AutoRepeat(Document):
def onload(self):
self.set_onload("auto_repeat_schedule", self.get_auto_repeat_schedule())
def validate(self):
self.update_status()
self.validate_reference_doctype()
self.validate_dates()
self.validate_next_schedule_date()
self.validate_email_id()
self.link_party()
validate_template(self.subject or "")
validate_template(self.message or "")
def before_submit(self):
if not self.next_schedule_date:
self.next_schedule_date = get_next_schedule_date(
self.start_date, self.frequency, self.repeat_on_day)
def on_submit(self):
self.update_auto_repeat_id()
def on_update_after_submit(self):
self.validate_dates()
self.set_next_schedule_date()
def before_cancel(self):
self.unlink_auto_repeat_id()
self.next_schedule_date = None
def unlink_auto_repeat_id(self):
frappe.db.sql(
"update `tab{0}` set auto_repeat = null where auto_repeat=%s".format(self.reference_doctype), self.name)
def validate_reference_doctype(self):
if not frappe.get_meta(self.reference_doctype).has_field('auto_repeat'):
frappe.throw(_("Add custom field Auto Repeat in the doctype {0}").format(self.reference_doctype))
def validate_dates(self):
if self.end_date and getdate(self.start_date) > getdate(self.end_date):
frappe.throw(_("End date must be greater than start date"))
def validate_next_schedule_date(self):
if self.repeat_on_day and self.next_schedule_date:
next_date = getdate(self.next_schedule_date)
if next_date.day != self.repeat_on_day:
# if the repeat day is the last day of the month (31)
# and the current month does not have as many days,
# then the last day of the current month is a valid date
lastday = calendar.monthrange(next_date.year, next_date.month)[1]
if self.repeat_on_day < lastday:
# the specified day of the month is not same as the day specified
# or the last day of the month
frappe.throw(_("Next Date's day and Repeat on Day of Month must be equal"))
def validate_email_id(self):
if self.notify_by_email:
if self.recipients:
email_list = split_emails(self.recipients.replace("\n", ""))
from frappe.utils import validate_email_address
for email in email_list:
if not validate_email_address(email):
frappe.throw(_("{0} is an invalid email address in 'Recipients'").format(email))
else:
frappe.throw(_("'Recipients' not specified"))
def set_next_schedule_date(self):
if self.repeat_on_day:
self.next_schedule_date = get_next_date(self.next_schedule_date, 0, self.repeat_on_day)
def update_auto_repeat_id(self):
frappe.db.set_value(self.reference_doctype, self.reference_document, "auto_repeat", self.name)
def update_status(self, status=None):
self.status = {
'0': 'Draft',
'1': 'Submitted',
'2': 'Cancelled'
}[cstr(self.docstatus or 0)]
if status and status != 'Resumed':
self.status = status
def get_auto_repeat_schedule(self):
schedule_details = []
start_date_copy = getdate(self.start_date)
end_date_copy = getdate(self.end_date)
today_copy = frappe.utils.datetime.date.today()
if start_date_copy < today_copy:
start_date_copy = today_copy
if not self.end_date:
days = 60 if self.frequency in ['Daily', 'Weekly'] else 365
end_date_copy = add_days(today_copy, days)
while (getdate(start_date_copy) < getdate(end_date_copy)):
start_date_copy = get_next_schedule_date(start_date_copy, self.frequency, self.repeat_on_day)
row = {
"reference_document" : self.reference_document,
"frequency" : self.frequency,
"next_scheduled_date" : start_date_copy
}
schedule_details.append(row)
return schedule_details
def link_party(self):
reference = frappe.get_meta(self.reference_doctype)
for field in reference.fields:
if field.options in ['Customer', 'Supplier', 'Employee']:
self.reference_party_doctype = field.options
self.reference_party = frappe.db.get_value(self.reference_doctype, self.reference_document, field.fieldname)
break
def get_next_schedule_date(start_date, frequency, repeat_on_day):
mcount = month_map.get(frequency)
if mcount:
next_date = get_next_date(start_date, mcount, repeat_on_day)
else:
days = 7 if frequency == 'Weekly' else 1
next_date = add_days(start_date, days)
return next_date
def make_auto_repeat_entry(date=None):
enqueued_method = 'frappe.desk.doctype.auto_repeat.auto_repeat.create_repeated_entries'
jobs = get_jobs()
if not jobs or enqueued_method not in jobs[frappe.local.site]:
date = date or today()
for data in get_auto_repeat_entries(date):
frappe.enqueue(enqueued_method, data=data)
def create_repeated_entries(data):
schedule_date = getdate(data.next_schedule_date)
while schedule_date <= getdate(today()) and not frappe.db.get_value('Auto Repeat', data.name, 'disabled'):
create_documents(data, schedule_date)
schedule_date = get_next_schedule_date(schedule_date, data.frequency, data.repeat_on_day)
if schedule_date and not frappe.db.get_value('Auto Repeat', data.name, 'disabled'):
frappe.db.set_value('Auto Repeat', data.name, 'next_schedule_date', schedule_date)
frappe.db.commit()
def get_auto_repeat_entries(date):
return frappe.db.sql(""" select * from `tabAuto Repeat`
where docstatus = 1 and next_schedule_date <=%s
and reference_document is not null and reference_document != ''
and next_schedule_date <= ifnull(end_date, '2199-12-31')
and disabled = 0 and status != 'Stopped' """, (date), as_dict=1)
def create_documents(data, schedule_date):
try:
doc = make_new_document(data, schedule_date)
if data.notify_by_email and data.recipients:
print_format = data.print_format or "Standard"
send_notification(doc, data, print_format=print_format)
frappe.db.commit()
except Exception:
frappe.db.rollback()
frappe.db.begin()
frappe.log_error(frappe.get_traceback(), _("Recurring document creation failure"))
disable_auto_repeat(data)
frappe.db.commit()
if data.reference_document and not frappe.flags.in_test:
notify_error_to_user(data)
def disable_auto_repeat(data):
auto_repeat = frappe.get_doc('Auto Repeat', data.name)
auto_repeat.db_set('disabled', 1)
def notify_error_to_user(data):
party = ''
party_type = ''
if data.reference_doctype in ['Sales Order', 'Sales Invoice', 'Delivery Note']:
party_type = 'customer'
elif data.reference_doctype in ['Purchase Order', 'Purchase Invoice', 'Purchase Receipt']:
party_type = 'supplier'
if party_type:
party = frappe.db.get_value(data.reference_doctype, data.reference_document, party_type)
notify_errors(data.reference_document, data.reference_doctype, party, data.owner, data.name)
def make_new_document(args, schedule_date):
doc = frappe.get_doc(args.reference_doctype, args.reference_document)
new_doc = frappe.copy_doc(doc, ignore_no_copy=False)
update_doc(new_doc, doc, args, schedule_date)
new_doc.insert(ignore_permissions=True)
if args.submit_on_creation:
new_doc.submit()
return new_doc
def update_doc(new_document, reference_doc, args, schedule_date):
new_document.docstatus = 0
if new_document.meta.get_field('set_posting_time'):
new_document.set('set_posting_time', 1)
mcount = month_map.get(args.frequency)
if new_document.meta.get_field('auto_repeat'):
new_document.set('auto_repeat', args.name)
for fieldname in ['naming_series', 'ignore_pricing_rule', 'posting_time',
'select_print_heading', 'remarks', 'owner']:
if new_document.meta.get_field(fieldname):
new_document.set(fieldname, reference_doc.get(fieldname))
# copy item fields
if new_document.meta.get_field('items'):
for i, item in enumerate(new_document.items):
for fieldname in ("page_break",):
item.set(fieldname, reference_doc.items[i].get(fieldname))
for data in new_document.meta.fields:
if data.fieldtype == 'Date' and data.reqd:
new_document.set(data.fieldname, schedule_date)
set_auto_repeat_period(args, mcount, new_document)
new_document.run_method("on_recurring", reference_doc=reference_doc, auto_repeat_doc=args)
def set_auto_repeat_period(args, mcount, new_document):
if mcount and new_document.meta.get_field('from_date') and new_document.meta.get_field('to_date'):
last_ref_doc = frappe.db.sql("""
select name, from_date, to_date
from `tab{0}`
where auto_repeat=%s and docstatus < 2
order by creation desc
limit 1
""".format(args.reference_doctype), args.name, as_dict=1)
if not last_ref_doc:
return
from_date = get_next_date(last_ref_doc[0].from_date, mcount)
if (cstr(get_first_day(last_ref_doc[0].from_date)) == cstr(last_ref_doc[0].from_date)) and \
(cstr(get_last_day(last_ref_doc[0].to_date)) == cstr(last_ref_doc[0].to_date)):
to_date = get_last_day(get_next_date(last_ref_doc[0].to_date, mcount))
else:
to_date = get_next_date(last_ref_doc[0].to_date, mcount)
new_document.set('from_date', from_date)
new_document.set('to_date', to_date)
def get_next_date(dt, mcount, day=None):
dt = getdate(dt)
dt += relativedelta(months=mcount, day=day)
return dt
def send_notification(new_rv, auto_repeat_doc, print_format='Standard'):
"""Notify concerned persons about recurring document generation"""
print_format = print_format
subject = auto_repeat_doc.subject or ''
message = auto_repeat_doc.message or ''
if not auto_repeat_doc.subject:
subject = _("New {0}: #{1}").format(new_rv.doctype, new_rv.name)
elif "{" in auto_repeat_doc.subject:
subject = frappe.render_template(auto_repeat_doc.subject, {'doc': new_rv})
if not auto_repeat_doc.message:
message = _("Please find attached {0} #{1}").format(new_rv.doctype, new_rv.name)
elif "{" in auto_repeat_doc.message:
message = frappe.render_template(auto_repeat_doc.message, {'doc': new_rv})
attachments = [frappe.attach_print(new_rv.doctype, new_rv.name,
file_name=new_rv.name, print_format=print_format)]
make(doctype=new_rv.doctype, name=new_rv.name, recipients=auto_repeat_doc.recipients,
subject=subject, content=message, attachments=attachments, send_email=1)
def notify_errors(doc, doctype, party, owner, name):
recipients = get_system_managers(only_name=True)
frappe.sendmail(recipients + [frappe.db.get_value("User", owner, "email")],
subject=_("[Urgent] Error while creating recurring %s for %s" % (doctype, doc)),
message=frappe.get_template("templates/emails/recurring_document_failed.html").render({
"type": _(doctype),
"name": doc,
"party": party or "",
"auto_repeat": name
}))
try:
assign_task_to_owner(name, _("Recurring Documents Failed"), recipients)
except Exception:
frappe.log_error(frappe.get_traceback(), _("Recurring Documents Failed"))
def assign_task_to_owner(name, msg, users):
for d in users:
args = {
'doctype': 'Auto Repeat',
'assign_to': d,
'name': name,
'description': msg,
'priority': 'High'
}
assign_to.add(args)
@frappe.whitelist()
def make_auto_repeat(doctype, docname):
doc = frappe.new_doc('Auto Repeat')
reference_doc = frappe.get_doc(doctype, docname)
doc.reference_doctype = doctype
doc.reference_document = docname
doc.start_date = reference_doc.get('posting_date') or reference_doc.get('transaction_date')
return doc
@frappe.whitelist()
def stop_resume_auto_repeat(auto_repeat, status):
doc = frappe.get_doc('Auto Repeat', auto_repeat)
frappe.msgprint(_("Auto Repeat has been {0}").format(status))
if status == 'Resumed':
doc.next_schedule_date = get_next_schedule_date(today(),
doc.frequency, doc.repeat_on_day)
doc.update_status(status)
doc.save()
return doc.status
def auto_repeat_doctype_query(doctype, txt, searchfield, start, page_len, filters):
return frappe.db.sql("""select parent from `tabDocField`
where fieldname = 'auto_repeat'
and parent like %(txt)s
order by
if(locate(%(_txt)s, parent), locate(%(_txt)s, parent), 99999),
parent
limit %(start)s, %(page_len)s""".format(**{
'key': searchfield,
}), {
'txt': "%%%s%%" % txt,
'_txt': txt.replace("%", ""),
'start': start,
'page_len': page_len
})
@frappe.whitelist()
def get_contacts(reference_doctype, reference_name):
docfields = frappe.get_meta(reference_doctype).fields
contact_fields = []
for field in docfields:
if field.fieldtype == "Link" and field.options == "Contact":
contact_fields.append(field.fieldname)
if contact_fields:
contacts = []
for contact_field in contact_fields:
contacts.append(frappe.db.get_value(reference_doctype, reference_name, contact_field))
else:
return []
if contacts:
emails = []
for contact in contacts:
emails.append(frappe.db.get_value("Contact", contact, "email_id"))
return emails
else:
return []
@frappe.whitelist()
def update_reference(docname, reference):
try:
frappe.db.set_value("Auto Repeat", docname, "reference_document", reference)
return "success"
except Exception as e:
raise e
return "error"
@frappe.whitelist()
def generate_message_preview(reference_dt, reference_doc, message=None, subject=None):
doc = frappe.get_doc(reference_dt, reference_doc)
subject_preview = _("Please add a subject to your email")
msg_preview = frappe.render_template(message, {'doc': doc})
if subject:
subject_preview = frappe.render_template(subject, {'doc': doc})
return {'message': msg_preview, 'subject': subject_preview}

View file

@ -1,16 +0,0 @@
frappe.listview_settings['Auto Repeat'] = {
add_fields: ["next_schedule_date"],
get_indicator: function(doc) {
if(doc.disabled) {
return [__("Disabled"), "red"];
} else if(doc.next_schedule_date >= frappe.datetime.get_today() && doc.status != 'Stopped') {
return [__("Active"), "green"];
} else if(doc.docstatus === 0) {
return [__("Draft"), "red", "docstatus,=,0"];
} else if(doc.status === 'Stopped') {
return [__("Stopped"), "red"];
} else {
return [__("Expired"), "darkgrey"];
}
}
};

View file

@ -100,6 +100,7 @@ frappe.ui.form.on('Dashboard Chart', {
// nothing is mandatory
_df.reqd = 0;
_df.default = null;
_df.depends_on = null;
_df.read_only = 0;
_df.permlevel = 1;
_df.hidden = 0;

View file

@ -4,8 +4,9 @@
from __future__ import unicode_literals
import frappe, json
from frappe import _
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
from frappe.utils import nowdate, add_to_date, getdate, get_last_day, formatdate
from frappe.model.document import Document
@frappe.whitelist()
@ -59,7 +60,7 @@ def get(chart_name, from_date=None, to_date=None, refresh = None):
result = add_missing_values(result, timegrain, from_date, to_date)
return {
"labels": [r[0].strftime('%Y-%m-%d') for r in result],
"labels": [formatdate(r[0].strftime('%Y-%m-%d')) for r in result],
"datasets": [{
"name": chart.name,
"values": [r[1] for r in result]
@ -85,7 +86,7 @@ def convert_to_dates(data, timegrain):
def get_unit_function(datefield, timegrain):
unit_function = ''
if timegrain=='Daily':
if frappe.conf.db_type == 'mariadb':
if frappe.db.db_type == 'mariadb':
unit_function = 'dayofyear({})'.format(datefield)
else:
unit_function = 'extract(doy from {datefield})'.format(
@ -193,3 +194,12 @@ class DashboardChart(Document):
def on_update(self):
frappe.cache().delete_key('chart-data:{}'.format(self.name))
def validate(self):
if self.chart_type != 'Custom':
self.check_required_field()
def check_required_field(self):
if not self.based_on:
frappe.throw(_("Time series based on is required to create a dashboard chart"))
if not self.document_type:
frappe.throw(_("Document type is required to create a dashboard chart"))

View file

@ -4,7 +4,7 @@
from __future__ import unicode_literals
import unittest, frappe
from frappe.utils import getdate
from frappe.utils import getdate, formatdate
from frappe.desk.doctype.dashboard_chart.dashboard_chart import (get,
get_period_ending)
@ -56,7 +56,8 @@ class TestDashboardChart(unittest.TestCase):
result = get(chart_name ='Test Dashboard Chart', refresh = 1)
for idx in range(13):
month = str(cur_date.year) + '-' + str(cur_date.strftime('%m')) + '-' + str(calendar.monthrange(cur_date.year, cur_date.month)[1])
month = datetime(int(cur_date.year), int(cur_date.strftime('%m')), int(calendar.monthrange(cur_date.year, cur_date.month)[1]))
month = formatdate(month.strftime('%Y-%m-%d'))
self.assertEqual(result.get('labels')[idx], month)
cur_date += relativedelta(months=1)
@ -87,7 +88,8 @@ class TestDashboardChart(unittest.TestCase):
result = get(chart_name ='Test Empty Dashboard Chart', refresh = 1)
for idx in range(13):
month = str(cur_date.year) + '-' + str(cur_date.strftime('%m')) + '-' + str(calendar.monthrange(cur_date.year, cur_date.month)[1])
month = datetime(int(cur_date.year), int(cur_date.strftime('%m')), int(calendar.monthrange(cur_date.year, cur_date.month)[1]))
month = formatdate(month.strftime('%Y-%m-%d'))
self.assertEqual(result.get('labels')[idx], month)
cur_date += relativedelta(months=1)
@ -118,7 +120,8 @@ class TestDashboardChart(unittest.TestCase):
result = get(chart_name ='Test Empty Dashboard Chart 2', refresh = 1)
for idx in range(13):
month = str(cur_date.year) + '-' + str(cur_date.strftime('%m')) + '-' + str(calendar.monthrange(cur_date.year, cur_date.month)[1])
month = datetime(int(cur_date.year), int(cur_date.strftime('%m')), int(calendar.monthrange(cur_date.year, cur_date.month)[1]))
month = formatdate(month.strftime('%Y-%m-%d'))
self.assertEqual(result.get('labels')[idx], month)
cur_date += relativedelta(months=1)

View file

@ -4,6 +4,7 @@
from __future__ import unicode_literals
import frappe, os
from frappe import _
from frappe.model.document import Document
from frappe.modules.export_file import export_to_files
from frappe.modules import get_module_path, scrub

View file

@ -43,10 +43,17 @@ class Event(Document):
def sync_communication(self):
if self.event_participants:
for participant in self.event_participants:
communication_name = frappe.db.get_value("Communication", dict(reference_doctype=self.doctype, reference_name=self.name, timeline_doctype=participant.reference_doctype, timeline_name=participant.reference_docname), "name")
if communication_name:
communication = frappe.get_doc("Communication", communication_name)
self.update_communication(participant, communication)
comms = frappe.get_list("Communication", filters=[
["Communication", "reference_doctype", "=", self.doctype],
["Communication", "reference_name", "=", self.name],
["Communication Link", "link_doctype", "=", participant.reference_doctype],
["Communication Link", "link_name", "=", participant.reference_docname]
], fields=["name"])
if comms:
for comm in comms:
communication = frappe.get_doc("Communication", comm.name)
self.update_communication(participant, communication)
else:
meta = frappe.get_meta(participant.reference_doctype)
if hasattr(meta, "allow_events_in_timeline") and meta.allow_events_in_timeline==1:
@ -62,12 +69,11 @@ class Event(Document):
communication.subject = self.subject
communication.content = self.description if self.description else self.subject
communication.communication_date = self.starts_on
communication.timeline_doctype = participant.reference_doctype
communication.timeline_name = participant.reference_docname
communication.reference_doctype = self.doctype
communication.reference_name = self.name
communication.communication_medium = communication_mapping[self.event_category] if self.event_category else ""
communication.communication_medium = communication_mapping.get(self.event_category) if self.event_category else ""
communication.status = "Linked"
communication.add_link(participant.reference_doctype, participant.reference_docname)
communication.save(ignore_permissions=True)
@frappe.whitelist()
@ -76,9 +82,18 @@ def delete_communication(event, reference_doctype, reference_docname):
if isinstance(event, string_types):
event = json.loads(event)
communication_name = frappe.db.get_value("Communication", dict(reference_doctype=event["doctype"], reference_name=event["name"], timeline_doctype=deleted_participant.reference_doctype, timeline_name=deleted_participant.reference_docname), "name")
if communication_name:
deletion = frappe.get_doc("Communication", communication_name).delete()
comms = frappe.get_list("Communication", filters=[
["Communication", "reference_doctype", "=", event.get("doctype")],
["Communication", "reference_name", "=", event.get("name")],
["Communication Link", "link_doctype", "=", deleted_participant.reference_doctype],
["Communication Link", "link_name", "=", deleted_participant.reference_docname]
], fields=["name"])
if comms:
deletion = []
for comm in comms:
delete = frappe.get_doc("Communication", comm.name).delete()
deletion.append(delete)
return deletion

View file

@ -684,4 +684,4 @@
"track_changes": 1,
"track_seen": 1,
"track_views": 0
}
}

View file

@ -43,8 +43,11 @@ class ToDo(Document):
def on_trash(self):
# unlink todo from linked comments
frappe.db.sql("""update `tabCommunication` set link_doctype=null, link_name=null
where link_doctype=%(doctype)s and link_name=%(name)s""", {"doctype": self.doctype, "name": self.name})
frappe.db.sql("""
delete from `tabCommunication Link`
where link_doctype=%(doctype)s and link_name=%(name)s""", {
"doctype": self.doctype, "name": self.name
})
self.update_in_reference()
@ -94,7 +97,7 @@ def get_permission_query_conditions(user):
if "System Manager" in frappe.get_roles(user):
return None
else:
return """(tabToDo.owner = {user} or tabToDo.assigned_by = {user})"""\
return """(`tabToDo`.owner = {user} or `tabToDo`.assigned_by = {user})"""\
.format(user=frappe.db.escape(user))
def has_permission(doc, user):
@ -108,4 +111,4 @@ def new_todo(description):
frappe.get_doc({
'doctype': 'ToDo',
'description': description
}).insert()
}).insert()

View file

@ -160,36 +160,59 @@ def get_communication_data(doctype, name, start=0, limit=20, after=None, fields=
group_by=None, as_dict=True):
'''Returns list of communications for a given document'''
if not fields:
fields = '''`name`, `communication_type`,`communication_medium`, `comment_type`,
`communication_date`, `content`, `sender`, `sender_full_name`, `cc`, `bcc`,
`creation`, `subject`, `delivery_status`, `_liked_by`,
`timeline_doctype`, `timeline_name`, `reference_doctype`, `reference_name`,
`link_doctype`, `link_name`, `read_by_recipient`, `rating`, 'Communication' AS `doctype`'''
conditions = '''communication_type = 'Communication'
and (
(reference_doctype=%(doctype)s and reference_name=%(name)s)
or (
(timeline_doctype=%(doctype)s and timeline_name=%(name)s)
and (communication_type='Communication')
)
)'''
fields = '''
C.name, C.communication_type, C.communication_medium,
C.comment_type, C.communication_date, C.content,
C.sender, C.sender_full_name, C.cc, C.bcc,
C.creation AS creation, C.subject, C.delivery_status,
C._liked_by, C.reference_doctype, C.reference_name,
C.read_by_recipient, C.rating
'''
conditions = ''
if after:
# find after a particular date
conditions+= ' and creation > {0}'.format(after)
conditions += '''
AND C.creation > {0}
'''.format(after)
if doctype=='User':
conditions+= " and not (reference_doctype='User' and communication_type='Communication')"
conditions += '''
AND NOT (C.reference_doctype='User' AND C.communication_type='Communication')
'''
communications = frappe.db.sql("""select {fields}
from `tabCommunication`
where {conditions} {group_by}
order by creation desc LIMIT %(limit)s OFFSET %(start)s""".format(
fields = fields, conditions=conditions, group_by=group_by or ""),
{ "doctype": doctype, "name": name, "start": frappe.utils.cint(start), "limit": limit },
as_dict=as_dict)
# communications linked to reference_doctype
part1 = '''
SELECT {fields}
FROM `tabCommunication` as C
WHERE C.communication_type IN ('Communication', 'Feedback')
AND (C.reference_doctype = %(doctype)s AND C.reference_name = %(name)s)
{conditions}
'''.format(fields=fields, conditions=conditions)
# communications linked in Timeline Links
part2 = '''
SELECT {fields}
FROM `tabCommunication` as C
INNER JOIN `tabCommunication Link` ON C.name=`tabCommunication Link`.parent
WHERE C.communication_type IN ('Communication', 'Feedback')
AND `tabCommunication Link`.link_doctype = %(doctype)s AND `tabCommunication Link`.link_name = %(name)s
{conditions}
'''.format(fields=fields, conditions=conditions)
communications = frappe.db.sql('''
SELECT *
FROM (({part1}) UNION ({part2})) AS combined
{group_by}
ORDER BY creation DESC
LIMIT %(limit)s
OFFSET %(start)s
'''.format(part1=part1, part2=part2, group_by=(group_by or '')), dict(
doctype=doctype,
name=name,
start=frappe.utils.cint(start),
limit=limit
), as_dict=as_dict)
return communications
@ -229,4 +252,4 @@ def get_view_logs(doctype, docname):
if view_logs:
logs = view_logs
return logs
return logs

View file

@ -5,7 +5,7 @@ from __future__ import unicode_literals
import frappe, json
import frappe.desk.form.meta
import frappe.desk.form.load
from frappe.utils.html_utils import clean_email_html
from frappe.utils.html_utils import sanitize_html
from frappe.desk.form.document_follow import follow_document
from frappe import _
@ -18,7 +18,6 @@ def remove_attach():
file_name = frappe.form_dict.get('file_name')
frappe.delete_doc('File', fid)
@frappe.whitelist()
def validate_link():
"""validate link when updated by user"""
@ -64,7 +63,7 @@ def add_comment(reference_doctype, reference_name, content, comment_email):
doctype = 'Comment',
reference_doctype = reference_doctype,
reference_name = reference_name,
content = clean_email_html(content),
content = sanitize_html(content),
comment_email = comment_email,
comment_type = 'Comment'
)).insert(ignore_permissions = True)
@ -84,27 +83,23 @@ def update_comment(name, content):
doc.save(ignore_permissions=True)
@frappe.whitelist()
def get_next(doctype, value, prev, filters=None, order_by="modified desc"):
prev = not int(prev)
sort_field, sort_order = order_by.split(" ")
def get_next(doctype, value, prev, filters, sort_order, sort_field):
prev = int(prev)
if not filters: filters = []
if isinstance(filters, string_types):
filters = json.loads(filters)
# condition based on sort order
condition = ">" if sort_order.lower()=="desc" else "<"
# # condition based on sort order
condition = ">" if sort_order.lower() == "asc" else "<"
# switch the condition
if prev:
condition = "<" if condition==">" else "<"
else:
sort_order = "asc" if sort_order.lower()=="desc" else "desc"
sort_order = "asc" if sort_order.lower() == "desc" else "desc"
condition = "<" if condition == ">" else ">"
# add condition for next or prev item
if not order_by[0] in [f[1] for f in filters]:
filters.append([doctype, sort_field, condition, value])
# # add condition for next or prev item
filters.append([doctype, sort_field, condition, frappe.get_value(doctype, value, sort_field)])
res = frappe.get_list(doctype,
fields = ["name"],
@ -124,4 +119,4 @@ def get_pdf_link(doctype, docname, print_format='Standard', no_letterhead=0):
docname = docname,
print_format = print_format,
no_letterhead = no_letterhead
)
)

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