Merge branch 'develop' into feat-improve-a11y
This commit is contained in:
commit
eb9d126cd8
237 changed files with 2050 additions and 1494 deletions
2
.github/workflows/server-tests.yml
vendored
2
.github/workflows/server-tests.yml
vendored
|
|
@ -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
|
||||
|
||||
|
|
|
|||
1
.github/workflows/ui-tests.yml
vendored
1
.github/workflows/ui-tests.yml
vendored
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
context("Permissions API", () => {
|
||||
context.skip("Permissions API", () => {
|
||||
before(() => {
|
||||
cy.visit("/login");
|
||||
cy.remove_role("frappe@example.com", "System Manager");
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
|
|
@ -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 = []
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -62,6 +62,8 @@
|
|||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "ref_doctype.module",
|
||||
"fetch_if_empty": 1,
|
||||
"fieldname": "module",
|
||||
"fieldtype": "Link",
|
||||
"label": "Module",
|
||||
|
|
|
|||
1
frappe/core/doctype/sms_log/README.md
Normal file
1
frappe/core/doctype/sms_log/README.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
Log of SMS sent via SMS Center.
|
||||
0
frappe/core/doctype/sms_log/__init__.py
Normal file
0
frappe/core/doctype/sms_log/__init__.py
Normal file
6
frappe/core/doctype/sms_log/sms_log.js
Normal file
6
frappe/core/doctype/sms_log/sms_log.js
Normal 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) {},
|
||||
});
|
||||
371
frappe/core/doctype/sms_log/sms_log.json
Normal file
371
frappe/core/doctype/sms_log/sms_log.json
Normal 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
|
||||
}
|
||||
26
frappe/core/doctype/sms_log/sms_log.py
Normal file
26
frappe/core/doctype/sms_log/sms_log.py
Normal 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
|
||||
10
frappe/core/doctype/sms_log/test_sms_log.py
Normal file
10
frappe/core/doctype/sms_log/test_sms_log.py
Normal 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
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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>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>18",
|
||||
"fieldname": "depends_on",
|
||||
"fieldtype": "Code",
|
||||
"label": "Depends On",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
48
frappe/desk/doctype/bulk_update/test_bulk_update.py
Normal file
48
frappe/desk/doctype/bulk_update/test_bulk_update.py
Normal 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))
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)))
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
9
frappe/patches/v15_0/validate_newsletter_recipients.py
Normal file
9
frappe/patches/v15_0/validate_newsletter_recipients.py
Normal 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()
|
||||
BIN
frappe/public/css/fonts/inter/Inter-Black.woff2
Normal file
BIN
frappe/public/css/fonts/inter/Inter-Black.woff2
Normal file
Binary file not shown.
BIN
frappe/public/css/fonts/inter/Inter-BlackItalic.woff2
Normal file
BIN
frappe/public/css/fonts/inter/Inter-BlackItalic.woff2
Normal file
Binary file not shown.
BIN
frappe/public/css/fonts/inter/Inter-Bold.woff2
Normal file
BIN
frappe/public/css/fonts/inter/Inter-Bold.woff2
Normal file
Binary file not shown.
BIN
frappe/public/css/fonts/inter/Inter-BoldItalic.woff2
Normal file
BIN
frappe/public/css/fonts/inter/Inter-BoldItalic.woff2
Normal file
Binary file not shown.
BIN
frappe/public/css/fonts/inter/Inter-ExtraBold.woff2
Normal file
BIN
frappe/public/css/fonts/inter/Inter-ExtraBold.woff2
Normal file
Binary file not shown.
BIN
frappe/public/css/fonts/inter/Inter-ExtraBoldItalic.woff2
Normal file
BIN
frappe/public/css/fonts/inter/Inter-ExtraBoldItalic.woff2
Normal file
Binary file not shown.
BIN
frappe/public/css/fonts/inter/Inter-ExtraLight.woff2
Normal file
BIN
frappe/public/css/fonts/inter/Inter-ExtraLight.woff2
Normal file
Binary file not shown.
BIN
frappe/public/css/fonts/inter/Inter-ExtraLightItalic.woff2
Normal file
BIN
frappe/public/css/fonts/inter/Inter-ExtraLightItalic.woff2
Normal file
Binary file not shown.
Binary file not shown.
BIN
frappe/public/css/fonts/inter/Inter-Italic.woff2
Normal file
BIN
frappe/public/css/fonts/inter/Inter-Italic.woff2
Normal file
Binary file not shown.
BIN
frappe/public/css/fonts/inter/Inter-Light.woff2
Normal file
BIN
frappe/public/css/fonts/inter/Inter-Light.woff2
Normal file
Binary file not shown.
BIN
frappe/public/css/fonts/inter/Inter-LightItalic.woff2
Normal file
BIN
frappe/public/css/fonts/inter/Inter-LightItalic.woff2
Normal file
Binary file not shown.
BIN
frappe/public/css/fonts/inter/Inter-Medium.woff2
Normal file
BIN
frappe/public/css/fonts/inter/Inter-Medium.woff2
Normal file
Binary file not shown.
BIN
frappe/public/css/fonts/inter/Inter-MediumItalic.woff2
Normal file
BIN
frappe/public/css/fonts/inter/Inter-MediumItalic.woff2
Normal file
Binary file not shown.
BIN
frappe/public/css/fonts/inter/Inter-Regular.woff2
Normal file
BIN
frappe/public/css/fonts/inter/Inter-Regular.woff2
Normal file
Binary file not shown.
BIN
frappe/public/css/fonts/inter/Inter-SemiBold.woff2
Normal file
BIN
frappe/public/css/fonts/inter/Inter-SemiBold.woff2
Normal file
Binary file not shown.
BIN
frappe/public/css/fonts/inter/Inter-SemiBoldItalic.woff2
Normal file
BIN
frappe/public/css/fonts/inter/Inter-SemiBoldItalic.woff2
Normal file
Binary file not shown.
BIN
frappe/public/css/fonts/inter/Inter-Thin.woff2
Normal file
BIN
frappe/public/css/fonts/inter/Inter-Thin.woff2
Normal file
Binary file not shown.
BIN
frappe/public/css/fonts/inter/Inter-ThinItalic.woff2
Normal file
BIN
frappe/public/css/fonts/inter/Inter-ThinItalic.woff2
Normal file
Binary file not shown.
Binary file not shown.
BIN
frappe/public/css/fonts/inter/InterVariable-Italic.woff2
Normal file
BIN
frappe/public/css/fonts/inter/InterVariable-Italic.woff2
Normal file
Binary file not shown.
BIN
frappe/public/css/fonts/inter/InterVariable.woff2
Normal file
BIN
frappe/public/css/fonts/inter/InterVariable.woff2
Normal file
Binary file not shown.
|
|
@ -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.
|
||||
|
|
@ -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"); }
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
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.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue