Merge branch 'frappe:develop' into multiple_imap_folder
This commit is contained in:
commit
ecdaeffcbd
69 changed files with 741 additions and 408 deletions
1
.github/workflows/patch-mariadb-tests.yml
vendored
1
.github/workflows/patch-mariadb-tests.yml
vendored
|
|
@ -10,6 +10,7 @@ concurrency:
|
|||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
|
||||
name: Patch Test
|
||||
|
||||
|
|
|
|||
3
.github/workflows/server-mariadb-tests.yml
vendored
3
.github/workflows/server-mariadb-tests.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
1
.github/workflows/server-postgres-tests.yml
vendored
1
.github/workflows/server-postgres-tests.yml
vendored
|
|
@ -13,6 +13,7 @@ concurrency:
|
|||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
|
|
|||
1
.github/workflows/ui-tests.yml
vendored
1
.github/workflows/ui-tests.yml
vendored
|
|
@ -13,6 +13,7 @@ concurrency:
|
|||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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":
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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 {}
|
||||
|
|
|
|||
|
|
@ -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": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
BIN
frappe/public/images/ui-states/404.png
Normal file
BIN
frappe/public/images/ui-states/404.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 94 KiB |
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
];
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 () {
|
||||
|
|
|
|||
|
|
@ -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")) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
|
|
@ -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"));
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
18
frappe/public/scss/website/error-state.scss
Normal file
18
frappe/public/scss/website/error-state.scss
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -26,6 +26,7 @@
|
|||
@import 'doc';
|
||||
@import 'navbar';
|
||||
@import 'footer';
|
||||
@import 'error-state';
|
||||
|
||||
.ql-editor.read-mode {
|
||||
padding: 0;
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
# Copyright (c) 2021, Frappe Technologies and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
class TestEnergyPointSettings(unittest.TestCase):
|
||||
pass
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
22
frappe/tests/test_printview.py
Normal file
22
frappe/tests/test_printview.py
Normal 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"]))
|
||||
|
|
@ -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))
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
@ -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">
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue