Merge branch 'frappe:develop' into multiple_imap_folder

This commit is contained in:
Manuel 2021-11-23 17:23:23 +01:00 committed by GitHub
commit ecdaeffcbd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
69 changed files with 741 additions and 408 deletions

View file

@ -10,6 +10,7 @@ concurrency:
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 60
name: Patch Test

View file

@ -14,6 +14,7 @@ concurrency:
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 60
strategy:
fail-fast: false
@ -128,4 +129,4 @@ jobs:
fail_ci_if_error: true
files: /home/runner/frappe-bench/sites/coverage.xml
verbose: true
flags: server
flags: server

View file

@ -13,6 +13,7 @@ concurrency:
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 60
strategy:
fail-fast: false

View file

@ -13,6 +13,7 @@ concurrency:
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 60
strategy:
fail-fast: false

View file

@ -7,6 +7,8 @@ context('Report View', () => {
cy.visit('/app/website');
cy.insert_doc('DocType', custom_submittable_doctype, true);
cy.clear_cache();
});
it('Field with enabled allow_on_submit should be editable.', () => {
cy.insert_doc(doctype_name, {
'title': 'Doc 1',
'description': 'Random Text',
@ -14,8 +16,6 @@ context('Report View', () => {
// submit document
'docstatus': 1
}, true).as('doc');
});
it('Field with enabled allow_on_submit should be editable.', () => {
cy.intercept('POST', 'api/method/frappe.client.set_value').as('value-update');
cy.visit(`/app/List/${doctype_name}/Report`);
// check status column added from docstatus

View file

@ -1523,8 +1523,8 @@ def format(*args, **kwargs):
import frappe.utils.formatters
return frappe.utils.formatters.format_value(*args, **kwargs)
def get_print(doctype=None, name=None, print_format=None, style=None,
html=None, as_pdf=False, doc=None, output=None, no_letterhead=0, password=None):
def get_print(doctype=None, name=None, print_format=None, style=None, html=None,
as_pdf=False, doc=None, output=None, no_letterhead=0, password=None, pdf_options=None):
"""Get Print Format for given document.
:param doctype: DocType of document.
@ -1543,15 +1543,15 @@ def get_print(doctype=None, name=None, print_format=None, style=None,
local.form_dict.doc = doc
local.form_dict.no_letterhead = no_letterhead
options = None
pdf_options = pdf_options or {}
if password:
options = {'password': password}
pdf_options['password'] = password
if not html:
html = get_response_content("printview")
if as_pdf:
return get_pdf(html, output = output, options = options)
return get_pdf(html, options=pdf_options, output=output)
else:
return html

View file

@ -120,6 +120,8 @@ def init_request(request):
else:
frappe.connect(set_admin_as_user=False)
request.max_content_length = frappe.local.conf.get('max_file_size') or 10 * 1024 * 1024
make_form_dict(request)
if request.method != "OPTIONS":

View file

@ -461,6 +461,7 @@ def migrate(context, skip_failing=False, skip_search_index=False):
skip_search_index=skip_search_index
)
finally:
print()
frappe.destroy()
if not context.sites:
raise SiteNotSpecifiedError

View file

@ -791,10 +791,11 @@ def request(context, args=None, path=None):
@click.command('make-app')
@click.argument('destination')
@click.argument('app_name')
def make_app(destination, app_name):
@click.option('--no-git', is_flag=True, default=False, help='Do not initialize git repository for the app')
def make_app(destination, app_name, no_git=False):
"Creates a boilerplate app"
from frappe.utils.boilerplate import make_boilerplate
make_boilerplate(destination, app_name)
make_boilerplate(destination, app_name, no_git=no_git)
@click.command('set-config')

View file

@ -146,25 +146,43 @@ def add_attachments(name, attachments):
})
_file.save(ignore_permissions=True)
@frappe.whitelist(allow_guest=True)
def mark_email_as_seen(name=None):
@frappe.whitelist(allow_guest=True, methods=("GET",))
def mark_email_as_seen(name: str = None):
try:
if name and frappe.db.exists("Communication", name) and not frappe.db.get_value("Communication", name, "read_by_recipient"):
frappe.db.set_value("Communication", name, "read_by_recipient", 1)
frappe.db.set_value("Communication", name, "delivery_status", "Read")
frappe.db.set_value("Communication", name, "read_by_recipient_on", get_datetime())
frappe.db.commit()
update_communication_as_read(name)
frappe.db.commit() # nosemgrep: this will be called in a GET request
except Exception:
frappe.log_error(frappe.get_traceback())
finally:
# Return image as response under all circumstances
from PIL import Image
import io
im = Image.new('RGBA', (1, 1))
im.putdata([(255,255,255,0)])
buffered_obj = io.BytesIO()
im.save(buffered_obj, format="PNG")
frappe.response["type"] = 'binary'
frappe.response["filename"] = "imaginary_pixel.png"
frappe.response["filecontent"] = buffered_obj.getvalue()
finally:
frappe.response.update({
"type": "binary",
"filename": "imaginary_pixel.png",
"filecontent": (
b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00"
b"\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\r"
b"IDATx\x9cc\xf8\xff\xff?\x03\x00\x08\xfc\x02\xfe\xa7\x9a\xa0"
b"\xa0\x00\x00\x00\x00IEND\xaeB`\x82"
)
})
def update_communication_as_read(name):
if not name or not isinstance(name, str):
return
communication = frappe.db.get_value(
"Communication",
name,
"read_by_recipient",
as_dict=True
)
if not communication or communication.read_by_recipient:
return
frappe.db.set_value("Communication", name, {
"read_by_recipient": 1,
"delivery_status": "Read",
"read_by_recipient_on": get_datetime()
})

View file

@ -191,7 +191,7 @@ class Exporter:
[format_column_name(df) for df in self.fields if df.parent == child_table_doctype]
)
)
data = frappe.db.get_list(
data = frappe.db.get_all(
child_table_doctype,
filters={
"parent": ("in", parent_names),

View file

@ -716,13 +716,11 @@ def delete_file(path):
os.remove(path)
@frappe.whitelist()
def get_max_file_size():
return cint(conf.get('max_file_size')) or 10485760
def has_permission(doc, ptype=None, user=None):
has_access = False
user = user or frappe.session.user

View file

@ -2,15 +2,22 @@
# License: MIT. See LICENSE
import frappe
from frappe.model.document import Document
desk_properties = ("search_bar", "notifications", "list_sidebar",
"bulk_actions", "view_switcher", "form_sidebar", "timeline", "dashboard")
STANDARD_ROLES = (
"Administrator",
"System Manager",
"Script Manager",
"All",
"Guest"
)
class Role(Document):
def before_rename(self, old, new, merge=False):
if old in ("Guest", "Administrator", "System Manager", "All"):
if old in STANDARD_ROLES:
frappe.throw(frappe._("Standard roles cannot be renamed"))
def after_insert(self):
@ -23,7 +30,7 @@ class Role(Document):
self.set_desk_properties()
def disable_role(self):
if self.name in ("Guest", "Administrator", "System Manager", "All"):
if self.name in STANDARD_ROLES:
frappe.throw(frappe._("Standard roles cannot be disabled"))
else:
self.remove_roles()

View file

@ -599,7 +599,7 @@
"fieldname": "desk_theme",
"fieldtype": "Select",
"label": "Desk Theme",
"options": "Light\nDark"
"options": "Light\nDark\nAutomatic"
},
{
"fieldname": "module_profile",
@ -669,7 +669,7 @@
}
],
"max_attachments": 5,
"modified": "2021-10-27 17:17:16.098457",
"modified": "2021-11-17 17:17:16.098457",
"modified_by": "Administrator",
"module": "Core",
"name": "User",

View file

@ -1046,7 +1046,7 @@ def generate_keys(user):
@frappe.whitelist()
def switch_theme(theme):
if theme in ["Dark", "Light"]:
if theme in ["Dark", "Light", "Automatic"]:
frappe.db.set_value("User", frappe.session.user, "desk_theme", theme)
def get_enabled_users():

View file

@ -568,11 +568,10 @@ class Database(object):
def _get_value_for_many_names(self, doctype, names, field, debug=False, run=True):
names = list(filter(None, names))
if names:
return self.get_all(doctype,
fields=['name', field],
filters=[['name', 'in', names]],
fields=field,
filters=names,
debug=debug, as_list=1, run=run)
else:
return {}

View file

@ -53,7 +53,7 @@
},
{
"fieldname": "subject",
"fieldtype": "Data",
"fieldtype": "Small Text",
"in_global_search": 1,
"in_list_view": 1,
"label": "Subject",
@ -277,10 +277,11 @@
"icon": "fa fa-calendar",
"idx": 1,
"links": [],
"modified": "2020-01-14 21:47:15.825287",
"modified": "2021-11-18 05:06:24.881742",
"modified_by": "Administrator",
"module": "Desk",
"name": "Event",
"naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{

View file

@ -15,7 +15,9 @@
"enable_email_energy_point",
"enable_email_share",
"user",
"seen"
"seen",
"system_notifications_section",
"energy_points_system_notifications"
],
"fields": [
{
@ -84,15 +86,27 @@
"fieldtype": "Check",
"hidden": 1,
"label": "Seen"
},
{
"fieldname": "system_notifications_section",
"fieldtype": "Section Break",
"label": "System Notifications"
},
{
"default": "1",
"fieldname": "energy_points_system_notifications",
"fieldtype": "Check",
"label": "Energy Points"
}
],
"in_create": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2020-11-04 12:54:57.989317",
"modified": "2021-11-16 12:18:46.955501",
"modified_by": "Administrator",
"module": "Desk",
"name": "Notification Settings",
"naming_rule": "Set by user",
"owner": "Administrator",
"permissions": [
{
@ -111,4 +125,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}
}

View file

@ -240,6 +240,7 @@ def remove_app(app_name, dry_run=False, yes=False, no_backup=False, force=False)
if not dry_run:
remove_from_installed_apps(app_name)
frappe.get_single('Installed Applications').update_versions()
frappe.db.commit()
click.secho(f"Uninstalled App {app_name} from Site {site}", fg="green")

View file

@ -175,6 +175,8 @@ def parse_naming_series(parts, doctype='', doc=''):
part = today.strftime("%d")
elif e == 'YYYY':
part = today.strftime('%Y')
elif e == 'WW':
part = determine_consecutive_week_number(today)
elif e == 'timestamp':
part = str(today)
elif e == 'FY':
@ -193,6 +195,19 @@ def parse_naming_series(parts, doctype='', doc=''):
return n
def determine_consecutive_week_number(datetime):
"""Determines the consecutive calendar week"""
m = datetime.month
# ISO 8601 calandar week
w = datetime.strftime('%V')
# Ensure consecutiveness for the first and last days of a year
if m == 1 and int(w) >= 52:
w = '00'
elif m == 12 and int(w) <= 1:
w = '53'
return w
def getseries(key, digits):
# series created ?
# Using frappe.qb as frappe.get_values does not allow order_by=None

View file

@ -10,6 +10,8 @@
"repeat_header_footer",
"column_break_4",
"pdf_page_size",
"pdf_page_height",
"pdf_page_width",
"view_link_in_email",
"with_letterhead",
"allow_print_for_draft",
@ -56,7 +58,7 @@
"fieldname": "pdf_page_size",
"fieldtype": "Select",
"label": "PDF Page Size",
"options": "A4\nLetter"
"options": "A0\nA1\nA2\nA3\nA4\nA5\nA6\nA7\nA8\nA9\nB0\nB1\nB2\nB3\nB4\nB5\nB6\nB7\nB8\nB9\nB10\nC5E\nComm10E\nDLE\nExecutive\nFolio\nLedger\nLegal\nLetter\nTabloid\nCustom"
},
{
"fieldname": "view_link_in_email",
@ -156,6 +158,18 @@
"fieldname": "font_size",
"fieldtype": "Float",
"label": "Font Size"
},
{
"depends_on": "eval:doc.pdf_page_size == \"Custom\"",
"fieldname": "pdf_page_height",
"fieldtype": "Float",
"label": "PDF Page Height (in mm)"
},
{
"depends_on": "eval:doc.pdf_page_size == \"Custom\"",
"fieldname": "pdf_page_width",
"fieldtype": "Float",
"label": "PDF Page Width (in mm)"
}
],
"icon": "fa fa-cog",

View file

@ -8,14 +8,23 @@ from frappe.utils import cint
from frappe.model.document import Document
class PrintSettings(Document):
def validate(self):
if self.pdf_page_size == "Custom" and not (
self.pdf_page_height and self.pdf_page_width
):
frappe.throw(_("Page height and width cannot be zero"))
def on_update(self):
frappe.clear_cache()
@frappe.whitelist()
def is_print_server_enabled():
if not hasattr(frappe.local, 'enable_print_server'):
frappe.local.enable_print_server = cint(frappe.db.get_single_value('Print Settings',
'enable_print_server'))
if not hasattr(frappe.local, "enable_print_server"):
frappe.local.enable_print_server = cint(
frappe.db.get_single_value("Print Settings", "enable_print_server")
)
return frappe.local.enable_print_server

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

View file

@ -64,6 +64,19 @@ frappe.Application = class Application {
}
});
frappe.ui.add_system_theme_switch_listener();
const root = document.documentElement;
const observer = new MutationObserver(() => {
frappe.ui.set_theme();
});
observer.observe(root, {
attributes: true,
attributeFilter: ['data-theme-mode']
});
frappe.ui.set_theme();
// page container
this.make_page_container();
this.set_route();

View file

@ -29,21 +29,26 @@
</span>
</div>
<label v-if="is_optimizable" class="optimize-checkbox"><input type="checkbox" :checked="optimize" @change="$emit('toggle_optimize')">Optimize</label>
<div>
<span v-if="file.error_message" class="file-error text-danger">
{{ file.error_message }}
</span>
</div>
</div>
<div class="file-actions">
<ProgressRing
v-show="file.uploading && !uploaded"
v-show="file.uploading && !uploaded && !file.failed"
primary="var(--primary-color)"
secondary="var(--gray-200)"
radius="24"
:radius="24"
:progress="progress"
stroke="3"
:stroke="3"
/>
<div v-if="uploaded" v-html="frappe.utils.icon('solid-success', 'lg')"></div>
<div v-if="file.failed" v-html="frappe.utils.icon('solid-red', 'lg')"></div>
<div v-if="file.failed" v-html="frappe.utils.icon('solid-error', 'lg')"></div>
<div class="file-action-buttons">
<button v-if="is_cropable" class="btn btn-crop muted" @click="$emit('toggle_image_cropper')" v-html="frappe.utils.icon('crop', 'md')"></button>
<button v-if="!uploaded && !file.uploading" class="btn muted" @click="$emit('remove')" v-html="frappe.utils.icon('delete', 'md')"></button>
<button v-if="!uploaded && !file.uploading && !file.failed" class="btn muted" @click="$emit('remove')" v-html="frappe.utils.icon('delete', 'md')"></button>
</div>
</div>
</div>
@ -89,18 +94,18 @@ export default {
return this.file.doc ? this.file.doc.is_private : this.file.private;
},
uploaded() {
return this.file.total && this.file.total === this.file.progress && !this.file.failed;
return this.file.request_succeeded;
},
is_image() {
return this.file.file_obj.type.startsWith('image');
},
is_optimizable() {
let is_svg = this.file.file_obj.type == 'image/svg+xml';
return this.is_image && !is_svg;
return this.is_image && !is_svg && !this.uploaded && !this.file.failed;
},
is_cropable() {
let croppable_types = ['image/jpeg', 'image/png'];
return !this.uploaded && !this.file.uploading && croppable_types.includes(this.file.file_obj.type);
return !this.uploaded && !this.file.uploading && !this.file.failed && croppable_types.includes(this.file.file_obj.type);
},
progress() {
let value = Math.round((this.file.progress * 100) / this.file.total);
@ -208,4 +213,9 @@ export default {
align-items: center;
padding-top: 0.25rem;
}
.file-error {
font-size: var(--text-sm);
font-weight: var(--text-bold);
}
</style>

View file

@ -197,6 +197,7 @@ export default {
show_image_cropper: false,
crop_image_with_index: -1,
trigger_upload: false,
close_dialog: false,
hide_dialog_footer: false,
allow_take_photo: false,
allow_web_link: true,
@ -218,6 +219,12 @@ export default {
}
});
}
if (this.restrictions.max_file_size == null) {
frappe.call('frappe.core.doctype.file.file.get_max_file_size')
.then(res => {
this.restrictions.max_file_size = Number(res.message);
});
}
},
watch: {
files(newvalue, oldvalue) {
@ -289,6 +296,8 @@ export default {
progress: 0,
total: 0,
failed: false,
request_succeeded: false,
error_message: null,
uploading: false,
private: !is_image
}
@ -329,9 +338,17 @@ export default {
if (!is_correct_type) {
console.warn('File skipped because of invalid file type', file);
frappe.show_alert({
message: __('File "{0}" was skipped because of invalid file type', [file.name]),
indicator: 'orange'
});
}
if (!valid_file_size) {
console.warn('File skipped because of invalid file size', file.size, file);
frappe.show_alert({
message: __('File "{0}" was skipped because size exceeds {1} MB', [file.name, max_file_size / (1024 * 1024)]),
indicator: 'orange'
});
}
return is_correct_type && valid_file_size;
@ -357,9 +374,10 @@ export default {
let selected_file = this.$refs.file_browser.selected_node;
if (!selected_file.value) {
frappe.msgprint(__('Click on a file to select it.'));
this.close_dialog = true;
return Promise.reject();
}
this.close_dialog = true;
return this.upload_file({
file_url: selected_file.file_url
});
@ -368,9 +386,11 @@ export default {
let file_url = this.$refs.web_link.url;
if (!file_url) {
frappe.msgprint(__('Invalid URL'));
this.close_dialog = true;
return Promise.reject();
}
file_url = decodeURI(file_url)
this.close_dialog = true;
return this.upload_file({
file_url
});
@ -383,6 +403,7 @@ export default {
this.on_success && this.on_success(file);
})
);
this.close_dialog = true;
return Promise.all(promises);
},
upload_file(file, i) {
@ -410,6 +431,7 @@ export default {
xhr.onreadystatechange = () => {
if (xhr.readyState == XMLHttpRequest.DONE) {
if (xhr.status === 200) {
file.request_succeeded = true;
let r = null;
let file_doc = null;
try {
@ -426,15 +448,24 @@ export default {
if (this.on_success) {
this.on_success(file_doc, r);
}
if (i == this.files.length - 1 && this.files.every(file => file.request_succeeded)) {
this.close_dialog = true;
}
} else if (xhr.status === 403) {
file.failed = true;
let response = JSON.parse(xhr.responseText);
frappe.msgprint({
title: __('Not permitted'),
indicator: 'red',
message: response._error_message
});
file.error_message = `Not permitted. ${response._error_message || ''}`;
} else if (xhr.status === 413) {
file.failed = true;
file.error_message = 'Size exceeds the maximum allowed file size.';
} else {
file.failed = true;
file.error_message = xhr.status === 0 ? 'XMLHttpRequest Error' : `${xhr.status} : ${xhr.statusText}`;
let error = null;
try {
error = JSON.parse(xhr.responseText);

View file

@ -67,6 +67,12 @@ export default class FileUploader {
}
});
this.uploader.$watch('close_dialog', (close_dialog) => {
if (close_dialog) {
this.dialog && this.dialog.hide();
}
});
this.uploader.$watch('hide_dialog_footer', (hide_dialog_footer) => {
if (hide_dialog_footer) {
this.dialog && this.dialog.footer.addClass('hide');
@ -84,10 +90,8 @@ export default class FileUploader {
upload_files() {
this.dialog && this.dialog.get_primary_btn().prop('disabled', true);
return this.uploader.upload_files()
.then(() => {
this.dialog && this.dialog.hide();
});
this.dialog && this.dialog.get_secondary_btn().prop('disabled', true);
return this.uploader.upload_files();
}
make_dialog() {

View file

@ -3,6 +3,11 @@ frappe.provide('frappe.utils.utils');
frappe.ui.form.ControlGeolocation = class ControlGeolocation extends frappe.ui.form.ControlData {
static horizontal = false
async make() {
await frappe.require(this.required_libs);
super.make();
}
make_wrapper() {
// Create the elements for map area
super.make_wrapper();
@ -196,4 +201,17 @@ frappe.ui.form.ControlGeolocation = class ControlGeolocation extends frappe.ui.f
this.editableLayers.removeLayer(l);
});
}
get required_libs() {
return [
"assets/frappe/js/lib/leaflet/easy-button.css",
"assets/frappe/js/lib/leaflet/L.Control.Locate.css",
"assets/frappe/js/lib/leaflet/leaflet.draw.css",
"assets/frappe/js/lib/leaflet/leaflet.css",
"assets/frappe/js/lib/leaflet/leaflet.js",
"assets/frappe/js/lib/leaflet/easy-button.js",
"assets/frappe/js/lib/leaflet/leaflet.draw.js",
"assets/frappe/js/lib/leaflet/L.Control.Locate.js",
];
}
};

View file

@ -14,6 +14,7 @@ frappe.ui.form.Dashboard = class FormDashboard {
this.progress_area = this.make_section({
css_class: 'progress-area',
hidden: 1,
collapsible: 1,
is_dashboard_section: 1,
});
@ -21,6 +22,7 @@ frappe.ui.form.Dashboard = class FormDashboard {
label: __("Overview"),
css_class: 'form-heatmap',
hidden: 1,
collapsible: 1,
is_dashboard_section: 1,
body_html: `
<div id="heatmap-${frappe.model.scrub(this.frm.doctype)}" class="heatmap"></div>
@ -32,6 +34,7 @@ frappe.ui.form.Dashboard = class FormDashboard {
label: __("Graph"),
css_class: 'form-graph',
hidden: 1,
collapsible: 1,
is_dashboard_section: 1
});
@ -40,6 +43,7 @@ frappe.ui.form.Dashboard = class FormDashboard {
label: __("Stats"),
css_class: 'form-stats',
hidden: 1,
collapsible: 1,
is_dashboard_section: 1,
body_html: this.stats_area_row
});
@ -50,6 +54,7 @@ frappe.ui.form.Dashboard = class FormDashboard {
label: __("Connections"),
css_class: 'form-links',
hidden: 1,
collapsible: 1,
is_dashboard_section: 1,
body_html: this.transactions_area
});
@ -84,9 +89,10 @@ frappe.ui.form.Dashboard = class FormDashboard {
hidden,
body_html,
make_card: true,
collapsible: 1,
is_dashboard_section: 1
};
return new Section(this.frm.layout.wrapper, options).body;
return new Section(this.parent, options).body;
}
add_progress(title, percent, message) {
@ -203,7 +209,7 @@ frappe.ui.form.Dashboard = class FormDashboard {
after_refresh() {
// show / hide new buttons (if allowed)
this.links_area.body.find('.btn-new').each((i, el) => {
if (this.frm.can_create($(this).attr('data-doctype'))) {
if (this.frm.can_create($(el).attr('data-doctype'))) {
$(el).removeClass('hidden');
}
});

View file

@ -156,8 +156,11 @@ frappe.ui.form.Form = class FrappeForm {
let dashboard_parent = $('<div class="form-dashboard">');
let main_page = this.layout.tabs.length ? this.layout.tabs[0].wrapper : this.layout.wrapper;
main_page.prepend(dashboard_parent);
if (this.layout.tabs.length) {
this.layout.tabs[0].wrapper.prepend(dashboard_parent);
} else {
dashboard_parent.insertAfter(this.layout.wrapper.find('.form-message'));
}
this.dashboard = new frappe.ui.form.Dashboard(dashboard_parent, this);
this.tour = new frappe.ui.form.FormTour({

View file

@ -245,7 +245,7 @@ frappe.ui.form.Layout = class Layout {
}
make_section(df) {
this.section = new Section(this.current_tab ? this.current_tab.wrapper : this.page, df, this.card_layout);
this.section = new Section(this.current_tab ? this.current_tab.wrapper : this.page, df, this.card_layout, this);
// append to layout fields
if (df) {

View file

@ -267,7 +267,7 @@ frappe.ui.form.QuickEntryForm = class QuickEntryForm {
render_edit_in_full_page_link() {
var me = this;
this.dialog.add_custom_action(
`${frappe.utils.icon('edit', 'xs')} ${__("Edit in full page")}`,
`${__("Edit in full page")}`,
() => me.open_doc(true)
);
}

View file

@ -1,5 +1,6 @@
export default class Section {
constructor(parent, df, card_layout) {
constructor(parent, df, card_layout, layout) {
this.layout = layout;
this.card_layout = card_layout;
this.parent = parent;
this.df = df || {};
@ -25,6 +26,7 @@ export default class Section {
${this.df.is_dashboard_section ? "form-dashboard-section" : "form-section"}
${ make_card ? "card-section" : "" }">
`).appendTo(this.parent);
this.layout && this.layout.sections.push(this);
if (this.df) {
if (this.df.label) {

View file

@ -24,51 +24,84 @@ export default class BulkOperations {
return;
}
if (valid_docs.length > 0) {
const dialog = new frappe.ui.Dialog({
title: __('Print Documents'),
fields: [
{
'fieldtype': 'Select',
'label': __('Letter Head'),
'fieldname': 'letter_sel',
'default': __('No Letterhead'),
options: this.get_letterhead_options()
},
{
'fieldtype': 'Select',
'label': __('Print Format'),
'fieldname': 'print_sel',
options: frappe.meta.get_print_formats(this.doctype)
}
]
});
dialog.set_primary_action(__('Print'), args => {
if (!args) return;
const default_print_format = frappe.get_meta(this.doctype).default_print_format;
const with_letterhead = args.letter_sel == __("No Letterhead") ? 0 : 1;
const print_format = args.print_sel ? args.print_sel : default_print_format;
const json_string = JSON.stringify(valid_docs);
const letterhead = args.letter_sel;
const w = window.open('/api/method/frappe.utils.print_format.download_multi_pdf?' +
'doctype=' + encodeURIComponent(this.doctype) +
'&name=' + encodeURIComponent(json_string) +
'&format=' + encodeURIComponent(print_format) +
'&no_letterhead=' + (with_letterhead ? '0' : '1') +
'&letterhead=' + encodeURIComponent(letterhead)
);
if (!w) {
frappe.msgprint(__('Please enable pop-ups'));
return;
}
});
dialog.show();
} else {
if (valid_docs.length === 0) {
frappe.msgprint(__('Select atleast 1 record for printing'));
return;
}
const dialog = new frappe.ui.Dialog({
title: __('Print Documents'),
fields: [{
fieldtype: 'Select',
label: __('Letter Head'),
fieldname: 'letter_sel',
default: __('No Letterhead'),
options: this.get_letterhead_options()
},
{
fieldtype: 'Select',
label: __('Print Format'),
fieldname: 'print_sel',
options: frappe.meta.get_print_formats(this.doctype)
},
{
fieldtype: 'Select',
label: __('Page Size'),
fieldname: 'page_size',
options: frappe.meta.get_print_sizes(),
default: print_settings.pdf_page_size
},
{
fieldtype: 'Float',
label: __('Page Height (in mm)'),
fieldname: 'page_height',
depends_on: 'eval:doc.page_size == "Custom"',
default: print_settings.pdf_page_height
},
{
fieldtype: 'Float',
label: __('Page Width (in mm)'),
fieldname: 'page_width',
depends_on: 'eval:doc.page_size == "Custom"',
default: print_settings.pdf_page_width
}]
});
dialog.set_primary_action(__('Print'), args => {
if (!args) return;
const default_print_format = frappe.get_meta(this.doctype).default_print_format;
const with_letterhead = args.letter_sel == __("No Letterhead") ? 0 : 1;
const print_format = args.print_sel ? args.print_sel : default_print_format;
const json_string = JSON.stringify(valid_docs);
const letterhead = args.letter_sel;
let pdf_options;
if (args.page_size === "Custom") {
if (args.page_height === 0 || args.page_width === 0) {
frappe.throw(__('Page height and width cannot be zero'));
}
pdf_options = JSON.stringify({ "page-height": args.page_height, "page-width": args.page_width });
} else {
pdf_options = JSON.stringify({ "page-size": args.page_size });
}
const w = window.open(
'/api/method/frappe.utils.print_format.download_multi_pdf?' +
'doctype=' + encodeURIComponent(this.doctype) +
'&name=' + encodeURIComponent(json_string) +
'&format=' + encodeURIComponent(print_format) +
'&no_letterhead=' + (with_letterhead ? '0' : '1') +
'&letterhead=' + encodeURIComponent(letterhead) +
'&options=' + encodeURIComponent(pdf_options)
);
if (!w) {
frappe.msgprint(__('Please enable pop-ups'));
return;
}
});
dialog.show();
}
get_letterhead_options () {

View file

@ -307,6 +307,8 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
}
update_checkbox(target) {
if (!this.$checkbox_actions) return;
let $check_all_checkbox = this.$checkbox_actions.find(".list-check-all");
if ($check_all_checkbox.prop("checked") && target && !target.prop("checked")) {

View file

@ -192,6 +192,15 @@ $.extend(frappe.meta, {
}
},
get_print_sizes: function() {
return [
"A0", "A1", "A2", "A3", "A4", "A5", "A6", "A7", "A8", "A9",
"B0", "B1", "B2", "B3", "B4", "B5", "B6", "B7", "B8", "B9", "B10",
"C5E", "Comm10E", "DLE", "Executive", "Folio", "Ledger", "Legal",
"Letter", "Tabloid", "Custom"
];
},
get_print_formats: function(doctype) {
var print_format_list = ["Standard"];
var default_print_format = locals.DocType[doctype].default_print_format;

View file

@ -42,7 +42,7 @@ frappe.ui.ThemeSwitcher = class ThemeSwitcher {
}
refresh() {
this.current_theme = document.documentElement.getAttribute("data-theme") || "light";
this.current_theme = document.documentElement.getAttribute("data-theme-mode") || "light";
this.fetch_themes().then(() => {
this.render();
});
@ -54,10 +54,17 @@ frappe.ui.ThemeSwitcher = class ThemeSwitcher {
{
name: "light",
label: __("Frappe Light"),
info: __("Light Theme")
},
{
name: "dark",
label: __("Timeless Night"),
info: __("Dark Theme")
},
{
name: "automatic",
label: __("Automatic"),
info: __("Uses system's theme to switch between light and dark mode")
}
];
@ -74,11 +81,15 @@ frappe.ui.ThemeSwitcher = class ThemeSwitcher {
}
get_preview_html(theme) {
const is_auto_theme = theme.name === "automatic";
const preview = $(`<div class="${this.current_theme == theme.name ? "selected" : "" }">
<div data-theme=${theme.name}>
<div data-theme=${is_auto_theme ? "light" : theme.name}
data-is-auto-theme="${is_auto_theme}" title="${theme.info}">
<div class="background">
<div>
<div class="preview-check">${frappe.utils.icon('tick', 'xs')}</div>
<div class="preview-check" data-theme=${is_auto_theme ? "dark" : theme.name}>
${frappe.utils.icon('tick', 'xs')}
</div>
</div>
<div class="navbar"></div>
<div class="p-2">
@ -112,13 +123,14 @@ frappe.ui.ThemeSwitcher = class ThemeSwitcher {
toggle_theme(theme) {
this.current_theme = theme.toLowerCase();
document.documentElement.setAttribute("data-theme", this.current_theme);
document.documentElement.setAttribute("data-theme-mode", this.current_theme);
frappe.show_alert("Theme Changed", 3);
frappe.xcall("frappe.core.doctype.user.user.switch_theme", {
theme: toTitle(theme)
});
}
show() {
this.dialog.show();
}
@ -127,3 +139,22 @@ frappe.ui.ThemeSwitcher = class ThemeSwitcher {
this.dialog.hide();
}
};
frappe.ui.add_system_theme_switch_listener = () => {
frappe.ui.dark_theme_media_query.addEventListener('change', () => {
frappe.ui.set_theme();
});
};
frappe.ui.dark_theme_media_query = window.matchMedia("(prefers-color-scheme: dark)");
frappe.ui.set_theme = (theme) => {
const root = document.documentElement;
let theme_mode = root.getAttribute("data-theme-mode");
if (!theme) {
if (theme_mode === "automatic") {
theme = frappe.ui.dark_theme_media_query.matches ? 'dark' : 'light';
}
}
root.setAttribute("data-theme", theme || theme_mode);
};

View file

@ -305,7 +305,7 @@ frappe.search.AwesomeBar = class AwesomeBar {
index: 80,
default: "Calculator",
onclick: function() {
frappe.msgprint(formatted_value, "Result");
frappe.msgprint(formatted_value, __("Result"));
}
});
} catch(e) {
@ -317,10 +317,10 @@ frappe.search.AwesomeBar = class AwesomeBar {
make_random(txt) {
if(txt.toLowerCase().includes('random')) {
this.options.push({
label: "Generate Random Password",
label: __("Generate Random Password"),
value: frappe.utils.get_random(16),
onclick: function() {
frappe.msgprint(frappe.utils.get_random(16), "Result");
frappe.msgprint(frappe.utils.get_random(16), __("Result"));
}
})
}

View file

@ -129,7 +129,7 @@ function format_currency(v, currency, decimals) {
}
if (symbol)
return symbol + " " + format_number(v, format, decimals);
return __(symbol) + " " + format_number(v, format, decimals);
else
return format_number(v, format, decimals);
}

View file

@ -42,7 +42,6 @@ frappe.views.Container = class Container {
cur_page = this;
if(this.page && this.page.label === label) {
$(this.page).trigger('show');
return;
}
var me = this;

View file

@ -634,6 +634,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
this.render_datatable();
this.add_chart_buttons_to_toolbar(true);
this.add_card_button_to_toolbar();
this.$report.show();
} else {
this.data = [];
this.toggle_nothing_to_show(true);
@ -882,7 +883,6 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
hide_loading_screen() {
this.$loading.hide();
this.$report.show();
}
get_chart_options(data) {
@ -1789,6 +1789,19 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
this.$chart.toggle(flag);
this.$summary.toggle(flag);
}
get_checked_items(only_docnames) {
const indexes = this.datatable.rowmanager.getCheckedRows();
return indexes.reduce((items, i) => {
if (i === undefined) return items;
const item = this.data[i];
items.push(only_docnames ? item.name : item);
return items;
}, []);
}
// backward compatibility
get get_values() {
return this.get_filter_values;

View file

@ -106,6 +106,7 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
get_args() {
const args = super.get_args();
delete args.group_by;
this.group_by_control.set_args(args);
return args;

View file

@ -211,7 +211,7 @@ export default class NumberCardWidget extends Widget {
const symbol = number_parts[1] || '';
const formatted_number = $(frappe.format(number_parts[0], df)).text();
this.formatted_number = formatted_number + ' ' + symbol;
this.formatted_number = formatted_number + ' ' + __(symbol);
}
render_number() {

View file

@ -1,6 +1,6 @@
.modal-body .theme-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
grid-template-columns: repeat(3, minmax(0, 1fr));
grid-gap: 18px;
.background {
@ -9,7 +9,7 @@
border-radius: var(--border-radius-lg);
overflow: hidden;
cursor: pointer;
height: 160px;
height: 120px;
position: relative;
&:hover {
@ -28,6 +28,7 @@
margin-right: var(--margin-sm);
border-radius: var(--border-radius-full);
z-index: 1;
}
}
@ -72,6 +73,7 @@
border-radius: var(--border-radius-sm);
height: 10px;
width: 20px;
z-index: 1;
}
.text {
@ -80,4 +82,17 @@
height: 10px;
width: 40px;
}
}
// TODO: Replace with better alternative
[data-is-auto-theme="true"] {
.background::after {
content: "";
top: 0;
right: 0;
height: 100%;
width: 50%;
background: var(--gray-900);
position: absolute;
}
}

View file

@ -0,0 +1,18 @@
.error-page {
text-align: center;
.img-404 {
width: 40%;
margin: var(--margin-2xl) auto;
@include media-breakpoint-down(sm) {
width: 80%
}
}
.back-to-home {
font-size: var(--text-base);
}
}

View file

@ -26,6 +26,7 @@
@import 'doc';
@import 'navbar';
@import 'footer';
@import 'error-state';
.ql-editor.read-mode {
padding: 0;

View file

@ -158,6 +158,8 @@ def get():
bootinfo["setup_complete"] = cint(frappe.db.get_single_value('System Settings', 'setup_complete'))
bootinfo["is_first_startup"] = cint(frappe.db.get_single_value('System Settings', 'is_first_startup'))
bootinfo['desk_theme'] = frappe.db.get_value("User", frappe.session.user, "desk_theme") or 'Light'
return bootinfo
@frappe.whitelist()

View file

@ -32,7 +32,9 @@ class EnergyPointLog(Document):
frappe.cache().hdel('energy_points', self.user)
frappe.publish_realtime('update_points', after_commit=True)
if self.type != 'Review':
if self.type != 'Review' and \
frappe.get_cached_value('Notification Settings', self.user, 'energy_points_system_notifications'):
reference_user = self.user if self.type == 'Auto' else self.owner
notification_doc = {
'type': 'Energy Point',

View file

@ -8,6 +8,18 @@ from frappe.utils.testutils import add_custom_field, clear_custom_fields
from frappe.desk.form.assign_to import add as assign_to
class TestEnergyPointLog(unittest.TestCase):
@classmethod
def setUpClass(cls):
settings = frappe.get_single('Energy Point Settings')
settings.enabled = 1
settings.save()
@classmethod
def tearDownClass(cls):
settings = frappe.get_single('Energy Point Settings')
settings.enabled = 0
settings.save()
def setUp(self):
frappe.cache().delete_value('energy_point_rule_map')
@ -336,4 +348,4 @@ def assign_users_to_todo(todo_name, users):
'assign_to': [user],
'doctype': 'ToDo',
'name': todo_name
})
})

View file

@ -1,229 +1,70 @@
{
"allow_copy": 0,
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"actions": [],
"creation": "2019-03-19 13:17:51.710241",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"enabled",
"section_break_2",
"review_levels",
"point_allocation_periodicity",
"last_point_allocation_date"
],
"fields": [
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "1",
"fetch_if_empty": 0,
"default": "0",
"fieldname": "enabled",
"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": "Enabled",
"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
"label": "Enabled"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "enabled",
"fetch_if_empty": 0,
"fieldname": "section_break_2",
"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,
"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
"fieldtype": "Section Break"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "review_levels",
"fieldtype": "Table",
"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": "Review Levels",
"length": 0,
"no_copy": 0,
"options": "Review Level",
"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
"options": "Review Level"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "Weekly",
"fetch_if_empty": 0,
"fieldname": "point_allocation_periodicity",
"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": "Point Allocation Periodicity",
"length": 0,
"no_copy": 0,
"options": "Daily\nWeekly\nMonthly",
"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
"options": "Daily\nWeekly\nMonthly"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "last_point_allocation_date",
"fieldtype": "Date",
"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": "Last Point Allocation Date",
"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,
"translatable": 0,
"unique": 0
"read_only": 1
}
],
"has_web_view": 0,
"hide_toolbar": 1,
"idx": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 1,
"istable": 0,
"max_attachments": 0,
"modified": "2019-03-26 19:10:14.087840",
"links": [],
"modified": "2021-11-16 23:24:01.366928",
"modified_by": "Administrator",
"module": "Social",
"name": "Energy Point Settings",
"name_case": "",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 0,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
}
],
"quick_entry": 1,
"read_only": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "ASC",
"track_changes": 1,
"track_seen": 0,
"track_views": 0
"track_changes": 1
}

View file

@ -0,0 +1,8 @@
# Copyright (c) 2021, Frappe Technologies and Contributors
# See license.txt
# import frappe
import unittest
class TestEnergyPointSettings(unittest.TestCase):
pass

View file

@ -28,16 +28,6 @@ def add_comment(comment, comment_email, comment_by, reference_doctype, reference
frappe.msgprint(_('Comments cannot have links or email addresses'))
return False
comments_count = frappe.db.count("Comment", {
"comment_type": "Comment",
"comment_email": comment_email,
"creation": (">", add_to_date(now(), hours=-1))
})
if comments_count > 20:
frappe.msgprint(_('Hourly comment limit reached for: {0}').format(frappe.bold(comment_email)))
return False
comment = doc.add_comment(
text=comment,
comment_email=comment_email,
@ -54,14 +44,17 @@ def add_comment(comment, comment_email, comment_by, reference_doctype, reference
comment.name,
_("View Comment")))
# notify creator
frappe.sendmail(
recipients=frappe.db.get_value('User', doc.owner, 'email') or doc.owner,
subject=_('New Comment on {0}: {1}').format(doc.doctype, doc.name),
message=content,
reference_doctype=doc.doctype,
reference_name=doc.name
)
if doc.doctype == "Blog Post" and not doc.enable_email_notification:
pass
else:
# notify creator
frappe.sendmail(
recipients=frappe.db.get_value('User', doc.owner, 'email') or doc.owner,
subject=_('New Comment on {0}: {1}').format(doc.doctype, doc.name),
message=content,
reference_doctype=doc.doctype,
reference_name=doc.name
)
# revert with template if all clear (no backlinks)
template = frappe.get_template("templates/includes/comments/comment.html")

View file

@ -12,8 +12,8 @@ from frappe.website.doctype.blog_settings.blog_settings import get_feedback_limi
@rate_limit(key='reference_name', limit=get_feedback_limit, seconds=60*60)
def give_feedback(reference_doctype, reference_name, like):
like = frappe.parse_json(like)
doc = frappe.get_doc(reference_doctype, reference_name)
if doc.disable_feedback == 1:
ref_doc = frappe.get_doc(reference_doctype, reference_name)
if ref_doc.disable_feedback == 1:
return
filters = {
@ -33,7 +33,7 @@ def give_feedback(reference_doctype, reference_name, like):
doc.save(ignore_permissions=True)
subject = _('Feedback on {0}: {1}').format(reference_doctype, reference_name)
send_mail(doc, subject)
ref_doc.enable_email_notification and send_mail(doc, subject)
return doc
def send_mail(feedback, subject):

View file

@ -195,9 +195,9 @@ data-fieldname="{{ df.fieldname }}" data-fieldtype="{{ df.fieldtype }}"
{% else %}
<div class="print-heading">
<h2>
<div>{{ doc.select_print_heading or (doc.print_heading if doc.print_heading != None
<div>{{ _(doc.select_print_heading) or (_(doc.print_heading) if doc.print_heading != None
else _(doc.doctype)) }}</div>
<small class="sub-heading">{{ doc.sub_heading if doc.sub_heading != None
<small class="sub-heading">{{ _(doc.sub_heading) if doc.sub_heading != None
else doc.name }}</small>
</h2>
</div>

View file

@ -11,14 +11,7 @@ from frappe.utils.boilerplate import make_boilerplate
class TestBoilerPlate(unittest.TestCase):
@classmethod
def tearDownClass(cls):
bench_path = frappe.utils.get_bench_path()
test_app_dir = os.path.join(bench_path, "apps", "test_app")
if os.path.exists(test_app_dir):
shutil.rmtree(test_app_dir)
def test_create_app(self):
def setUpClass(cls):
title = "Test App"
description = "This app's description contains 'single quotes' and \"double quotes\"."
publisher = "Test Publisher"
@ -27,7 +20,7 @@ class TestBoilerPlate(unittest.TestCase):
color = ""
app_license = "MIT"
user_input = [
cls.user_input = [
title,
description,
publisher,
@ -37,22 +30,21 @@ class TestBoilerPlate(unittest.TestCase):
app_license,
]
bench_path = frappe.utils.get_bench_path()
apps_dir = os.path.join(bench_path, "apps")
app_name = "test_app"
cls.bench_path = frappe.utils.get_bench_path()
cls.apps_dir = os.path.join(cls.bench_path, "apps")
cls.app_names = ("test_app", "test_app_no_git")
cls.gitignore_file = ".gitignore"
cls.git_folder = ".git"
with patch("builtins.input", side_effect=user_input):
make_boilerplate(apps_dir, app_name)
root_paths = [
app_name,
cls.root_paths = [
"requirements.txt",
"README.md",
"setup.py",
"license.txt",
".git",
cls.git_folder,
cls.gitignore_file
]
paths_inside_app = [
cls.paths_inside_app = [
"__init__.py",
"hooks.py",
"patches.txt",
@ -60,25 +52,68 @@ class TestBoilerPlate(unittest.TestCase):
"www",
"config",
"modules.txt",
"public",
app_name,
"public"
]
new_app_dir = os.path.join(bench_path, apps_dir, app_name)
@classmethod
def tearDownClass(cls):
test_app_dirs = (os.path.join(cls.bench_path, "apps", app_name) for app_name in cls.app_names)
for test_app_dir in test_app_dirs:
if os.path.exists(test_app_dir):
shutil.rmtree(test_app_dir)
def test_create_app(self):
with patch("builtins.input", side_effect=self.user_input):
make_boilerplate(self.apps_dir, self.app_names[0])
new_app_dir = os.path.join(self.bench_path, self.apps_dir, self.app_names[0])
paths = self.get_paths(new_app_dir, self.app_names[0])
for path in paths:
self.assertTrue(
os.path.exists(path),
msg=f"{path} should exist in {self.app_names[0]} app"
)
self.check_parsable_python_files(new_app_dir)
def test_create_app_without_git_init(self):
with patch("builtins.input", side_effect=self.user_input):
make_boilerplate(self.apps_dir, self.app_names[1], no_git=True)
new_app_dir = os.path.join(self.apps_dir, self.app_names[1])
paths = self.get_paths(new_app_dir, self.app_names[1])
for path in paths:
if os.path.basename(path) in (self.git_folder, self.gitignore_file):
self.assertFalse(
os.path.exists(path),
msg=f"{path} shouldn't exist in {self.app_names[1]} app"
)
else:
self.assertTrue(
os.path.exists(path),
msg=f"{path} should exist in {self.app_names[1]} app"
)
self.check_parsable_python_files(new_app_dir)
def get_paths(self, app_dir, app_name):
all_paths = list()
for path in root_paths:
all_paths.append(os.path.join(new_app_dir, path))
for path in self.root_paths:
all_paths.append(os.path.join(app_dir, path))
for path in paths_inside_app:
all_paths.append(os.path.join(new_app_dir, app_name, path))
all_paths.append(os.path.join(app_dir, app_name))
for path in all_paths:
self.assertTrue(os.path.exists(path), msg=f"{path} should exist in new app")
for path in self.paths_inside_app:
all_paths.append(os.path.join(app_dir, app_name, path))
return all_paths
def check_parsable_python_files(self, app_dir):
# check if python files are parsable
python_files = glob.glob(new_app_dir + "**/*.py", recursive=True)
python_files = glob.glob(app_dir + "**/*.py", recursive=True)
for python_file in python_files:
with open(python_file) as p:

View file

@ -5,6 +5,7 @@ import gzip
import json
import os
import shlex
import shutil
import subprocess
import sys
import unittest
@ -102,14 +103,24 @@ def exists_in_backup(doctypes, file):
class BaseTestCommands(unittest.TestCase):
def execute(self, command, kwargs=None):
site = {"site": frappe.local.site}
cmd_input = None
if kwargs:
cmd_input = kwargs.get("cmd_input", None)
if cmd_input:
if not isinstance(cmd_input, bytes):
raise Exception(
f"The input should be of type bytes, not {type(cmd_input).__name__}"
)
del kwargs["cmd_input"]
kwargs.update(site)
else:
kwargs = site
self.command = " ".join(command.split()).format(**kwargs)
print("{0}$ {1}{2}".format(color.silver, self.command, color.nc))
command = shlex.split(self.command)
self._proc = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
self._proc = subprocess.run(command, input=cmd_input, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
self.stdout = clean(self._proc.stdout)
self.stderr = clean(self._proc.stderr)
self.returncode = clean(self._proc.returncode)
@ -466,6 +477,28 @@ class TestCommands(BaseTestCommands):
self.assertEqual(self.returncode, 0)
self.assertEqual(check_password('Administrator', 'test2'), 'Administrator')
def test_make_app(self):
user_input = [
b"Test App", # title
b"This app's description contains 'single quotes' and \"double quotes\".", # description
b"Test Publisher", # publisher
b"example@example.org", # email
b"", # icon
b"", # color
b"MIT" # app_license
]
app_name = "testapp0"
apps_path = os.path.join(frappe.utils.get_bench_path(), "apps")
test_app_path = os.path.join(apps_path, app_name)
self.execute(f"bench make-app {apps_path} {app_name}", {"cmd_input": b'\n'.join(user_input)})
self.assertEqual(self.returncode, 0)
self.assertTrue(
os.path.exists(test_app_path)
)
# cleanup
shutil.rmtree(test_app_path)
class RemoveAppUnitTests(unittest.TestCase):
def test_delete_modules(self):

View file

@ -34,7 +34,21 @@ class TestDB(unittest.TestCase):
self.assertEqual(frappe.db.sql("""SELECT name FROM `tabUser` WHERE name >= 't' ORDER BY MODIFIED DESC""")[0][0],
frappe.db.get_value("User", {"name": [">=", "t"]}))
self.assertIn("concat_ws", frappe.db.get_value("User", filters={"name": "Administrator"}, fieldname=Concat_ws(" ", "LastName"), run=False).lower())
self.assertIn(
"concat_ws",
frappe.db.get_value(
"User",
filters={"name": "Administrator"},
fieldname=Concat_ws(" ", "LastName"),
run=False,
).lower(),
)
self.assertEqual(
frappe.db.sql("select email from tabUser where name='Administrator' order by modified DESC"),
frappe.db.get_values(
"User", filters=[["name", "=", "Administrator"]], fieldname="email"
),
)
def test_set_value(self):
todo1 = frappe.get_doc(dict(doctype='ToDo', description = 'test_set_value 1')).insert()

View file

@ -157,7 +157,7 @@ class TestDocument(unittest.TestCase):
def test_varchar_length(self):
d = self.test_insert()
d.subject = "abcde"*100
d.sender = "abcde"*100 + "@user.com"
self.assertRaises(frappe.CharacterLengthExceededError, d.save)
def test_xss_filter(self):
@ -251,4 +251,4 @@ class TestDocument(unittest.TestCase):
'doctype': 'Test Formatted',
'currency': 100000
})
self.assertEquals(d.get_formatted('currency', currency='INR', format="#,###.##"), '₹ 100,000.00')
self.assertEquals(d.get_formatted('currency', currency='INR', format="#,###.##"), '₹ 100,000.00')

View file

@ -7,6 +7,7 @@ from frappe.utils import now_datetime
from frappe.model.naming import getseries
from frappe.model.naming import append_number_if_name_exists, revert_series_if_last
from frappe.model.naming import determine_consecutive_week_number, parse_naming_series
class TestNaming(unittest.TestCase):
def tearDown(self):
@ -60,6 +61,34 @@ class TestNaming(unittest.TestCase):
self.assertEqual(todo.name, 'TODO-{month}-{status}-{series}'.format(
month=now_datetime().strftime('%m'), status=todo.status, series=series))
def test_format_autoname_for_consecutive_week_number(self):
'''
Test if braced params are replaced for consecutive week number in format autoname
'''
doctype = 'ToDo'
todo_doctype = frappe.get_doc('DocType', doctype)
todo_doctype.autoname = 'format:TODO-{WW}-{##}'
todo_doctype.save()
description = 'Format'
todo = frappe.new_doc(doctype)
todo.description = description
todo.insert()
series = getseries('', 2)
series = str(int(series)-1)
if len(series) < 2:
series = '0' + series
week = determine_consecutive_week_number(now_datetime())
self.assertEqual(todo.name, 'TODO-{week}-{series}'.format(
week=week, series=series))
def test_revert_series(self):
from datetime import datetime
year = datetime.now().year
@ -150,3 +179,32 @@ class TestNaming(unittest.TestCase):
self.assertEqual(amended_doc.name, "{}-CANC-1".format(original_name))
submittable_doctype.delete()
def test_parse_naming_series_for_consecutive_week_number(self):
week = determine_consecutive_week_number(now_datetime())
name = parse_naming_series('PREFIX-.WW.-SUFFIX')
expected_name = 'PREFIX-{}-SUFFIX'.format(week)
self.assertEqual(name, expected_name)
def test_determine_consecutive_week_number(self):
from datetime import datetime
dt = datetime.fromisoformat("2019-12-31")
w = determine_consecutive_week_number(dt)
self.assertEqual(w, "53")
dt = datetime.fromisoformat("2020-01-01")
w = determine_consecutive_week_number(dt)
self.assertEqual(w, "01")
dt = datetime.fromisoformat("2020-01-15")
w = determine_consecutive_week_number(dt)
self.assertEqual(w, "03")
dt = datetime.fromisoformat("2021-01-01")
w = determine_consecutive_week_number(dt)
self.assertEqual(w, "00")
dt = datetime.fromisoformat("2021-12-31")
w = determine_consecutive_week_number(dt)
self.assertEqual(w, "52")

View file

@ -0,0 +1,22 @@
import unittest
import frappe
from frappe.www.printview import get_html_and_style
class PrintViewTest(unittest.TestCase):
def test_print_view_without_errors(self):
user = frappe.get_last_doc("User")
messages_before = frappe.get_message_log()
ret = get_html_and_style(doc=user.as_json(), print_format="Standard", no_letterhead=1)
messages_after = frappe.get_message_log()
if len(messages_after) > len(messages_before):
new_messages = messages_after[len(messages_before):]
self.fail("Print view showing error/warnings: \n"
+ "\n".join(str(msg) for msg in new_messages))
# html should exist
self.assertTrue(bool(ret["html"]))

View file

@ -3,7 +3,7 @@
import frappe, os, re, git
from frappe.utils import touch_file, cstr
def make_boilerplate(dest, app_name):
def make_boilerplate(dest, app_name, no_git=False):
if not os.path.exists(dest):
print("Destination directory does not exist")
return
@ -63,9 +63,6 @@ def make_boilerplate(dest, app_name):
with open(os.path.join(dest, hooks.app_name, "MANIFEST.in"), "w") as f:
f.write(frappe.as_unicode(manifest_template.format(**hooks)))
with open(os.path.join(dest, hooks.app_name, ".gitignore"), "w") as f:
f.write(frappe.as_unicode(gitignore_template.format(app_name = hooks.app_name)))
with open(os.path.join(dest, hooks.app_name, "requirements.txt"), "w") as f:
f.write("# frappe -- https://github.com/frappe/frappe is installed via 'bench init'")
@ -98,11 +95,16 @@ def make_boilerplate(dest, app_name):
with open(os.path.join(dest, hooks.app_name, hooks.app_name, "config", "docs.py"), "w") as f:
f.write(frappe.as_unicode(docs_template.format(**hooks)))
# initialize git repository
app_directory = os.path.join(dest, hooks.app_name)
app_repo = git.Repo.init(app_directory)
app_repo.git.add(A=True)
app_repo.index.commit("feat: Initialize App")
if not no_git:
with open(os.path.join(dest, hooks.app_name, ".gitignore"), "w") as f:
f.write(frappe.as_unicode(gitignore_template.format(app_name = hooks.app_name)))
# initialize git repository
app_repo = git.Repo.init(app_directory)
app_repo.git.add(A=True)
app_repo.index.commit("feat: Initialize App")
print("'{app}' created at {path}".format(app=app_name, path=app_directory))

View file

@ -868,7 +868,7 @@ def fmt_money(amount, precision=None, currency=None, format=None):
if currency and frappe.defaults.get_global_default("hide_currency_symbol") != "Yes":
symbol = frappe.db.get_value("Currency", currency, "symbol", cache=True) or currency
amount = symbol + " " + amount
amount = frappe._(symbol) + " " + amount
return amount

View file

@ -95,7 +95,7 @@ def prepare_options(html, options):
'quiet': None,
# 'no-outline': None,
'encoding': "UTF-8",
#'load-error-handling': 'ignore'
# 'load-error-handling': 'ignore'
})
if not options.get("margin-right"):
@ -111,8 +111,21 @@ def prepare_options(html, options):
options.update(get_cookie_options())
# page size
if not options.get("page-size"):
options['page-size'] = frappe.db.get_single_value("Print Settings", "pdf_page_size") or "A4"
pdf_page_size = (
options.get("page-size")
or frappe.db.get_single_value("Print Settings", "pdf_page_size")
or "A4"
)
if pdf_page_size == "Custom":
options["page-height"] = options.get("page-height") or frappe.db.get_single_value(
"Print Settings", "pdf_page_height"
)
options["page-width"] = options.get("page-width") or frappe.db.get_single_value(
"Print Settings", "pdf_page_width"
)
else:
options["page-size"] = pdf_page_size
return html, options

View file

@ -11,7 +11,7 @@ base_template_path = "www/printview.html"
standard_format = "templates/print_formats/standard.html"
@frappe.whitelist()
def download_multi_pdf(doctype, name, format=None, no_letterhead=0):
def download_multi_pdf(doctype, name, format=None, no_letterhead=False, options=None):
"""
Concatenate multiple docs as PDF .
@ -54,18 +54,21 @@ def download_multi_pdf(doctype, name, format=None, no_letterhead=0):
import json
output = PdfFileWriter()
if isinstance(options, str):
options = json.loads(options)
if not isinstance(doctype, dict):
result = json.loads(name)
# Concatenating pdf files
for i, ss in enumerate(result):
output = frappe.get_print(doctype, ss, format, as_pdf = True, output = output, no_letterhead=no_letterhead)
output = frappe.get_print(doctype, ss, format, as_pdf=True, output=output, no_letterhead=no_letterhead, pdf_options=options)
frappe.local.response.filename = "{doctype}.pdf".format(doctype=doctype.replace(" ", "-").replace("/", "-"))
else:
for doctype_name in doctype:
for doc_name in doctype[doctype_name]:
try:
output = frappe.get_print(doctype_name, doc_name, format, as_pdf = True, output = output, no_letterhead=no_letterhead)
output = frappe.get_print(doctype_name, doc_name, format, as_pdf=True, output=output, no_letterhead=no_letterhead, pdf_options=options)
except Exception:
frappe.log_error("Permission Error on doc {} of doctype {}".format(doc_name, doctype_name))
frappe.local.response.filename = "{}.pdf".format(name)

View file

@ -17,6 +17,7 @@
"published",
"featured",
"hide_cta",
"enable_email_notification",
"disable_comments",
"disable_feedback",
"section_break_5",
@ -197,6 +198,13 @@
"fieldname": "disable_feedback",
"fieldtype": "Check",
"label": "Disable Feedback"
},
{
"default": "1",
"description": "Enable email notification for any comment or feedback on your Blog Post.",
"fieldname": "enable_email_notification",
"fieldtype": "Check",
"label": "Enable Email Notification"
}
],
"has_web_view": 1,
@ -206,7 +214,7 @@
"is_published_field": "published",
"links": [],
"max_attachments": 5,
"modified": "2021-09-13 17:19:35.436045",
"modified": "2021-11-23 10:42:01.759723",
"modified_by": "Administrator",
"module": "Website",
"name": "Blog Post",
@ -240,4 +248,4 @@
"sort_order": "ASC",
"title_field": "title",
"track_changes": 1
}
}

