Merge branch 'develop' into feat-improve-a11y

This commit is contained in:
Ankush Menat 2023-12-03 12:09:05 +05:30
commit eb9d126cd8
237 changed files with 2050 additions and 1494 deletions

View file

@ -132,6 +132,7 @@ jobs:
env:
BEFORE: ${{ env.GITHUB_EVENT_PATH.before }}
AFTER: ${{ env.GITHUB_EVENT_PATH.after }}
FRAPPE_SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
TYPE: server
DB: ${{ matrix.db }}
@ -142,6 +143,7 @@ jobs:
SITE: test_site
CI_BUILD_ID: ${{ github.run_id }}
BUILD_NUMBER: ${{ matrix.container }}
FRAPPE_SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
TOTAL_BUILDS: 2
COVERAGE_RCFILE: /home/runner/frappe-bench/apps/frappe/.coveragerc

View file

@ -120,6 +120,7 @@ jobs:
env:
BEFORE: ${{ env.GITHUB_EVENT_PATH.before }}
AFTER: ${{ env.GITHUB_EVENT_PATH.after }}
FRAPPE_SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
TYPE: ui
DB: mariadb

View file

@ -2,7 +2,11 @@ context("Awesome Bar", () => {
before(() => {
cy.visit("/login");
cy.login();
cy.visit("/app/website");
cy.visit("/app/todo"); // Make sure ToDo filters are cleared.
cy.clear_filters();
cy.visit("/app/blog-post"); // Make sure Blog Post filters are cleared.
cy.clear_filters();
cy.visit("/app/website"); // Go to some other page.
});
beforeEach(() => {
@ -11,36 +15,61 @@ context("Awesome Bar", () => {
cy.get("@awesome_bar").type("{selectall}");
});
after(() => {
cy.visit("/app/todo"); // Make sure we're not bleeding any filters to the next spec.
cy.clear_filters();
});
it("navigates to doctype list", () => {
cy.get("@awesome_bar").type("todo");
cy.wait(100);
cy.wait(100); // Wait a bit before hitting enter.
cy.get(".awesomplete").findByRole("listbox").should("be.visible");
cy.get("@awesome_bar").type("{enter}");
cy.get(".title-text").should("contain", "To Do");
cy.location("pathname").should("eq", "/app/todo");
});
it("find text in doctype list", () => {
it("finds text in doctype list", () => {
cy.get("@awesome_bar").type("test in todo");
cy.wait(100);
cy.wait(150); // Wait a bit before hitting enter.
cy.get("@awesome_bar").type("{enter}");
cy.get(".title-text").should("contain", "To Do");
cy.wait(200);
const name_filter = cy.get('[data-original-title="ID"] > input');
name_filter.should("have.value", "%test%");
cy.clear_filters();
cy.wait(200); // Wait a bit longer before checking the filter.
cy.get('[data-original-title="ID"] > input').should("have.value", "%test%");
});
it("filter preserved, now finds something else", () => {
cy.visit("/app/todo");
cy.get(".title-text").should("contain", "To Do");
cy.wait(200); // Wait a bit longer before checking the filter.
cy.get('[data-original-title="ID"] > input').as("filter");
cy.get("@filter").should("have.value", "%test%");
cy.get("@awesome_bar").type("anothertest in todo");
cy.wait(200); // Wait a bit longer before hitting enter.
cy.get("@awesome_bar").type("{enter}");
cy.wait(200); // Wait a bit longer before checking the filter.
cy.get("@filter").should("have.value", "%anothertest%");
});
it("navigates to another doctype, filter not bleeding", () => {
cy.get("@awesome_bar").type("blog post");
cy.wait(150); // Wait a bit before hitting enter.
cy.get("@awesome_bar").type("{enter}");
cy.get(".title-text").should("contain", "Blog Post");
cy.wait(200); // Wait a bit longer before checking the filter.
cy.location("search").should("be.empty");
});
it("navigates to new form", () => {
cy.get("@awesome_bar").type("new blog post");
cy.wait(100);
cy.wait(150); // Wait a bit before hitting enter
cy.get("@awesome_bar").type("{enter}");
cy.get(".title-text:visible").should("have.text", "New Blog Post");
});
it("calculates math expressions", () => {
cy.get("@awesome_bar").type("55 + 32");
cy.wait(100);
cy.wait(150); // Wait a bit before hitting enter
cy.get("@awesome_bar").type("{downarrow}{enter}");
cy.get(".modal-title").should("contain", "Result");
cy.get(".msgprint").should("contain", "55 + 32 = 87");

View file

@ -152,7 +152,7 @@ context("Control Link", () => {
cy.get(".frappe-control[data-fieldname=link] input").focus().as("input");
cy.wait("@search_link");
cy.get("@input").type("todo for link");
cy.get("@input").type("todo for link", { delay: 200 });
cy.wait("@search_link");
cy.get(".frappe-control[data-fieldname=link] ul").should("be.visible");
cy.get(".frappe-control[data-fieldname=link] input").type("{enter}", { delay: 100 });
@ -260,7 +260,7 @@ context("Control Link", () => {
cy.get(".frappe-control[data-fieldname=link] input").focus().as("input");
cy.wait("@search_link");
cy.get("@input").type("Sonstiges", { delay: 100 });
cy.get("@input").type("Sonstiges", { delay: 200 });
cy.wait("@search_link");
cy.get(".frappe-control[data-fieldname=link] ul").should("be.visible");
cy.get(".frappe-control[data-fieldname=link] input").type("{enter}", { delay: 100 });
@ -291,7 +291,7 @@ context("Control Link", () => {
cy.get(".frappe-control[data-fieldname=link] input").focus().as("input");
cy.wait("@search_link");
cy.get("@input").type("Non-Conforming", { delay: 100 });
cy.get("@input").type("Non-Conforming", { delay: 200 });
cy.wait("@search_link");
cy.get(".frappe-control[data-fieldname=link] ul").should("be.visible");
cy.get(".frappe-control[data-fieldname=link] input").type("{enter}", { delay: 100 });

View file

@ -6,6 +6,10 @@ context("Control Phone", () => {
cy.visit("/app/website");
});
afterEach(() => {
cy.clear_dialogs();
});
function get_dialog_with_phone() {
return cy.dialog({
title: "Phone",
@ -20,31 +24,37 @@ context("Control Phone", () => {
it("should set flag and data", () => {
get_dialog_with_phone().as("dialog");
cy.get(".selected-phone").click();
cy.wait(100);
cy.get(".phone-picker .phone-wrapper[id='afghanistan']").click();
cy.wait(100);
cy.get(".selected-phone .country").should("have.text", "+93");
cy.get(".selected-phone > img").should("have.attr", "src").and("include", "/af.svg");
cy.get(".selected-phone").click();
cy.wait(100);
cy.get(".phone-picker .phone-wrapper[id='india']").click();
cy.wait(100);
cy.get(".selected-phone .country").should("have.text", "+91");
cy.get(".selected-phone > img").should("have.attr", "src").and("include", "/in.svg");
let phone_number = "9312672712";
cy.get(".selected-phone > img").click().first();
cy.get_field("phone").first().click({ multiple: true });
cy.get_field("phone").first().click();
cy.get(".frappe-control[data-fieldname=phone]")
.findByRole("textbox")
.first()
.type(phone_number, { force: true });
.type(phone_number);
cy.get_field("phone").first().should("have.value", phone_number);
cy.get_field("phone").first().blur({ force: true });
cy.get_field("phone").first().blur();
cy.wait(100);
cy.get("@dialog").then((dialog) => {
let value = dialog.get_value("phone");
expect(value).to.equal("+91-" + phone_number);
});
});
it("case insensitive search for country and clear search", () => {
let search_text = "india";
cy.get(".selected-phone").click().first();
cy.get(".phone-picker").get(".search-phones").click().type(search_text);

View file

@ -116,50 +116,6 @@ context("Form", () => {
cy.get_field("location").should("have.value", "Bermuda");
});
it("let user undo/redo field value changes", { scrollBehavior: false }, () => {
const undo = () => cy.get("body").type("{esc}").type("{ctrl+z}").wait(500);
const redo = () => cy.get("body").type("{esc}").type("{ctrl+y}").wait(500);
cy.new_form("User");
jump_to_field("Email");
type_value("admin@example.com");
jump_to_field("Username");
type_value("admin42");
jump_to_field("Send Welcome Email");
cy.focused().uncheck();
// make a mistake
jump_to_field("Username");
type_value("admin24");
// undo behaviour
undo();
cy.get_field("username").should("have.value", "admin42");
// redo behaviour
redo();
cy.get_field("username").should("have.value", "admin24");
// undo everything & redo everything, ensure same values at the end
undo();
undo();
undo();
undo();
redo();
redo();
redo();
redo();
cy.compare_document({
username: "admin24",
email: "admin@example.com",
send_welcome_email: 0,
});
});
it("update docfield property using set_df_property in child table", () => {
cy.visit("/app/contact/Test Form Contact 1");
cy.window()

View file

@ -76,6 +76,11 @@ context("MultiSelectDialog", () => {
});
it("tests more button", () => {
cy.get_open_dialog()
.get(`.frappe-control[data-fieldname="search_term"]`)
.find('input[data-fieldname="search_term"]')
.should("exist")
.type("Test", { delay: 200 });
cy.get_open_dialog()
.get(`.frappe-control[data-fieldname="more_child_btn"]`)
.should("exist")

View file

@ -1,4 +1,4 @@
context("Permissions API", () => {
context.skip("Permissions API", () => {
before(() => {
cy.visit("/login");
cy.remove_role("frappe@example.com", "System Manager");

View file

@ -1,37 +0,0 @@
context("Theme Switcher Shortcut", () => {
before(() => {
cy.login();
cy.visit("/app");
});
beforeEach(() => {
cy.reload();
});
it("Check Toggle", () => {
cy.open_theme_dialog();
cy.get(".modal-backdrop").should("exist");
cy.intercept("POST", "/api/method/frappe.core.doctype.user.user.switch_theme").as(
"set_theme"
);
cy.findByText("Timeless Night").click();
cy.wait("@set_theme");
cy.close_theme("{ctrl+shift+g}");
cy.get(".modal-backdrop").should("not.exist");
});
it("Check Enter", () => {
cy.open_theme_dialog();
cy.intercept("POST", "/api/method/frappe.core.doctype.user.user.switch_theme").as(
"set_theme"
);
cy.findByText("Frappe Light").click();
cy.wait("@set_theme");
cy.close_theme("{enter}");
cy.get(".modal-backdrop").should("not.exist");
});
});
Cypress.Commands.add("open_theme_dialog", () => {
cy.get("body").type("{ctrl+shift+g}");
});
Cypress.Commands.add("close_theme", (shortcut_keys) => {
cy.get(".modal-header").type(shortcut_keys);
});

View file

@ -1,91 +0,0 @@
import custom_submittable_doctype from "../fixtures/custom_submittable_doctype";
context("Timeline", () => {
before(() => {
cy.visit("/login");
cy.login();
});
it("Adding new ToDo, adding new comment, verifying comment addition & deletion and deleting ToDo", () => {
//Adding new ToDo
cy.new_form("ToDo");
cy.get('[data-fieldname="description"] .ql-editor.ql-blank')
.type("Test ToDo", { force: true })
.wait(200);
cy.get(".page-head .page-actions").findByRole("button", { name: "Save" }).click();
cy.go_to_list("ToDo");
cy.clear_filters();
cy.click_listview_row_item(0);
//To check if the comment box is initially empty and tying some text into it
cy.get('[data-fieldname="comment"] .ql-editor')
.should("contain", "")
.type("Testing Timeline");
//Adding new comment
cy.get(".comment-box").findByRole("button", { name: "Comment" }).click();
//To check if the commented text is visible in the timeline content
cy.get(".timeline-content").should("contain", "Testing Timeline");
//Editing comment
cy.click_timeline_action_btn("Edit");
cy.get('.timeline-content [data-fieldname="comment"] .ql-editor').first().type(" 123");
cy.click_timeline_action_btn("Save");
//To check if the edited comment text is visible in timeline content
cy.get(".timeline-content").should("contain", "Testing Timeline 123");
//Discarding comment
cy.click_timeline_action_btn("Edit");
cy.click_timeline_action_btn("Dismiss");
//To check if after discarding the timeline content is same as previous
cy.get(".timeline-content").should("contain", "Testing Timeline 123");
//Deleting the added comment
cy.get(".timeline-message-box .more-actions > .action-btn").click(); //Menu button in timeline item
cy.get(".timeline-message-box .more-actions .dropdown-item")
.contains("Delete")
.click({ force: true });
cy.get_open_dialog().findByRole("button", { name: "Yes" }).click({ force: true });
cy.get(".timeline-content").should("not.contain", "Testing Timeline 123");
});
it("Timeline should have submit and cancel activity information", () => {
cy.visit("/app/doctype");
//Creating custom doctype
cy.insert_doc("DocType", custom_submittable_doctype, true);
cy.visit("/app/custom-submittable-doctype");
cy.click_listview_primary_button("Add Custom Submittable DocType");
//Adding a new entry for the created custom doctype
cy.fill_field("title", "Test");
cy.click_modal_primary_button("Save");
cy.click_modal_primary_button("Submit");
cy.visit("/app/custom-submittable-doctype");
cy.click_listview_row_item(0);
//To check if the submission of the documemt is visible in the timeline content
cy.get(".timeline-content").should("contain", "You submitted this document");
cy.get('[id="page-Custom Submittable DocType"] .page-actions')
.findByRole("button", { name: "Cancel" })
.click();
cy.get_open_dialog().findByRole("button", { name: "Yes" }).click();
//To check if the cancellation of the documemt is visible in the timeline content
cy.get(".timeline-content").should("contain", "You cancelled this document");
//Deleting the document
cy.visit("/app/custom-submittable-doctype");
cy.select_listview_row_checkbox(0);
cy.get(".page-actions").findByRole("button", { name: "Actions" }).click();
cy.get('.page-actions .actions-btn-group [data-label="Delete"]').click();
cy.click_modal_primary_button("Yes");
});
});

View file

@ -43,7 +43,7 @@ from .utils.jinja import (
render_template,
)
__version__ = "15.0.0-dev"
__version__ = "16.0.0-dev"
__title__ = "Frappe Framework"
controllers = {}
@ -59,6 +59,32 @@ if _dev_server:
warnings.simplefilter("always", DeprecationWarning)
warnings.simplefilter("always", PendingDeprecationWarning)
# Always initialize sentry SDK if the DSN is sent
if sentry_dsn := os.getenv("FRAPPE_SENTRY_DSN"):
import sentry_sdk
from sentry_sdk.integrations.argv import ArgvIntegration
from sentry_sdk.integrations.atexit import AtexitIntegration
from sentry_sdk.integrations.dedupe import DedupeIntegration
from sentry_sdk.integrations.excepthook import ExcepthookIntegration
from sentry_sdk.integrations.modules import ModulesIntegration
from frappe.utils.sentry import before_send
sentry_sdk.init(
dsn=sentry_dsn,
before_send=before_send,
release=__version__,
auto_enabling_integrations=False,
default_integrations=False,
integrations=[
AtexitIntegration(),
ExcepthookIntegration(),
DedupeIntegration(),
ModulesIntegration(),
ArgvIntegration(),
],
)
class _dict(dict):
"""dict like object that exposes keys as attributes"""
@ -163,6 +189,7 @@ if TYPE_CHECKING: # pragma: no cover
from frappe.database.mariadb.database import MariaDBDatabase
from frappe.database.postgres.database import PostgresDatabase
from frappe.email.doctype.email_queue.email_queue import EmailQueue
from frappe.model.document import Document
from frappe.query_builder.builder import MariaDB, Postgres
from frappe.utils.redis_wrapper import RedisWrapper
@ -237,7 +264,7 @@ def init(site: str, sites_path: str = ".", new_site: bool = False, force=False)
local.jloader = None
local.cache = {}
local.form_dict = _dict()
local.preload_assets = {"style": [], "script": []}
local.preload_assets = {"style": [], "script": [], "icons": []}
local.session = _dict()
local.dev_server = _dev_server
local.qb = get_query_builder(local.conf.db_type)
@ -448,6 +475,8 @@ def msgprint(
primary_action: str = None,
is_minimizable: bool = False,
wide: bool = False,
*,
realtime=False,
) -> None:
"""Print a message to the user (via HTTP response).
Messages are sent in the `__server_messages` property in the
@ -461,6 +490,7 @@ def msgprint(
:param primary_action: [optional] Bind a primary server/client side action.
:param is_minimizable: [optional] Allow users to minimize the modal
:param wide: [optional] Show wide modal
:param realtime: Publish message immediately using websocket.
"""
import inspect
import sys
@ -527,7 +557,10 @@ def msgprint(
if wide:
out.wide = wide
message_log.append(out)
if realtime:
publish_realtime(event="msgprint", message=out)
else:
message_log.append(out)
_raise_exception()
@ -659,7 +692,7 @@ def sendmail(
print_letterhead=False,
with_container=False,
email_read_tracker_url=None,
):
) -> Optional["EmailQueue"]:
"""Send email using user's default **Email Account** or global default **Email Account**.
@ -744,7 +777,7 @@ def sendmail(
)
# build email queue and send the email if send_now is True.
builder.process(send_now=now)
return builder.process(send_now=now)
whitelisted = []

View file

@ -22,7 +22,7 @@ import frappe.rate_limiter
import frappe.recorder
import frappe.utils.response
from frappe import _
from frappe.auth import SAFE_HTTP_METHODS, UNSAFE_HTTP_METHODS, HTTPRequest, validate_auth
from frappe.auth import SAFE_HTTP_METHODS, UNSAFE_HTTP_METHODS, HTTPRequest, validate_auth # noqa
from frappe.middlewares import StaticDataMiddleware
from frappe.utils import CallbackManager, cint, get_site_name
from frappe.utils.data import escape_html

View file

@ -202,7 +202,7 @@ def start_scheduler():
def start_worker(
queue, quiet=False, rq_username=None, rq_password=None, burst=False, strategy=None
):
"""Start a backgrond worker"""
"""Start a background worker"""
from frappe.utils.background_jobs import start_worker
start_worker(
@ -225,7 +225,7 @@ def start_worker(
@click.option("--quiet", is_flag=True, default=False, help="Hide Log Outputs")
@click.option("--burst", is_flag=True, default=False, help="Run Worker in Burst mode.")
def start_worker_pool(queue, quiet=False, num_workers=2, burst=False):
"""Start a backgrond worker"""
"""Start a pool of background workers"""
from frappe.utils.background_jobs import start_worker_pool
start_worker_pool(

View file

@ -31,6 +31,7 @@
"additional_info",
"communication_date",
"read_receipt",
"send_after",
"column_break_14",
"sender_full_name",
"read_by_recipient",
@ -125,7 +126,7 @@
"fieldtype": "Select",
"hidden": 1,
"label": "Delivery Status",
"options": "\nSent\nBounced\nOpened\nMarked As Spam\nRejected\nDelayed\nSoft-Bounced\nClicked\nRecipient Unsubscribed\nError\nExpired\nSending\nRead"
"options": "\nSent\nBounced\nOpened\nMarked As Spam\nRejected\nDelayed\nSoft-Bounced\nClicked\nRecipient Unsubscribed\nError\nExpired\nSending\nRead\nScheduled"
},
{
"fieldname": "section_break_8",
@ -390,12 +391,17 @@
"hidden": 1,
"label": "IMAP Folder",
"read_only": 1
},
{
"fieldname": "send_after",
"fieldtype": "Datetime",
"label": "Send After"
}
],
"icon": "fa fa-comment",
"idx": 1,
"links": [],
"modified": "2023-08-29 17:20:52.541483",
"modified": "2023-11-27 20:38:27.467076",
"modified_by": "Administrator",
"module": "Core",
"name": "Communication",

View file

@ -87,6 +87,7 @@ class Communication(Document, CommunicationEmailMixin):
"Expired",
"Sending",
"Read",
"Scheduled",
]
email_account: DF.Link | None
email_status: DF.Literal["Open", "Spam", "Trash"]
@ -106,6 +107,7 @@ class Communication(Document, CommunicationEmailMixin):
reference_name: DF.DynamicLink | None
reference_owner: DF.ReadOnly | None
seen: DF.Check
send_after: DF.Datetime | None
sender: DF.Data | None
sender_full_name: DF.Data | None
sent_or_received: DF.Literal["Sent", "Received"]
@ -162,6 +164,9 @@ class Communication(Document, CommunicationEmailMixin):
self.seen = 1
self.sent_or_received = "Sent"
if not self.send_after: # Handle empty string, always set NULL
self.send_after = None
validate_email(self)
if self.communication_medium == "Email":
@ -342,6 +347,9 @@ class Communication(Document, CommunicationEmailMixin):
else:
self.status = "Closed"
if self.send_after and self.is_new():
self.delivery_status = "Scheduled"
def mark_email_as_spam(self):
if (
self.communication_type == "Communication"

View file

@ -46,6 +46,7 @@ def make(
print_letterhead=True,
email_template=None,
communication_type=None,
send_after=None,
**kwargs,
) -> dict[str, str]:
"""Make a new communication. Checks for email permissions for specified Document.
@ -64,6 +65,7 @@ def make(
:param attachments: List of File names or dicts with keys "fname" and "fcontent"
:param send_me_a_copy: Send a copy to the sender (default **False**).
:param email_template: Template which is used to compose mail .
:param send_after: Send after the given datetime.
"""
if kwargs:
from frappe.utils.commands import warn
@ -99,6 +101,7 @@ def make(
email_template=email_template,
communication_type=communication_type,
add_signature=False,
send_after=send_after,
)
@ -124,6 +127,7 @@ def _make(
email_template=None,
communication_type=None,
add_signature=True,
send_after=None,
) -> dict[str, str]:
"""Internal method to make a new communication that ignores Permission checks."""
@ -151,6 +155,7 @@ def _make(
"read_receipt": read_receipt,
"has_attachment": 1 if attachments else 0,
"communication_type": communication_type,
"send_after": send_after,
}
)
comm.flags.skip_add_signature = not add_signature

View file

@ -290,6 +290,7 @@ class CommunicationEmailMixin:
"read_receipt": self.read_receipt,
"is_notification": (self.sent_or_received == "Received"),
"print_letterhead": print_letterhead,
"send_after": self.send_after,
}
def send_email(

View file

@ -184,7 +184,7 @@ def remove_file_by_url(file_url: str, doctype: str = None, name: str = None) ->
def get_content_hash(content: bytes | str) -> str:
if isinstance(content, str):
content = content.encode()
return hashlib.md5(content).hexdigest() # nosec
return hashlib.md5(content, usedforsecurity=False).hexdigest() # nosec
def generate_file_name(name: str, suffix: str | None = None, is_private: bool = False) -> str:

View file

@ -10,20 +10,6 @@ from frappe.model.document import Document
from frappe.utils import cint
from frappe.utils.caching import site_cache
DEFAULT_LOGTYPES_RETENTION = {
"Error Log": 30,
"Activity Log": 90,
"Email Queue": 30,
"Scheduled Job Log": 90,
"Route History": 90,
"Submission Queue": 30,
"Prepared Report": 30,
"Webhook Request Log": 30,
"Integration Request": 90,
"Unhandled Email": 30,
"Reminder": 30,
}
@runtime_checkable
class LogType(Protocol):
@ -81,12 +67,14 @@ class LogSettings(Document):
def add_default_logtypes(self):
existing_logtypes = {d.ref_doctype for d in self.logs_to_clear}
added_logtypes = set()
for logtype, retention in DEFAULT_LOGTYPES_RETENTION.items():
default_logtypes_retention = frappe.get_hooks("default_log_clearing_doctypes", {})
for logtype, retentions in default_logtypes_retention.items():
if logtype not in existing_logtypes and _supports_log_clearing(logtype):
if not frappe.db.exists("DocType", logtype):
continue
self.append("logs_to_clear", {"ref_doctype": logtype, "days": cint(retention)})
self.append("logs_to_clear", {"ref_doctype": logtype, "days": cint(retentions[-1])})
added_logtypes.add(logtype)
if added_logtypes:

View file

@ -62,6 +62,8 @@
"reqd": 1
},
{
"fetch_from": "ref_doctype.module",
"fetch_if_empty": 1,
"fieldname": "module",
"fieldtype": "Link",
"label": "Module",

View file

@ -0,0 +1 @@
Log of SMS sent via SMS Center.

View file

View file

@ -0,0 +1,6 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on("SMS Log", {
refresh: function (frm) {},
});

View file

@ -0,0 +1,371 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"autoname": "SYS-SMS-.#####",
"beta": 0,
"creation": "2012-03-27 14:36:47",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"editable_grid": 0,
"fields": [
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "sender_name",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Sender Name",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"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
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "sent_on",
"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": "Sent On",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"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
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break0",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"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,
"width": "50%"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "message",
"fieldtype": "Small Text",
"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": "Message",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"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
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "sec_break1",
"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,
"options": "Simple",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "no_of_requested_sms",
"fieldtype": "Int",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "No of Requested SMS",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"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
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "requested_numbers",
"fieldtype": "Code",
"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": "Requested Numbers",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"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
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break1",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"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,
"width": "50%"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "no_of_sent_sms",
"fieldtype": "Int",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "No of Sent SMS",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"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
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "sent_to",
"fieldtype": "Code",
"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": "Sent To",
"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
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"icon": "fa fa-mobile-phone",
"idx": 1,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2018-08-21 16:15:40.898889",
"modified_by": "Administrator",
"module": "Core",
"name": "SMS Log",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"cancel": 0,
"create": 0,
"delete": 0,
"email": 1,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 0,
"submit": 0,
"write": 0
}
],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"track_changes": 1,
"track_seen": 0,
"track_views": 0
}

View file

@ -0,0 +1,26 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
from frappe.model.document import Document
class SMSLog(Document):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from frappe.types import DF
message: DF.SmallText | None
no_of_requested_sms: DF.Int
no_of_sent_sms: DF.Int
requested_numbers: DF.Code | None
sender_name: DF.Data | None
sent_on: DF.Date | None
sent_to: DF.Code | None
# end: auto-generated types
pass

View file

@ -0,0 +1,10 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import unittest
# test_records = frappe.get_test_records('SMS Log')
class TestSMSLog(unittest.TestCase):
pass

View file

@ -23,38 +23,23 @@
"float_precision",
"currency_precision",
"rounding_method",
"sec_backup_limit",
"backup_limit",
"encrypt_backup",
"background_workers",
"enable_scheduler",
"dormant_days",
"permissions",
"apply_strict_user_permissions",
"column_break_21",
"allow_guests_to_upload_files",
"force_web_capture_mode_for_uploads",
"allow_older_web_view_links",
"security_tab",
"security",
"session_expiry",
"document_share_key_expiry",
"column_break_13",
"column_break_txqh",
"deny_multiple_sessions",
"disable_user_pass_login",
"login_methods_section",
"allow_login_using_mobile_number",
"allow_login_using_user_name",
"disable_user_pass_login",
"column_break_uhqk",
"login_with_email_link",
"login_with_email_link_expiry",
"allow_error_traceback",
"strip_exif_metadata_from_uploaded_images",
"allow_older_web_view_links",
"password_settings",
"logout_on_password_reset",
"force_user_to_reset_password",
"reset_password_link_expiry_duration",
"password_reset_limit",
"column_break_31",
"enable_password_policy",
"minimum_password_score",
"brute_force_security",
"allow_consecutive_login_attempts",
"column_break_34",
@ -66,6 +51,16 @@
"two_factor_method",
"lifespan_qrcode_image",
"otp_issuer_name",
"password_tab",
"password_settings",
"logout_on_password_reset",
"force_user_to_reset_password",
"reset_password_link_expiry_duration",
"password_reset_limit",
"column_break_31",
"enable_password_policy",
"minimum_password_score",
"email_tab",
"email",
"email_footer_address",
"email_retry_limit",
@ -75,17 +70,31 @@
"attach_view_link",
"welcome_email_template",
"reset_password_template",
"prepared_report_section",
"max_auto_email_report_per_user",
"files_tab",
"files_section",
"max_file_size",
"allow_guests_to_upload_files",
"force_web_capture_mode_for_uploads",
"strip_exif_metadata_from_uploaded_images",
"column_break_uqma",
"allowed_file_extensions",
"updates_tab",
"system_updates_section",
"disable_system_update_notification",
"disable_change_log_notification",
"backups_tab",
"sec_backup_limit",
"backup_limit",
"encrypt_backup",
"advanced_tab",
"prepared_report_section",
"max_auto_email_report_per_user",
"background_workers",
"enable_scheduler",
"dormant_days",
"telemetry_section",
"enable_telemetry",
"files_section",
"max_file_size",
"column_break_uqma",
"allowed_file_extensions"
"allow_error_traceback",
"enable_telemetry"
],
"fields": [
{
@ -126,7 +135,6 @@
"read_only": 1
},
{
"collapsible": 1,
"fieldname": "date_and_number_format",
"fieldtype": "Section Break",
"label": "Date and Number Format"
@ -171,10 +179,8 @@
"options": "\n0\n1\n2\n3\n4\n5\n6\n7\n8\n9"
},
{
"collapsible": 1,
"fieldname": "sec_backup_limit",
"fieldtype": "Section Break",
"label": "Backups"
"fieldtype": "Section Break"
},
{
"default": "3",
@ -184,7 +190,6 @@
"label": "Number of Backups"
},
{
"collapsible": 1,
"fieldname": "background_workers",
"fieldtype": "Section Break",
"label": "Background Workers"
@ -198,7 +203,6 @@
"label": "Enable Scheduled Jobs"
},
{
"collapsible": 1,
"fieldname": "permissions",
"fieldtype": "Section Break",
"label": "Permissions"
@ -211,10 +215,8 @@
"label": "Apply Strict User Permissions"
},
{
"collapsible": 1,
"fieldname": "security",
"fieldtype": "Section Break",
"label": "Security"
"fieldtype": "Section Break"
},
{
"default": "170:00",
@ -223,10 +225,6 @@
"fieldtype": "Data",
"label": "Session Expiry (idle timeout)"
},
{
"fieldname": "column_break_13",
"fieldtype": "Column Break"
},
{
"default": "0",
"description": "Note: Multiple sessions will be allowed in case of mobile device",
@ -255,7 +253,6 @@
"label": "Show Full Error and Allow Reporting of Issues to the Developer"
},
{
"collapsible": 1,
"fieldname": "password_settings",
"fieldtype": "Section Break",
"label": "Password"
@ -286,7 +283,6 @@
"options": "2\n3\n4"
},
{
"collapsible": 1,
"fieldname": "brute_force_security",
"fieldtype": "Section Break",
"label": "Brute Force Security"
@ -309,7 +305,6 @@
"label": "Allow Login After Fail"
},
{
"collapsible": 1,
"fieldname": "two_factor_authentication",
"fieldtype": "Section Break",
"label": "Two Factor Authentication"
@ -338,6 +333,7 @@
},
{
"default": "OTP App",
"depends_on": "enable_two_factor_auth",
"description": "Choose authentication method to be used by all users",
"fieldname": "two_factor_method",
"fieldtype": "Select",
@ -345,7 +341,7 @@
"options": "OTP App\nSMS\nEmail"
},
{
"depends_on": "eval:doc.two_factor_method == \"OTP App\"",
"depends_on": "eval:doc.enable_two_factor_auth && doc.two_factor_method == \"OTP App\"",
"description": "Time in seconds to retain QR code image on server. Min:<strong>240</strong>",
"fieldname": "lifespan_qrcode_image",
"fieldtype": "Int",
@ -359,10 +355,8 @@
"label": "OTP Issuer Name"
},
{
"collapsible": 1,
"fieldname": "email",
"fieldtype": "Section Break",
"label": "Email"
"fieldtype": "Section Break"
},
{
"description": "Your organization name and address for the email footer.",
@ -430,7 +424,6 @@
"label": "Include Web View Link in Email"
},
{
"collapsible": 1,
"fieldname": "prepared_report_section",
"fieldtype": "Section Break",
"label": "Reports"
@ -456,10 +449,8 @@
"label": "Encrypt Backups"
},
{
"collapsible": 1,
"fieldname": "system_updates_section",
"fieldtype": "Section Break",
"label": "System Updates"
"fieldtype": "Section Break"
},
{
"default": "0",
@ -547,7 +538,6 @@
"label": "Disable Document Sharing"
},
{
"collapsible": 1,
"fieldname": "telemetry_section",
"fieldtype": "Section Break",
"label": "Telemetry"
@ -578,10 +568,8 @@
"label": "Force Web Capture Mode for Uploads"
},
{
"collapsible": 1,
"fieldname": "files_section",
"fieldtype": "Section Break",
"label": "Files"
"fieldtype": "Section Break"
},
{
"fieldname": "max_file_size",
@ -598,12 +586,60 @@
"fieldname": "allowed_file_extensions",
"fieldtype": "Small Text",
"label": "Allowed File Extensions"
},
{
"fieldname": "security_tab",
"fieldtype": "Tab Break",
"label": "Login"
},
{
"fieldname": "email_tab",
"fieldtype": "Tab Break",
"label": "Email"
},
{
"fieldname": "files_tab",
"fieldtype": "Tab Break",
"label": "Files"
},
{
"fieldname": "updates_tab",
"fieldtype": "Tab Break",
"label": "Updates"
},
{
"fieldname": "backups_tab",
"fieldtype": "Tab Break",
"label": "Backups"
},
{
"fieldname": "advanced_tab",
"fieldtype": "Tab Break",
"label": "Advanced"
},
{
"fieldname": "password_tab",
"fieldtype": "Tab Break",
"label": "Password"
},
{
"fieldname": "column_break_txqh",
"fieldtype": "Column Break"
},
{
"fieldname": "login_methods_section",
"fieldtype": "Section Break",
"label": "Login Methods"
},
{
"fieldname": "column_break_uhqk",
"fieldtype": "Column Break"
}
],
"icon": "fa fa-cog",
"issingle": 1,
"links": [],
"modified": "2023-10-17 16:12:28.145496",
"modified": "2023-11-27 14:08:01.927794",
"modified_by": "Administrator",
"module": "Core",
"name": "System Settings",

View file

@ -93,7 +93,6 @@ class SystemSettings(Document):
time_zone: DF.Literal
two_factor_method: DF.Literal["OTP App", "SMS", "Email"]
welcome_email_template: DF.Link | None
# end: auto-generated types
def validate(self):
from frappe.twofactor import toggle_two_factor_auth

View file

@ -1227,27 +1227,31 @@ def create_contact(user, ignore_links=False, ignore_mandatory=False):
contact_name = get_contact_name(user.email)
if not contact_name:
contact = frappe.get_doc(
{
"doctype": "Contact",
"first_name": user.first_name,
"last_name": user.last_name,
"user": user.name,
"gender": user.gender,
}
)
try:
contact = frappe.get_doc(
{
"doctype": "Contact",
"first_name": user.first_name,
"last_name": user.last_name,
"user": user.name,
"gender": user.gender,
}
)
if user.email:
contact.add_email(user.email, is_primary=True)
if user.email:
contact.add_email(user.email, is_primary=True)
if user.phone:
contact.add_phone(user.phone, is_primary_phone=True)
if user.phone:
contact.add_phone(user.phone, is_primary_phone=True)
if user.mobile_no:
contact.add_phone(user.mobile_no, is_primary_mobile_no=True)
contact.insert(
ignore_permissions=True, ignore_links=ignore_links, ignore_mandatory=ignore_mandatory
)
if user.mobile_no:
contact.add_phone(user.mobile_no, is_primary_mobile_no=True)
contact.insert(
ignore_permissions=True, ignore_links=ignore_links, ignore_mandatory=ignore_mandatory
)
except frappe.DuplicateEntryError:
pass
else:
contact = frappe.get_doc("Contact", contact_name)
contact.first_name = user.first_name

View file

@ -84,7 +84,7 @@ frappe.ui.form.on("Customize Form", {
if (!in_list(["Table", "Table MultiSelect"], f.fieldtype)) return;
frm.add_custom_button(
f.options,
__(f.options),
() => frm.set_value("doc_type", f.options),
__("Customize Child Table")
);
@ -97,7 +97,7 @@ frappe.ui.form.on("Customize Form", {
if (frm.doc.doc_type) {
frappe.model.with_doctype(frm.doc.doc_type).then(() => {
frm.page.set_title(__("Customize Form - {0}", [frm.doc.doc_type]));
frm.page.set_title(__("Customize Form - {0}", [__(frm.doc.doc_type)]));
frappe.customize_form.set_primary_action(frm);
frm.add_custom_button(

View file

@ -205,7 +205,7 @@
"label": "Permissions"
},
{
"description": "This field will appear only if the fieldname defined here has value OR the rules are true (examples): \nmyfield\neval:doc.myfield=='My Value'\neval:doc.age&gt;18",
"description": "This field will appear only if the fieldname defined here has value OR the rules are true (examples):\nmyfield\neval:doc.myfield=='My Value'\neval:doc.age&gt;18",
"fieldname": "depends_on",
"fieldtype": "Code",
"label": "Depends On",

View file

@ -46,8 +46,29 @@ class BulkUpdate(Document):
@frappe.whitelist()
def submit_cancel_or_update_docs(doctype, docnames, action="submit", data=None):
docnames = frappe.parse_json(docnames)
if isinstance(docnames, str):
docnames = frappe.parse_json(docnames)
if len(docnames) < 20:
return _bulk_action(doctype, docnames, action, data)
elif len(docnames) <= 500:
frappe.msgprint(_("Bulk operation is enqueued in background."), alert=True)
frappe.enqueue(
_bulk_action,
doctype=doctype,
docnames=docnames,
action=action,
data=data,
queue="short",
timeout=1000,
)
else:
frappe.throw(
_("Bulk operations only support up to 500 documents."), title=_("Too Many Documents")
)
def _bulk_action(doctype, docnames, action, data):
if data:
data = frappe.parse_json(data)
@ -85,5 +106,4 @@ def submit_cancel_or_update_docs(doctype, docnames, action="submit", data=None):
def show_progress(docnames, message, i, description):
n = len(docnames)
if n >= 10:
frappe.publish_progress(float(i) * 100 / n, title=message, description=description)
frappe.publish_progress(float(i) * 100 / n, title=message, description=description)

View file

@ -0,0 +1,48 @@
# Copyright (c) 2023, Frappe Technologies and Contributors
# See LICENSE
import time
import frappe
from frappe.core.doctype.doctype.test_doctype import new_doctype
from frappe.desk.doctype.bulk_update.bulk_update import submit_cancel_or_update_docs
from frappe.tests.utils import FrappeTestCase, timeout
class TestBulkUpdate(FrappeTestCase):
@classmethod
def setUpClass(cls) -> None:
super().setUpClass()
cls.doctype = new_doctype(is_submittable=1, custom=1).insert().name
frappe.db.commit()
for _ in range(50):
frappe.new_doc(cls.doctype, some_fieldname=frappe.mock("name")).insert()
@timeout()
def wait_for_assertion(self, assertion):
"""Wait till an assertion becomes True"""
while True:
if assertion():
break
time.sleep(0.2)
def test_bulk_submit_in_background(self):
unsubmitted = frappe.get_all(self.doctype, {"docstatus": 0}, limit=5, pluck="name")
failed = submit_cancel_or_update_docs(self.doctype, unsubmitted, action="submit")
self.assertEqual(failed, [])
def check_docstatus(docs, status):
frappe.db.rollback()
matching_docs = frappe.get_all(
self.doctype, {"docstatus": status, "name": ("in", docs)}, pluck="name"
)
return set(matching_docs) == set(docs)
unsubmitted = frappe.get_all(self.doctype, {"docstatus": 0}, limit=20, pluck="name")
submit_cancel_or_update_docs(self.doctype, unsubmitted, action="submit")
self.wait_for_assertion(lambda: check_docstatus(unsubmitted, 1))
submitted = frappe.get_all(self.doctype, {"docstatus": 1}, limit=20, pluck="name")
submit_cancel_or_update_docs(self.doctype, submitted, action="cancel")
self.wait_for_assertion(lambda: check_docstatus(submitted, 2))

View file

@ -209,10 +209,10 @@ def get_chart_config(chart, filters, timespan, timegrain, from_date, to_date):
data = frappe.db.get_list(
doctype,
fields=[f"{datefield} as _unit", f"SUM({value_field})", "COUNT(*)"],
fields=[datefield, f"SUM({value_field})", "COUNT(*)"],
filters=filters,
group_by="_unit",
order_by="_unit asc",
group_by=datefield,
order_by=datefield,
as_list=True,
)

View file

@ -102,8 +102,7 @@
"fieldname": "url",
"fieldtype": "Data",
"in_list_view": 1,
"label": "URL",
"options": "URL"
"label": "URL"
},
{
"depends_on": "eval:doc.doc_view == \"Kanban\"",
@ -116,7 +115,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2023-07-18 16:12:53.546430",
"modified": "2023-11-27 14:13:38.489737",
"modified_by": "Administrator",
"module": "Desk",
"name": "Workspace Shortcut",

View file

@ -196,7 +196,7 @@ frappe.setup.SetupWizard = class SetupWizard extends frappe.ui.Slides {
this.abort_setup(r.message.fail);
}
},
error: () => this.abort_setup("Error in setup"),
error: () => this.abort_setup(),
});
}
@ -213,7 +213,11 @@ frappe.setup.SetupWizard = class SetupWizard extends frappe.ui.Slides {
abort_setup(fail_msg) {
this.$working_state.find(".state-icon-container").html("");
fail_msg = fail_msg ? fail_msg : __("Failed to complete setup");
fail_msg = fail_msg
? fail_msg
: frappe.last_response.setup_wizard_failure_message
? frappe.last_response.setup_wizard_failure_message
: __("Failed to complete setup");
this.update_setup_message("Could not start up: " + fail_msg);
@ -463,7 +467,7 @@ frappe.setup.slides_settings = [
fieldtype: "Data",
options: "Email",
},
{ fieldname: "password", label: __("Password"), fieldtype: "Password" },
{ fieldname: "password", label: __("Password"), fieldtype: "Password", length: 512 },
],
onload: function (slide) {

View file

@ -83,11 +83,14 @@ def process_setup_stages(stages, user_input, is_background_task=False):
task.get("fn")(task.get("args"))
except Exception:
handle_setup_exception(user_input)
message = current_task.get("fail_msg") if current_task else "Failed to complete setup"
frappe.log_error(title=f"Setup failed: {message}")
if not is_background_task:
return {"status": "fail", "fail": current_task.get("fail_msg")}
frappe.response["setup_wizard_failure_message"] = message
raise
frappe.publish_realtime(
"setup_task",
{"status": "fail", "fail_msg": current_task.get("fail_msg")},
{"status": "fail", "fail_msg": message},
user=frappe.session.user,
)
else:

View file

@ -159,6 +159,15 @@ frappe.ui.form.on("Email Account", {
delete frappe.route_flags.delete_user_from_locals;
delete locals["User"][frappe.route_flags.linked_user];
}
if (frappe.boot.developer_mode && !frm.is_dirty() && frm.doc.enable_incoming) {
frm.add_custom_button(__("Pull Emails"), () => {
frm.call({
method: "pull_emails",
args: { email_account: frm.doc.name },
});
});
}
},
authorize_api_access: function (frm) {

View file

@ -831,6 +831,14 @@ def pull(now=False):
)
@frappe.whitelist()
def pull_emails(email_account: str) -> None:
"""Pull emails from given email account."""
frappe.has_permission("Email Account", "read", throw=True)
pull_from_email_account(email_account)
def pull_from_email_account(email_account):
"""Runs within a worker process"""
email_account = frappe.get_doc("Email Account", email_account)

View file

@ -1,6 +1,8 @@
# Copyright (c) 2015, Frappe Technologies and contributors
# License: MIT. See LICENSE
import contextlib
import frappe
from frappe import _
from frappe.model.document import Document
@ -41,7 +43,7 @@ class EmailGroup(Document):
added = 0
for user in frappe.get_all(doctype, [email_field, unsubscribed_field or "name"]):
try:
with contextlib.suppress(frappe.UniqueValidationError, frappe.InvalidEmailAddressError):
email = parse_addr(user.get(email_field))[1] if user.get(email_field) else None
if email:
frappe.get_doc(
@ -52,10 +54,7 @@ class EmailGroup(Document):
"unsubscribed": user.get(unsubscribed_field) if unsubscribed_field else 0,
}
).insert(ignore_permissions=True)
added += 1
except frappe.UniqueValidationError:
pass
frappe.msgprint(_("{0} subscribers added").format(added))

View file

@ -28,6 +28,7 @@
"in_global_search": 1,
"in_list_view": 1,
"label": "Email",
"options": "Email",
"reqd": 1
},
{
@ -40,7 +41,7 @@
}
],
"links": [],
"modified": "2022-07-11 16:38:34.165271",
"modified": "2023-11-25 16:54:59.828669",
"modified_by": "Administrator",
"module": "Email",
"name": "Email Group Member",

View file

@ -687,13 +687,13 @@ class QueueBuilder:
mail.set_in_reply_to(self.in_reply_to)
return mail
def process(self, send_now=False):
def process(self, send_now=False) -> EmailQueue | None:
"""Build and return the email queues those are created.
Sends email incase if it is requested to send now.
"""
final_recipients = self.final_recipients()
queue_separately = (final_recipients and self.queue_separately) or len(final_recipients) > 20
queue_separately = (final_recipients and self.queue_separately) or len(final_recipients) > 100
if not (final_recipients + self.final_cc()):
return []
@ -705,6 +705,7 @@ class QueueBuilder:
recipients = list(set(final_recipients + self.final_cc() + self.bcc))
q = EmailQueue.new({**queue_data, **{"recipients": recipients}}, ignore_permissions=True)
send_now and q.send()
return q
else:
if send_now and len(final_recipients) >= 1000:
# force queueing if there are too many recipients to avoid timeouts

View file

@ -50,10 +50,10 @@ class Newsletter(WebsiteGenerator):
total_recipients: DF.Int
total_views: DF.Int
# end: auto-generated types
def validate(self):
self.route = f"newsletters/{self.name}"
self.validate_sender_address()
self.validate_recipient_address()
self.validate_publishing()
self.validate_scheduling_date()
@ -135,7 +135,6 @@ class Newsletter(WebsiteGenerator):
def validate_newsletter_recipients(self):
if not self.newsletter_recipients:
frappe.throw(_("Newsletter should have atleast one recipient"), exc=NoRecipientFoundError)
self.validate_recipient_address()
def validate_sender_address(self):
"""Validate self.send_from is a valid email address or not."""
@ -145,11 +144,6 @@ class Newsletter(WebsiteGenerator):
f"{self.sender_name} <{self.sender_email}>" if self.sender_name else self.sender_email
)
def validate_recipient_address(self):
"""Validate if self.newsletter_recipients are all valid email addresses or not."""
for recipient in self.newsletter_recipients:
frappe.utils.validate_email_address(recipient, throw=True)
def validate_publishing(self):
if self.send_webview_link and not self.published:
frappe.throw(_("Newsletter must be published to send webview link in email"))
@ -308,11 +302,11 @@ def confirmed_unsubscribe(email, group):
@frappe.whitelist(allow_guest=True)
@rate_limit(limit=10, seconds=60 * 60)
def subscribe(email, email_group=None): # noqa
def subscribe(email, email_group=None):
"""API endpoint to subscribe an email to a particular email group. Triggers a confirmation email."""
if email_group is None:
email_group = _("Website")
email_group = get_default_email_group()
# build subscription confirmation URL
api_endpoint = frappe.utils.get_url(
@ -355,13 +349,16 @@ def subscribe(email, email_group=None): # noqa
@frappe.whitelist(allow_guest=True)
def confirm_subscription(email, email_group=_("Website")): # noqa
def confirm_subscription(email, email_group=None):
"""API endpoint to confirm email subscription.
This endpoint is called when user clicks on the link sent to their mail.
"""
if not verify_request():
return
if email_group is None:
email_group = get_default_email_group()
if not frappe.db.exists("Email Group", email_group):
frappe.get_doc({"doctype": "Email Group", "title": email_group}).insert(ignore_permissions=True)
@ -438,3 +435,7 @@ def newsletter_email_read(recipient_email=None, reference_doctype=None, referenc
finally:
frappe.response.update(frappe.utils.get_imaginary_pixel_response())
def get_default_email_group():
return _("Website", lang=frappe.db.get_default("language"))

View file

@ -31,6 +31,7 @@ app_include_js = [
"report.bundle.js",
"telemetry.bundle.js",
]
app_include_css = [
"desk.bundle.css",
"report.bundle.css",
@ -436,6 +437,22 @@ after_job = [
extend_bootinfo = [
"frappe.utils.telemetry.add_bootinfo",
"frappe.core.doctype.user_permission.user_permission.send_user_permissions",
"frappe.utils.sentry.add_bootinfo",
]
export_python_type_annotations = True
# log doctype cleanups to automatically add in log settings
default_log_clearing_doctypes = {
"Error Log": 30,
"Activity Log": 90,
"Email Queue": 30,
"Scheduled Job Log": 90,
"Route History": 90,
"Submission Queue": 30,
"Prepared Report": 30,
"Webhook Request Log": 30,
"Integration Request": 90,
"Unhandled Email": 30,
"Reminder": 30,
}

View file

@ -67,7 +67,12 @@ def _new_site(
if not db_name:
import hashlib
db_name = "_" + hashlib.sha1(os.path.realpath(frappe.get_site_path()).encode()).hexdigest()[:16]
db_name = (
"_"
+ hashlib.sha1(
os.path.realpath(frappe.get_site_path()).encode(), usedforsecurity=False
).hexdigest()[:16]
)
try:
# enable scheduler post install?

View file

@ -48,8 +48,7 @@ class ConnectedApp(Document):
def validate(self):
base_url = frappe.utils.get_url()
callback_path = (
"/api/method/frappe.integrations.doctype.connected_app.connected_app.callback"
+ f"?app={self.name}"
"/api/method/frappe.integrations.doctype.connected_app.connected_app.callback/" + self.name
)
self.redirect_uri = urljoin(base_url, callback_path)
@ -149,7 +148,7 @@ class ConnectedApp(Document):
@frappe.whitelist(methods=["GET"], allow_guest=True)
def callback(code=None, state=None, app=None):
def callback(code=None, state=None):
"""Handle client's code.
Called during the oauthorization flow by the remote oAuth2 server to
@ -162,7 +161,11 @@ def callback(code=None, state=None, app=None):
frappe.local.response["location"] = "/login?" + urlencode({"redirect-to": frappe.request.url})
return
connected_app = frappe.get_doc("Connected App", app)
path = frappe.request.path[1:].split("/")
if len(path) != 4 or not path[3]:
frappe.throw(_("Invalid Parameters."))
connected_app = frappe.get_doc("Connected App", path[3])
token_cache = frappe.get_doc("Token Cache", connected_app.name + "-" + frappe.session.user)
if state != token_cache.state:

View file

@ -4,60 +4,45 @@
import frappe
def get_all_webhooks():
# query webhooks
webhooks_list = frappe.get_all(
"Webhook",
fields=["name", "condition", "webhook_docevent", "webhook_doctype"],
filters={"enabled": True},
)
# make webhooks map
webhooks = {}
for w in webhooks_list:
webhooks.setdefault(w.webhook_doctype, []).append(w)
return webhooks
def run_webhooks(doc, method):
"""Run webhooks for this method"""
frappe_flags = frappe.local.flags
if (
frappe.flags.in_import
or frappe.flags.in_patch
or frappe.flags.in_install
or frappe.flags.in_migrate
frappe_flags.in_import
or frappe_flags.in_patch
or frappe_flags.in_install
or frappe_flags.in_migrate
):
return
if frappe.flags.webhooks_executed is None:
frappe.flags.webhooks_executed = {}
# TODO: remove this hazardous unnecessary cache in flags
if frappe.flags.webhooks is None:
# load webhooks from cache
webhooks = frappe.cache.get_value("webhooks")
if webhooks is None:
# query webhooks
webhooks_list = frappe.get_all(
"Webhook",
fields=["name", "condition", "webhook_docevent", "webhook_doctype"],
filters={"enabled": True},
)
# make webhooks map for cache
webhooks = {}
for w in webhooks_list:
webhooks.setdefault(w.webhook_doctype, []).append(w)
frappe.cache.set_value("webhooks", webhooks)
frappe.flags.webhooks = webhooks
# load all webhooks from cache / DB
webhooks = frappe.cache.get_value("webhooks", get_all_webhooks)
# get webhooks for this doctype
webhooks_for_doc = frappe.flags.webhooks.get(doc.doctype, None)
webhooks_for_doc = webhooks.get(doc.doctype, None)
if not webhooks_for_doc:
# no webhooks, quit
return
def _webhook_request(webhook):
if webhook.name not in frappe.flags.webhooks_executed.get(doc.name, []):
frappe.enqueue(
"frappe.integrations.doctype.webhook.webhook.enqueue_webhook",
enqueue_after_commit=True,
doc=doc,
webhook=webhook,
)
# keep list of webhooks executed for this doc in this request
# so that we don't run the same webhook for the same document multiple times
# in one request
frappe.flags.webhooks_executed.setdefault(doc.name, []).append(webhook.name)
event_list = ["on_update", "after_insert", "on_submit", "on_cancel", "on_trash"]
if not doc.flags.in_insert:
@ -76,4 +61,52 @@ def run_webhooks(doc, method):
trigger_webhook = True
if trigger_webhook and event and webhook.webhook_docevent == event:
_webhook_request(webhook)
_add_webhook_to_queue(webhook, doc)
def _add_webhook_to_queue(webhook, doc):
# Maintain a queue and flush on commit
if not getattr(frappe.local, "_webhook_queue", None):
frappe.local._webhook_queue = []
frappe.db.after_commit.add(flush_webhook_execution_queue)
frappe.local._webhook_queue.append(frappe._dict(doc=doc, webhook=webhook))
def flush_webhook_execution_queue():
"""Enqueue all pending webhook executions.
Each webhook can trigger multiple times on same document or even different instance of same
document. We assume that last enqueued version of document is the final document for this DB
transaction.
"""
if not getattr(frappe.local, "_webhook_queue", None):
return
uniq_hooks = set()
unique_last_instances = []
# reverse
frappe.local._webhook_queue.reverse()
# deduplicate on (doc.name, webhook.name)
# 'doc' holds the last instance values
for execution in frappe.local._webhook_queue:
key = (execution.webhook.get("name"), execution.doc.get("name"))
if key not in uniq_hooks:
uniq_hooks.add(key)
unique_last_instances.append(execution)
# Clear original queue so next enqueue computation happens correctly.
del frappe.local._webhook_queue
# reverse again, to get back the original order on which to execute webhooks
unique_last_instances.reverse()
for instance in unique_last_instances:
frappe.enqueue(
"frappe.integrations.doctype.webhook.webhook.enqueue_webhook",
doc=instance.doc,
webhook=instance.webhook,
now=frappe.flags.in_test,
)

View file

@ -7,6 +7,7 @@ import responses
from responses.matchers import json_params_matcher
import frappe
from frappe.integrations.doctype.webhook import flush_webhook_execution_queue
from frappe.integrations.doctype.webhook.webhook import (
enqueue_webhook,
get_webhook_data,
@ -96,6 +97,7 @@ class TestWebhook(FrappeTestCase):
self.test_user = frappe.new_doc("User")
self.test_user.email = "user1@integration.webhooks.test.com"
self.test_user.first_name = "user1"
self.test_user.send_welcome_email = False
self.responses = responses.RequestsMock()
self.responses.start()
@ -112,18 +114,19 @@ class TestWebhook(FrappeTestCase):
"""Test webhook trigger for enabled webhooks"""
frappe.cache.delete_value("webhooks")
frappe.flags.webhooks = None
# Insert the user to db
self.test_user.insert()
self.assertTrue("User" in frappe.flags.webhooks)
webhooks = frappe.cache.get_value("webhooks")
self.assertTrue("User" in webhooks)
self.assertEqual(len(webhooks.get("User")), 1)
# only 1 hook (enabled) must be queued
self.assertEqual(len(frappe.flags.webhooks.get("User")), 1)
self.assertTrue(self.test_user.email in frappe.flags.webhooks_executed)
self.assertEqual(
frappe.flags.webhooks_executed.get(self.test_user.email)[0], self.sample_webhooks[0].name
)
self.assertEqual(len(frappe.local._webhook_queue), 1)
execution = frappe.local._webhook_queue[0]
self.assertEqual(execution.webhook.name, self.sample_webhooks[0].name)
self.assertEqual(execution.doc.name, self.test_user.name)
def test_validate_doc_events(self):
"Test creating a submit-related webhook for a non-submittable DocType"
@ -206,7 +209,7 @@ class TestWebhook(FrappeTestCase):
wh_config = {
"doctype": "Webhook",
"webhook_doctype": "Note",
"webhook_docevent": "after_insert",
"webhook_docevent": "on_change",
"enabled": 1,
"request_url": "https://httpbin.org/post",
"request_method": "POST",
@ -223,8 +226,9 @@ class TestWebhook(FrappeTestCase):
doc = frappe.new_doc("Note")
doc.title = "Test Webhook Note"
final_title = frappe.generate_hash()
expected_req = [{"title": doc.title} for _ in range(3)]
expected_req = [{"title": final_title} for _ in range(3)]
self.responses.add(
responses.POST,
"https://httpbin.org/post",
@ -233,8 +237,15 @@ class TestWebhook(FrappeTestCase):
match=[json_params_matcher(expected_req)],
)
with get_test_webhook(wh_config) as wh:
enqueue_webhook(doc, wh)
with get_test_webhook(wh_config):
# It should only execute once in a transaction
doc.insert()
doc.reload()
doc.save()
doc = frappe.get_doc(doc.doctype, doc.name)
doc.title = final_title
doc.save()
flush_webhook_execution_queue()
log = frappe.get_last_doc("Webhook Request Log")
self.assertEqual(len(json.loads(log.response)), 3)

View file

@ -43,6 +43,14 @@ def make_put_request(url, **kwargs):
return make_request("PUT", url, **kwargs)
def make_patch_request(url, **kwargs):
return make_request("PATCH", url, **kwargs)
def make_delete_request(url, **kwargs):
return make_request("DELETE", url, **kwargs)
def create_request_log(
data,
integration_type=None,

View file

@ -344,18 +344,17 @@ class BaseDocument:
if ignore_virtual or fieldname not in self.permitted_fieldnames:
continue
if value is None:
if (prop := getattr(type(self), fieldname, None)) and is_a_property(prop):
value = getattr(self, fieldname)
if (prop := getattr(type(self), fieldname, None)) and is_a_property(prop):
value = getattr(self, fieldname)
elif options := getattr(df, "options", None):
from frappe.utils.safe_exec import get_safe_globals
elif options := getattr(df, "options", None):
from frappe.utils.safe_exec import get_safe_globals
value = frappe.safe_eval(
code=options,
eval_globals=get_safe_globals(),
eval_locals={"doc": self},
)
value = frappe.safe_eval(
code=options,
eval_globals=get_safe_globals(),
eval_locals={"doc": self},
)
if isinstance(value, list) and df.fieldtype not in table_fields:
frappe.throw(_("Value for {0} cannot be a list").format(_(df.label)))

View file

@ -152,6 +152,9 @@ def get_mapped_doc(
True if target_doc.get(target_parentfield) else False
)
if table_map.get("ignore"):
continue
if table_map.get("add_if_empty") and row_exists_for_parentfield.get(target_parentfield):
continue
@ -163,6 +166,7 @@ def get_mapped_doc(
if postprocess:
postprocess(source_doc, target_doc)
ret_doc.run_method("after_mapping", source_doc)
ret_doc.set_onload("load_after_mapping", True)
if (

View file

@ -153,8 +153,11 @@ def remove_orphan_doctypes():
orphan_doctypes = []
clear_controller_cache()
class_overrides = frappe.get_hooks("override_doctype_class", {})
for doctype in doctype_names:
if doctype in class_overrides:
continue
try:
get_controller(doctype=doctype)
except ImportError:

View file

@ -1,6 +1,7 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
import json
from collections import defaultdict
from typing import TYPE_CHECKING, Union
import frappe
@ -233,17 +234,30 @@ def get_workflow_field_value(workflow_name, field):
@frappe.whitelist()
def bulk_workflow_approval(docnames, doctype, action):
from collections import defaultdict
docnames = json.loads(docnames)
if len(docnames) < 20:
_bulk_workflow_action(docnames, doctype, action)
elif len(docnames) <= 500:
frappe.msgprint(_("Bulk {0} is enqueued in background.").format(action), alert=True)
frappe.enqueue(
_bulk_workflow_action,
docnames=docnames,
doctype=doctype,
action=action,
queue="short",
timeout=1000,
)
else:
frappe.throw(_("Bulk approval only support up to 500 documents."), title=_("Too Many Documents"))
def _bulk_workflow_action(docnames, doctype, action):
# dictionaries for logging
failed_transactions = defaultdict(list)
successful_transactions = defaultdict(list)
# WARN: message log is cleared
print("Clearing frappe.message_log...")
frappe.clear_messages()
docnames = json.loads(docnames)
for (idx, docname) in enumerate(docnames, 1):
message_dict = {}
try:
@ -308,7 +322,9 @@ def print_workflow_log(messages, title, doctype, indicator):
html = f"<div>{doc}</div>"
msg += html
frappe.msgprint(msg, title=_("Workflow Status"), indicator=indicator, is_minimizable=True)
frappe.msgprint(
msg, title=_("Workflow Status"), indicator=indicator, is_minimizable=True, realtime=True
)
@frappe.whitelist()

View file

@ -20,7 +20,7 @@ def calculate_hash(path: str) -> str:
Returns:
str: The calculated hash
"""
hash_md5 = hashlib.md5()
hash_md5 = hashlib.md5(usedforsecurity=False)
with open(path, "rb") as f:
for chunk in iter(lambda: f.read(4096), b""):
hash_md5.update(chunk)

View file

@ -84,7 +84,7 @@ class Monitor:
if job := rq.get_current_job():
self.data.uuid = job.id
waitdiff = self.data.timestamp - job.enqueued_at
waitdiff = self.data.timestamp - job.enqueued_at.replace(tzinfo=pytz.UTC)
self.data.job.wait = int(waitdiff.total_seconds() * 1000000)
def add_custom_data(self, **kwargs):

View file

@ -11,7 +11,7 @@ from oauthlib.openid import RequestValidator
import frappe
from frappe.auth import LoginManager
from frappe.utils.data import get_system_timezone
from frappe.utils.data import get_system_timezone, now_datetime
class OAuthWebRequestValidator(RequestValidator):
@ -240,13 +240,7 @@ class OAuthWebRequestValidator(RequestValidator):
def validate_bearer_token(self, token, scopes, request):
# Remember to check expiration and scope membership
otoken = frappe.get_doc("OAuth Bearer Token", token)
token_expiration_local = otoken.expiration_time.replace(
tzinfo=pytz.timezone(get_system_timezone())
)
token_expiration_utc = token_expiration_local.astimezone(pytz.utc)
is_token_valid = (
datetime.datetime.now(pytz.UTC) < token_expiration_utc
) and otoken.status != "Revoked"
is_token_valid = (now_datetime() < otoken.expiration_time) and otoken.status != "Revoked"
client_scopes = frappe.db.get_value("OAuth Client", otoken.client, "scopes").split(
get_url_delimiter()
)

View file

@ -231,3 +231,4 @@ execute:frappe.db.set_single_value("Document Naming Settings", "default_amend_na
frappe.patches.v15_0.move_event_cancelled_to_status
frappe.patches.v15_0.set_file_type
frappe.core.doctype.data_import.patches.remove_stale_docfields_from_legacy_version
frappe.patches.v15_0.validate_newsletter_recipients

View file

@ -0,0 +1,9 @@
import frappe
from frappe.utils import validate_email_address
def execute():
for name, email in frappe.get_all("Email Group Member", fields=["name", "email"], as_list=True):
if not validate_email_address(email, throw=False):
frappe.db.set_value("Email Group Member", name, "unsubscribed", 1)
frappe.db.commit()

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -1,94 +0,0 @@
Copyright (c) 2016-2020 The Inter Project Authors.
"Inter" is trademark of Rasmus Andersson.
https://github.com/rsms/inter
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION AND CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

View file

@ -1,166 +1,33 @@
/* This file is depricated use Inter.scss instead. */
/* Backward compatibility */
@font-face {
font-family: 'Inter V';
font-family: InterVariable;
font-style: normal;
font-weight: 100 900;
font-display: swap;
font-style: normal;
src: url('/assets/frappe/css/fonts/inter/Inter.var.woff2?v=3.19') format('woff2-variations'),
url('/assets/frappe/css/fonts/inter/Inter.var.woff2?v=3.19') format('woff2');
src: url('/assets/frappe/css/fonts/inter/Inter.var.woff2?v=3.19') format('woff2') tech('variations');
unicode-range: U+0460-052F,U+1C80-1C88,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F;
src: url("/assets/frappe/css/fonts/inter/InterVariable.woff2") format("woff2");
}
@font-face {
font-family: 'Inter V';
font-family: InterVariable;
font-style: italic;
font-weight: 100 900;
font-display: swap;
font-style: italic;
src: url('/assets/frappe/css/fonts/inter/Inter-Italic.var.woff2?v=3.19') format('woff2-variations'),
url('/assets/frappe/css/fonts/inter/Inter-Italic.var.woff2?v=3.19') format('woff2');
src: url('/assets/frappe/css/fonts/inter/Inter-Italic.var.woff2?v=3.19') format('woff2') tech('variations');
unicode-range: U+0460-052F,U+1C80-1C88,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F;
src: url("/assets/frappe/css/fonts/inter/InterVariable-Italic.woff2") format("woff2");
}
@font-face {
font-family: 'Inter';
font-display: swap;
font-style: italic;
font-weight: 100;
src: url("/assets/frappe/css/fonts/inter/inter_thinitalic.woff2") format("woff2"),
url("/assets/frappe/css/fonts/inter/inter_thinitalic.woff") format("woff");
}
@font-face {
font-family: 'Inter';
font-display: swap;
font-style: normal;
font-weight: 200;
src: url("/assets/frappe/css/fonts/inter/inter_extralight.woff2") format("woff2"),
url("/assets/frappe/css/fonts/inter/inter_extralight.woff") format("woff");
}
@font-face {
font-family: 'Inter';
font-display: swap;
font-style: italic;
font-weight: 200;
src: url("/assets/frappe/css/fonts/inter/inter_extralightitalic.woff2") format("woff2"),
url("/assets/frappe/css/fonts/inter/inter_extralightitalic.woff") format("woff");
}
@font-face {
font-family: 'Inter';
font-display: swap;
font-style: normal;
font-weight: 300;
src: url("/assets/frappe/css/fonts/inter/inter_light.woff2") format("woff2"),
url("/assets/frappe/css/fonts/inter/inter_light.woff") format("woff");
}
@font-face {
font-family: 'Inter';
font-display: swap;
font-style: italic;
font-weight: 300;
src: url("/assets/frappe/css/fonts/inter/inter_lightitalic.woff2") format("woff2"),
url("/assets/frappe/css/fonts/inter/inter_lightitalic.woff") format("woff");
}
@font-face {
font-family: 'Inter';
font-display: swap;
font-style: normal;
font-weight: 400;
src: url("/assets/frappe/css/fonts/inter/inter_regular.woff2") format("woff2"),
url("/assets/frappe/css/fonts/inter/inter_regular.woff") format("woff");
}
@font-face {
font-family: 'Inter';
font-display: swap;
font-style: italic;
font-weight: 400;
src: url("/assets/frappe/css/fonts/inter/inter_italic.woff2") format("woff2"),
url("/assets/frappe/css/fonts/inter/inter_italic.woff") format("woff");
}
@font-face {
font-family: 'Inter';
font-display: swap;
font-style: normal;
font-weight: 500;
src: url("/assets/frappe/css/fonts/inter/inter_medium.woff2") format("woff2"),
url("/assets/frappe/css/fonts/inter/inter_medium.woff") format("woff");
}
@font-face {
font-family: 'Inter';
font-display: swap;
font-style: italic;
font-weight: 500;
src: url("/assets/frappe/css/fonts/inter/inter_mediumitalic.woff2") format("woff2"),
url("/assets/frappe/css/fonts/inter/inter_mediumitalic.woff") format("woff");
}
@font-face {
font-family: 'Inter';
font-display: swap;
font-style: normal;
font-weight: 600;
src: url("/assets/frappe/css/fonts/inter/inter_semibold.woff2") format("woff2"),
url("/assets/frappe/css/fonts/inter/inter_semibold.woff") format("woff");
}
@font-face {
font-family: 'Inter';
font-display: swap;
font-style: italic;
font-weight: 600;
src: url("/assets/frappe/css/fonts/inter/inter_semibolditalic.woff2") format("woff2"),
url("/assets/frappe/css/fonts/inter/inter_semibolditalic.woff") format("woff");
}
@font-face {
font-family: 'Inter';
font-display: swap;
font-style: normal;
font-weight: 700;
src: url("/assets/frappe/css/fonts/inter/inter_bold.woff2") format("woff2"),
url("/assets/frappe/css/fonts/inter/inter_bold.woff") format("woff");
}
@font-face {
font-family: 'Inter';
font-display: swap;
font-style: italic;
font-weight: 700;
src: url("/assets/frappe/css/fonts/inter/inter_bolditalic.woff2") format("woff2"),
url("/assets/frappe/css/fonts/inter/inter_bolditalic.woff") format("woff");
}
@font-face {
font-family: 'Inter';
font-display: swap;
font-style: normal;
font-weight: 800;
src: url("/assets/frappe/css/fonts/inter/inter_extrabold.woff2") format("woff2"),
url("/assets/frappe/css/fonts/inter/inter_extrabold.woff") format("woff");
}
@font-face {
font-family: 'Inter';
font-display: swap;
font-style: italic;
font-weight: 800;
src: url("/assets/frappe/css/fonts/inter/inter_extrabolditalic.woff2") format("woff2"),
url("/assets/frappe/css/fonts/inter/inter_extrabolditalic.woff") format("woff");
}
@font-face {
font-family: 'Inter';
font-display: swap;
font-style: normal;
font-weight: 900;
src: url("/assets/frappe/css/fonts/inter/inter_black.woff2") format("woff2"),
url("/assets/frappe/css/fonts/inter/inter_black.woff") format("woff");
}
@font-face {
font-family: 'Inter';
font-display: swap;
font-style: italic;
font-weight: 900;
src: url("/assets/frappe/css/fonts/inter/inter_blackitalic.woff2") format("woff2"),
url("/assets/frappe/css/fonts/inter/inter_blackitalic.woff") format("woff");
}
/* static fonts */
@font-face { font-family: "Inter"; font-style: normal; font-weight: 100; font-display: swap; src: url("/assets/frappe/css/fonts/inter/Inter-Thin.woff2") format("woff2"); }
@font-face { font-family: "Inter"; font-style: italic; font-weight: 100; font-display: swap; src: url("/assets/frappe/css/fonts/inter/Inter-ThinItalic.woff2") format("woff2"); }
@font-face { font-family: "Inter"; font-style: normal; font-weight: 200; font-display: swap; src: url("/assets/frappe/css/fonts/inter/Inter-ExtraLight.woff2") format("woff2"); }
@font-face { font-family: "Inter"; font-style: italic; font-weight: 200; font-display: swap; src: url("/assets/frappe/css/fonts/inter/Inter-ExtraLightItalic.woff2") format("woff2"); }
@font-face { font-family: "Inter"; font-style: normal; font-weight: 300; font-display: swap; src: url("/assets/frappe/css/fonts/inter/Inter-Light.woff2") format("woff2"); }
@font-face { font-family: "Inter"; font-style: italic; font-weight: 300; font-display: swap; src: url("/assets/frappe/css/fonts/inter/Inter-LightItalic.woff2") format("woff2"); }
@font-face { font-family: "Inter"; font-style: normal; font-weight: 400; font-display: swap; src: url("/assets/frappe/css/fonts/inter/Inter-Regular.woff2") format("woff2"); }
@font-face { font-family: "Inter"; font-style: italic; font-weight: 400; font-display: swap; src: url("/assets/frappe/css/fonts/inter/Inter-Italic.woff2") format("woff2"); }
@font-face { font-family: "Inter"; font-style: normal; font-weight: 500; font-display: swap; src: url("/assets/frappe/css/fonts/inter/Inter-Medium.woff2") format("woff2"); }
@font-face { font-family: "Inter"; font-style: italic; font-weight: 500; font-display: swap; src: url("/assets/frappe/css/fonts/inter/Inter-MediumItalic.woff2") format("woff2"); }
@font-face { font-family: "Inter"; font-style: normal; font-weight: 600; font-display: swap; src: url("/assets/frappe/css/fonts/inter/Inter-SemiBold.woff2") format("woff2"); }
@font-face { font-family: "Inter"; font-style: italic; font-weight: 600; font-display: swap; src: url("/assets/frappe/css/fonts/inter/Inter-SemiBoldItalic.woff2") format("woff2"); }
@font-face { font-family: "Inter"; font-style: normal; font-weight: 700; font-display: swap; src: url("/assets/frappe/css/fonts/inter/Inter-Bold.woff2") format("woff2"); }
@font-face { font-family: "Inter"; font-style: italic; font-weight: 700; font-display: swap; src: url("/assets/frappe/css/fonts/inter/Inter-BoldItalic.woff2") format("woff2"); }
@font-face { font-family: "Inter"; font-style: normal; font-weight: 800; font-display: swap; src: url("/assets/frappe/css/fonts/inter/Inter-ExtraBold.woff2") format("woff2"); }
@font-face { font-family: "Inter"; font-style: italic; font-weight: 800; font-display: swap; src: url("/assets/frappe/css/fonts/inter/Inter-ExtraBoldItalic.woff2") format("woff2"); }
@font-face { font-family: "Inter"; font-style: normal; font-weight: 900; font-display: swap; src: url("/assets/frappe/css/fonts/inter/Inter-Black.woff2") format("woff2"); }
@font-face { font-family: "Inter"; font-style: italic; font-weight: 900; font-display: swap; src: url("/assets/frappe/css/fonts/inter/Inter-BlackItalic.woff2") format("woff2"); }

View file

@ -1,167 +1 @@
// TODO instead of making copy of inter.css find a way to import it.
// workaround for css import as it fails for custom website_theme_template
@font-face {
font-family: "Inter V";
font-weight: 100 900;
font-display: swap;
font-style: normal;
src: url("/assets/frappe/css/fonts/inter/Inter.var.woff2?v=3.19") format("woff2-variations"),
url("/assets/frappe/css/fonts/inter/Inter.var.woff2?v=3.19") format("woff2");
src: url("/assets/frappe/css/fonts/inter/Inter.var.woff2?v=3.19") format("woff2")
tech("variations");
}
@font-face {
font-family: "Inter V";
font-weight: 100 900;
font-display: swap;
font-style: italic;
src: url("/assets/frappe/css/fonts/inter/Inter-Italic.var.woff2?v=3.19")
format("woff2-variations"),
url("/assets/frappe/css/fonts/inter/Inter-Italic.var.woff2?v=3.19") format("woff2");
src: url("/assets/frappe/css/fonts/inter/Inter-Italic.var.woff2?v=3.19") format("woff2")
tech("variations");
}
@font-face {
font-family: "Inter";
font-display: swap;
font-style: italic;
font-weight: 100;
src: url("/assets/frappe/css/fonts/inter/inter_thinitalic.woff2") format("woff2"),
url("/assets/frappe/css/fonts/inter/inter_thinitalic.woff") format("woff");
}
@font-face {
font-family: "Inter";
font-display: swap;
font-style: normal;
font-weight: 200;
src: url("/assets/frappe/css/fonts/inter/inter_extralight.woff2") format("woff2"),
url("/assets/frappe/css/fonts/inter/inter_extralight.woff") format("woff");
}
@font-face {
font-family: "Inter";
font-display: swap;
font-style: italic;
font-weight: 200;
src: url("/assets/frappe/css/fonts/inter/inter_extralightitalic.woff2") format("woff2"),
url("/assets/frappe/css/fonts/inter/inter_extralightitalic.woff") format("woff");
}
@font-face {
font-family: "Inter";
font-display: swap;
font-style: normal;
font-weight: 300;
src: url("/assets/frappe/css/fonts/inter/inter_light.woff2") format("woff2"),
url("/assets/frappe/css/fonts/inter/inter_light.woff") format("woff");
}
@font-face {
font-family: "Inter";
font-display: swap;
font-style: italic;
font-weight: 300;
src: url("/assets/frappe/css/fonts/inter/inter_lightitalic.woff2") format("woff2"),
url("/assets/frappe/css/fonts/inter/inter_lightitalic.woff") format("woff");
}
@font-face {
font-family: "Inter";
font-display: swap;
font-style: normal;
font-weight: 400;
src: url("/assets/frappe/css/fonts/inter/inter_regular.woff2") format("woff2"),
url("/assets/frappe/css/fonts/inter/inter_regular.woff") format("woff");
}
@font-face {
font-family: "Inter";
font-display: swap;
font-style: italic;
font-weight: 400;
src: url("/assets/frappe/css/fonts/inter/inter_italic.woff2") format("woff2"),
url("/assets/frappe/css/fonts/inter/inter_italic.woff") format("woff");
}
@font-face {
font-family: "Inter";
font-display: swap;
font-style: normal;
font-weight: 500;
src: url("/assets/frappe/css/fonts/inter/inter_medium.woff2") format("woff2"),
url("/assets/frappe/css/fonts/inter/inter_medium.woff") format("woff");
}
@font-face {
font-family: "Inter";
font-display: swap;
font-style: italic;
font-weight: 500;
src: url("/assets/frappe/css/fonts/inter/inter_mediumitalic.woff2") format("woff2"),
url("/assets/frappe/css/fonts/inter/inter_mediumitalic.woff") format("woff");
}
@font-face {
font-family: "Inter";
font-display: swap;
font-style: normal;
font-weight: 600;
src: url("/assets/frappe/css/fonts/inter/inter_semibold.woff2") format("woff2"),
url("/assets/frappe/css/fonts/inter/inter_semibold.woff") format("woff");
}
@font-face {
font-family: "Inter";
font-display: swap;
font-style: italic;
font-weight: 600;
src: url("/assets/frappe/css/fonts/inter/inter_semibolditalic.woff2") format("woff2"),
url("/assets/frappe/css/fonts/inter/inter_semibolditalic.woff") format("woff");
}
@font-face {
font-family: "Inter";
font-display: swap;
font-style: normal;
font-weight: 700;
src: url("/assets/frappe/css/fonts/inter/inter_bold.woff2") format("woff2"),
url("/assets/frappe/css/fonts/inter/inter_bold.woff") format("woff");
}
@font-face {
font-family: "Inter";
font-display: swap;
font-style: italic;
font-weight: 700;
src: url("/assets/frappe/css/fonts/inter/inter_bolditalic.woff2") format("woff2"),
url("/assets/frappe/css/fonts/inter/inter_bolditalic.woff") format("woff");
}
@font-face {
font-family: "Inter";
font-display: swap;
font-style: normal;
font-weight: 800;
src: url("/assets/frappe/css/fonts/inter/inter_extrabold.woff2") format("woff2"),
url("/assets/frappe/css/fonts/inter/inter_extrabold.woff") format("woff");
}
@font-face {
font-family: "Inter";
font-display: swap;
font-style: italic;
font-weight: 800;
src: url("/assets/frappe/css/fonts/inter/inter_extrabolditalic.woff2") format("woff2"),
url("/assets/frappe/css/fonts/inter/inter_extrabolditalic.woff") format("woff");
}
@font-face {
font-family: "Inter";
font-display: swap;
font-style: normal;
font-weight: 900;
src: url("/assets/frappe/css/fonts/inter/inter_black.woff2") format("woff2"),
url("/assets/frappe/css/fonts/inter/inter_black.woff") format("woff");
}
@font-face {
font-family: "Inter";
font-display: swap;
font-style: italic;
font-weight: 900;
src: url("/assets/frappe/css/fonts/inter/inter_blackitalic.woff2") format("woff2"),
url("/assets/frappe/css/fonts/inter/inter_blackitalic.woff") format("woff");
}
@import "frappe/public/css/fonts/inter/inter.css";

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