View file

@ -104,7 +104,7 @@ class BlogPost(WebsiteGenerator):
context.parents = [{"name": _("Home"), "route":"/"},
{"name": "Blog", "route": "/blog"},
{"label": context.category.title, "route":context.category.route}]
context.guest_allowed = frappe.db.get_single_value("Blog Settings", "allow_guest_to_comment", cache=True)
context.guest_allowed = frappe.db.get_single_value("Blog Settings", "allow_guest_to_comment")
def fetch_cta(self):
if frappe.db.get_single_value("Blog Settings", "show_cta_in_blog", cache=True):

View file

@ -112,7 +112,7 @@
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2017-02-20 13:33:44.011509",
"modified": "2021-11-22 17:56:40.495232",
"modified_by": "Administrator",
"module": "Workflow",
"name": "Workflow State",
@ -137,6 +137,10 @@
"share": 1,
"submit": 0,
"write": 1
},
{
"role": "All",
"select": 1
}
],
"quick_entry": 1,

View file

@ -3,32 +3,22 @@
{%- block title -%}{{_("Not Found")}}{%- endblock -%}
{% block page_content %}
<style>
.hero-and-content {
background-color: #f5f7fa;
}
header, footer {
display: none;
}
html, body {
background-color: #f5f7fa;
}
{% include "templates/styles/card_style.css" %}
</style>
<script>
window.is_404 = true;
</script>
<div class='page-card'>
<div class='page-card-head'>
<span class='indicator gray'>{{_("Page Missing or Moved")}}</span>
<div class="error-page">
<img class="img-404" src="/assets/frappe/images/ui-states/404.png" />
<div>
<h2 class="mb-1 mt-4">
{{ _("There's nothing here") }}
</h2>
<div class="text-muted error-text">
{{ _("The page you are looking for have gone missing.") }}
</div>
<div class="mt-6 back-to-home"><a href='/' class='btn btn-primary'>{{ _("Back to Home") }}</a></div>
</div>
<p>{{_("The page you are looking for is missing. This could be because it is moved or there is a typo in the link.")}}</p>
<div><a href='/' class='btn btn-primary btn-sm'>{{ _("Home") }}</a></div>
</div>
<p class='text-muted text-center small' style='margin-top: -20px;'>{{ _("Error Code: {0}").format('404') }}</p>
<style>
.hero-and-content {
background-color: #f5f7fa;
}
</style>
{% endblock %}

View file

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html data-theme="{{ desk_theme.lower() }}" dir={{ layout_direction }} lang="{{ lang }}">
<html data-theme-mode="{{ desk_theme.lower() }}" data-theme="{{ desk_theme.lower() }}" dir={{ layout_direction }} lang="{{ lang }}">
<head>
<!-- Chrome, Firefox OS and Opera -->
<meta name="theme-color" content="#0089FF">