diff --git a/.github/workflows/patch-mariadb-tests.yml b/.github/workflows/patch-mariadb-tests.yml new file mode 100644 index 0000000000..e8627a01fb --- /dev/null +++ b/.github/workflows/patch-mariadb-tests.yml @@ -0,0 +1,83 @@ +name: Patch + +on: [pull_request, workflow_dispatch] + +jobs: + test: + runs-on: ubuntu-18.04 + + name: Patch Test + + services: + mysql: + image: mariadb:10.3 + env: + MYSQL_ALLOW_EMPTY_PASSWORD: YES + ports: + - 3306:3306 + options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3 + + steps: + - name: Clone + uses: actions/checkout@v2 + + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: 3.7 + + - name: Add to Hosts + run: echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts + + - name: Cache pip + uses: actions/cache@v2 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} + restore-keys: | + ${{ runner.os }}-pip- + ${{ runner.os }}- + + - name: Cache node modules + uses: actions/cache@v2 + env: + cache-name: cache-node-modules + with: + path: ~/.npm + key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-build-${{ env.cache-name }}- + ${{ runner.os }}-build- + ${{ runner.os }}- + + - name: Get yarn cache directory path + id: yarn-cache-dir-path + run: echo "::set-output name=dir::$(yarn cache dir)" + + - uses: actions/cache@v2 + id: yarn-cache + with: + path: ${{ steps.yarn-cache-dir-path.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + + - name: Install Dependencies + run: bash ${GITHUB_WORKSPACE}/.github/helper/install_dependencies.sh + env: + BEFORE: ${{ env.GITHUB_EVENT_PATH.before }} + AFTER: ${{ env.GITHUB_EVENT_PATH.after }} + TYPE: server + + - name: Install + run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh + env: + DB: mariadb + TYPE: server + + - name: Run Patch Tests + run: | + cd ~/frappe-bench/ + wget https://frappeframework.com/files/v10-frappe.sql.gz + bench --site test_site --force restore ~/frappe-bench/v10-frappe.sql.gz + bench --site test_site migrate diff --git a/.github/workflows/server-mariadb-tests.yml b/.github/workflows/server-mariadb-tests.yml index 1c7655528c..2476102e3d 100644 --- a/.github/workflows/server-mariadb-tests.yml +++ b/.github/workflows/server-mariadb-tests.yml @@ -91,7 +91,6 @@ jobs: DB: mariadb TYPE: server - - name: Run Tests run: cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --use-orchestrator --with-coverage env: diff --git a/cypress/integration/form.js b/cypress/integration/form.js index 20ed7a61cd..909955c1df 100644 --- a/cypress/integration/form.js +++ b/cypress/integration/form.js @@ -18,6 +18,7 @@ context('Form', () => { cy.get('.primary-action').click(); cy.wait('@form_save').its('response.statusCode').should('eq', 200); cy.visit('/app/todo'); + cy.wait(300); cy.get('.title-text').should('be.visible').and('contain', 'To Do'); cy.get('.list-row').should('contain', 'this is a test todo'); }); diff --git a/esbuild/esbuild.js b/esbuild/esbuild.js index 5154adb634..efa1959969 100644 --- a/esbuild/esbuild.js +++ b/esbuild/esbuild.js @@ -258,12 +258,17 @@ function get_watch_config() { async function clean_dist_folders(apps) { for (let app of apps) { let public_path = get_public_path(app); - await fs.promises.rmdir(path.resolve(public_path, "dist", "js"), { - recursive: true - }); - await fs.promises.rmdir(path.resolve(public_path, "dist", "css"), { - recursive: true - }); + let paths = [ + path.resolve(public_path, "dist", "js"), + path.resolve(public_path, "dist", "css") + ]; + for (let target of paths) { + if (fs.existsSync(target)) { + // rmdir is deprecated in node 16, this will work in both node 14 and 16 + let rmdir = fs.promises.rm || fs.promises.rmdir; + await rmdir(target, { recursive: true }); + } + } } } diff --git a/frappe/__init__.py b/frappe/__init__.py index bb4e409d61..01c7879a06 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -1110,9 +1110,7 @@ def setup_module_map(): if not (local.app_modules and local.module_app): local.module_app, local.app_modules = {}, {} - for app in get_all_apps(True): - if app == "webnotes": - app = "frappe" + for app in get_all_apps(with_internal_apps=True): local.app_modules.setdefault(app, []) for module in get_module_list(app): module = scrub(module) @@ -1493,7 +1491,7 @@ def get_print(doctype=None, name=None, print_format=None, style=None, :param style: Print Format style. :param as_pdf: Return as PDF. Default False. :param password: Password to encrypt the pdf with. Default None""" - from frappe.website.render import build_page + from frappe.website.serve import get_response_content from frappe.utils.pdf import get_pdf local.form_dict.doctype = doctype @@ -1508,7 +1506,7 @@ def get_print(doctype=None, name=None, print_format=None, style=None, options = {'password': password} if not html: - html = build_page("printview") + html = get_response_content("printview") if as_pdf: return get_pdf(html, output = output, options = options) diff --git a/frappe/app.py b/frappe/app.py index 6f5023be93..920628dda4 100644 --- a/frappe/app.py +++ b/frappe/app.py @@ -16,9 +16,9 @@ import frappe.handler import frappe.auth import frappe.api import frappe.utils.response -import frappe.website.render from frappe.utils import get_site_name, sanitize_html from frappe.middlewares import StaticDataMiddleware +from frappe.website.serve import get_response from frappe.utils.error import make_error_snapshot from frappe.core.doctype.comment.comment import update_comments_in_parent_after_request from frappe import _ @@ -72,7 +72,7 @@ def application(request): response = frappe.utils.response.download_private_file(request.path) elif request.method in ('GET', 'HEAD', 'POST'): - response = frappe.website.render.render() + response = get_response() else: raise NotFound @@ -266,8 +266,7 @@ def handle_exception(e): make_error_snapshot(e) if return_as_message: - response = frappe.website.render.render("message", - http_status_code=http_status_code) + response = get_response("message", http_status_code=http_status_code) return response diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.py b/frappe/automation/doctype/auto_repeat/auto_repeat.py index 998e73a42c..d2afda1553 100644 --- a/frappe/automation/doctype/auto_repeat/auto_repeat.py +++ b/frappe/automation/doctype/auto_repeat/auto_repeat.py @@ -333,7 +333,7 @@ class AutoRepeat(Document): if self.reference_doctype and self.reference_document: res = get_contacts_linking_to(self.reference_doctype, self.reference_document, fields=['email_id']) res += get_contacts_linked_from(self.reference_doctype, self.reference_document, fields=['email_id']) - email_ids = list(set([d.email_id for d in res])) + email_ids = {d.email_id for d in res} if not email_ids: frappe.msgprint(_('No contacts linked to document'), alert=True) else: diff --git a/frappe/cache_manager.py b/frappe/cache_manager.py index 52fba4568d..9f09f26be8 100644 --- a/frappe/cache_manager.py +++ b/frappe/cache_manager.py @@ -53,7 +53,7 @@ def clear_domain_cache(user=None): cache.delete_value(domain_cache_keys) def clear_global_cache(): - from frappe.website.render import clear_cache as clear_website_cache + from frappe.website.utils import clear_website_cache clear_doctype_cache() clear_website_cache() diff --git a/frappe/commands/utils.py b/frappe/commands/utils.py index bcb1749644..c16de497ec 100644 --- a/frappe/commands/utils.py +++ b/frappe/commands/utils.py @@ -69,14 +69,14 @@ def watch(apps=None): def clear_cache(context): "Clear cache, doctype cache and defaults" import frappe.sessions - import frappe.website.render + from frappe.website.utils import clear_website_cache from frappe.desk.notifications import clear_notifications for site in context.sites: try: frappe.connect(site) frappe.clear_cache() clear_notifications() - frappe.website.render.clear_cache() + clear_website_cache() finally: frappe.destroy() if not context.sites: @@ -86,12 +86,12 @@ def clear_cache(context): @pass_context def clear_website_cache(context): "Clear website cache" - import frappe.website.render + from frappe.website.utils import clear_website_cache for site in context.sites: try: frappe.init(site=site) frappe.connect() - frappe.website.render.clear_cache() + clear_website_cache() finally: frappe.destroy() if not context.sites: @@ -572,22 +572,29 @@ def run_tests(context, app=None, module=None, doctype=None, test=(), profile=Fal # Generate coverage report only for app that is being tested source_path = os.path.join(get_bench_path(), 'apps', app or 'frappe') - omit=[ - '*.html', + incl = [ + '*.py', + ] + omit = [ '*.js', '*.xml', + '*.pyc', '*.css', '*.less', '*.scss', '*.vue', + '*.html', + '*/test_*', + '*/node_modules/*', '*/doctype/*/*_dashboard.py', - '*/patches/*' + '*/patches/*', ] if not app or app == 'frappe': + omit.append('*/tests/*') omit.append('*/commands/*') - cov = Coverage(source=[source_path], omit=omit) + cov = Coverage(source=[source_path], omit=omit, include=incl) cov.start() ret = frappe.test_runner.main(app, module, doctype, context.verbose, tests=tests, diff --git a/frappe/contacts/address_and_contact.py b/frappe/contacts/address_and_contact.py index f21819ad98..77305168c1 100644 --- a/frappe/contacts/address_and_contact.py +++ b/frappe/contacts/address_and_contact.py @@ -153,7 +153,7 @@ def filter_dynamic_link_doctypes(doctype, txt, searchfield, start, page_len, fil doctypes = frappe.db.get_all("DocField", filters=filters, fields=["parent"], distinct=True, as_list=True) - doctypes = tuple([d for d in doctypes if re.search(txt+".*", _(d[0]), re.IGNORECASE)]) + doctypes = tuple(d for d in doctypes if re.search(txt+".*", _(d[0]), re.IGNORECASE)) filters.update({ "dt": ("not in", [d[0] for d in doctypes]) diff --git a/frappe/contacts/doctype/address/address.py b/frappe/contacts/doctype/address/address.py index bfcf91427d..755bc63064 100644 --- a/frappe/contacts/doctype/address/address.py +++ b/frappe/contacts/doctype/address/address.py @@ -257,7 +257,7 @@ def address_query(doctype, txt, searchfield, start, page_len, filters): def get_condensed_address(doc): fields = ["address_title", "address_line1", "address_line2", "city", "county", "state", "country"] - return ", ".join([doc.get(d) for d in fields if doc.get(d)]) + return ", ".join(doc.get(d) for d in fields if doc.get(d)) def update_preferred_address(address, field): frappe.db.set_value('Address', address, field, 0) diff --git a/frappe/core/doctype/comment/comment.py b/frappe/core/doctype/comment/comment.py index e29bae25a2..2706ab1c30 100644 --- a/frappe/core/doctype/comment/comment.py +++ b/frappe/core/doctype/comment/comment.py @@ -9,7 +9,7 @@ from frappe.core.doctype.user.user import extract_mentions from frappe.desk.doctype.notification_log.notification_log import enqueue_create_notification,\ get_title, get_title_html from frappe.utils import get_fullname -from frappe.website.render import clear_cache +from frappe.website.utils import clear_cache from frappe.database.schema import add_column from frappe.exceptions import ImplicitCommitError diff --git a/frappe/core/doctype/communication/communication.py b/frappe/core/doctype/communication/communication.py index 0caa565e2c..17b1290776 100644 --- a/frappe/core/doctype/communication/communication.py +++ b/frappe/core/doctype/communication/communication.py @@ -6,10 +6,11 @@ import frappe from frappe import _ from frappe.model.document import Document from frappe.utils import validate_email_address, strip_html, cstr, time_diff_in_seconds -from frappe.core.doctype.communication.email import validate_email, notify, _notify +from frappe.core.doctype.communication.email import validate_email +from frappe.core.doctype.communication.mixins import CommunicationEmailMixin from frappe.core.utils import get_parent_doc from frappe.utils.bot import BotReply -from frappe.utils import parse_addr +from frappe.utils import parse_addr, split_emails from frappe.core.doctype.comment.comment import update_comment_in_doc from email.utils import parseaddr from urllib.parse import unquote @@ -19,7 +20,7 @@ from frappe.automation.doctype.assignment_rule.assignment_rule import apply as a exclude_from_linked_with = True -class Communication(Document): +class Communication(Document, CommunicationEmailMixin): """Communication represents an external communication like Email. """ no_feed_on_delete = True @@ -125,6 +126,45 @@ class Communication(Document): if self.communication_type == "Communication": self.notify_change('delete') + @property + def sender_mailid(self): + return parse_addr(self.sender)[1] if self.sender else "" + + @staticmethod + def _get_emails_list(emails=None, exclude_displayname = False): + """Returns list of emails from given email string. + + * Removes duplicate mailids + * Removes display name from email address if exclude_displayname is True + """ + emails = split_emails(emails) if isinstance(emails, str) else (emails or []) + if exclude_displayname: + return [email.lower() for email in set([parse_addr(email)[1] for email in emails]) if email] + return [email.lower() for email in set(emails) if email] + + def to_list(self, exclude_displayname = True): + """Returns to list. + """ + return self._get_emails_list(self.recipients, exclude_displayname=exclude_displayname) + + def cc_list(self, exclude_displayname = True): + """Returns cc list. + """ + return self._get_emails_list(self.cc, exclude_displayname=exclude_displayname) + + def bcc_list(self, exclude_displayname = True): + """Returns bcc list. + """ + return self._get_emails_list(self.bcc, exclude_displayname=exclude_displayname) + + def get_attachments(self): + attachments = frappe.get_all( + "File", + fields=["name", "file_name", "file_url", "is_private"], + filters = {"attached_to_name": self.name, "attached_to_doctype": self.DOCTYPE} + ) + return attachments + def notify_change(self, action): frappe.publish_realtime('update_docinfo_for_{}_{}'.format(self.reference_doctype, self.reference_name), { 'doc': self.as_dict(), @@ -198,36 +238,6 @@ class Communication(Document): if not self.sender_full_name: self.sender_full_name = sender_email - def send(self, print_html=None, print_format=None, attachments=None, - send_me_a_copy=False, recipients=None): - """Send communication via Email. - - :param print_html: Send given value as HTML attachment. - :param print_format: Attach print format of parent document.""" - - self.send_me_a_copy = send_me_a_copy - self.notify(print_html, print_format, attachments, recipients) - - def notify(self, print_html=None, print_format=None, attachments=None, - recipients=None, cc=None, bcc=None,fetched_from_email_account=False): - """Calls a delayed task 'sendmail' that enqueus email in Email Queue queue - - :param print_html: Send given value as HTML attachment - :param print_format: Attach print format of parent document - :param attachments: A list of filenames that should be attached when sending this email - :param recipients: Email recipients - :param cc: Send email as CC to - :param fetched_from_email_account: True when pulling email, the notification shouldn't go to the main recipient - - """ - notify(self, print_html, print_format, attachments, recipients, cc, bcc, - fetched_from_email_account) - - def _notify(self, print_html=None, print_format=None, attachments=None, - recipients=None, cc=None, bcc=None): - - _notify(self, print_html, print_format, attachments, recipients, cc, bcc) - def bot_reply(self): if self.comment_type == 'Bot' and self.communication_type == 'Chat': reply = BotReply().get_reply(self.content) @@ -504,3 +514,4 @@ def set_avg_response_time(parent, communication): if response_times: avg_response_time = sum(response_times) / len(response_times) parent.db_set("avg_response_time", avg_response_time) + diff --git a/frappe/core/doctype/communication/email.py b/frappe/core/doctype/communication/email.py index c28956b41f..d35c118550 100755 --- a/frappe/core/doctype/communication/email.py +++ b/frappe/core/doctype/communication/email.py @@ -13,6 +13,11 @@ import time from frappe import _ from frappe.utils.background_jobs import enqueue +OUTGOING_EMAIL_ACCOUNT_MISSING = _(""" + Unable to send mail because of a missing email account. + Please setup default Email Account from Setup > Email > Email Account +""") + @frappe.whitelist() def make(doctype=None, name=None, content=None, subject=None, sent_or_received = "Sent", sender=None, sender_full_name=None, recipients=None, communication_medium="Email", send_email=False, @@ -36,7 +41,6 @@ def make(doctype=None, name=None, content=None, subject=None, sent_or_received = :param send_me_a_copy: Send a copy to the sender (default **False**). :param email_template: Template which is used to compose mail . """ - is_error_report = (doctype=="User" and name==frappe.session.user and subject=="Error Report") send_me_a_copy = cint(send_me_a_copy) @@ -84,12 +88,16 @@ def make(doctype=None, name=None, content=None, subject=None, sent_or_received = frappe.db.commit() if cint(send_email): - frappe.flags.print_letterhead = cint(print_letterhead) - comm.send(print_html, print_format, attachments, send_me_a_copy=send_me_a_copy) + if not comm.get_outgoing_email_account(): + frappe.throw(msg=OUTGOING_EMAIL_ACCOUNT_MISSING, exc=frappe.OutgoingEmailError) + comm.send_email(print_html=print_html, print_format=print_format, + send_me_a_copy=send_me_a_copy, print_letterhead=print_letterhead) + + emails_not_sent_to = comm.exclude_emails_list(include_sender=send_me_a_copy) return { "name": comm.name, - "emails_not_sent_to": ", ".join(comm.emails_not_sent_to) if hasattr(comm, "emails_not_sent_to") else None + "emails_not_sent_to": ", ".join(emails_not_sent_to or []) } def validate_email(doc): @@ -110,164 +118,6 @@ def validate_email(doc): # validate sender -def notify(doc, print_html=None, print_format=None, attachments=None, - recipients=None, cc=None, bcc=None, fetched_from_email_account=False): - """Calls a delayed task 'sendmail' that enqueus email in Email Queue queue - - :param print_html: Send given value as HTML attachment - :param print_format: Attach print format of parent document - :param attachments: A list of filenames that should be attached when sending this email - :param recipients: Email recipients - :param cc: Send email as CC to - :param bcc: Send email as BCC to - :param fetched_from_email_account: True when pulling email, the notification shouldn't go to the main recipient - - """ - recipients, cc, bcc = get_recipients_cc_and_bcc(doc, recipients, cc, bcc, - fetched_from_email_account=fetched_from_email_account) - - if not recipients and not cc: - return - - doc.emails_not_sent_to = set(doc.all_email_addresses) - set(doc.sent_email_addresses) - - if frappe.flags.in_test: - # for test cases, run synchronously - doc._notify(print_html=print_html, print_format=print_format, attachments=attachments, - recipients=recipients, cc=cc, bcc=None) - else: - enqueue(sendmail, queue="default", timeout=300, event="sendmail", - communication_name=doc.name, - print_html=print_html, print_format=print_format, attachments=attachments, - recipients=recipients, cc=cc, bcc=bcc, lang=frappe.local.lang, - session=frappe.local.session, print_letterhead=frappe.flags.print_letterhead) - -def _notify(doc, print_html=None, print_format=None, attachments=None, - recipients=None, cc=None, bcc=None): - - prepare_to_notify(doc, print_html, print_format, attachments) - - if doc.outgoing_email_account.send_unsubscribe_message: - unsubscribe_message = _("Leave this conversation") - else: - unsubscribe_message = "" - - frappe.sendmail( - recipients=(recipients or []), - cc=(cc or []), - bcc=(bcc or []), - expose_recipients="header", - sender=doc.sender, - reply_to=doc.incoming_email_account, - subject=doc.subject, - content=doc.content, - reference_doctype=doc.reference_doctype, - reference_name=doc.reference_name, - attachments=doc.attachments, - message_id=doc.message_id, - unsubscribe_message=unsubscribe_message, - delayed=True, - communication=doc.name, - read_receipt=doc.read_receipt, - is_notification=True if doc.sent_or_received =="Received" else False, - print_letterhead=frappe.flags.print_letterhead - ) - -def get_recipients_cc_and_bcc(doc, recipients, cc, bcc, fetched_from_email_account=False): - doc.all_email_addresses = [] - doc.sent_email_addresses = [] - doc.previous_email_sender = None - - if not recipients: - recipients = get_recipients(doc, fetched_from_email_account=fetched_from_email_account) - - if not cc: - cc = get_cc(doc, recipients, fetched_from_email_account=fetched_from_email_account) - - if not bcc: - bcc = get_bcc(doc, recipients, fetched_from_email_account=fetched_from_email_account) - - if fetched_from_email_account: - # email was already sent to the original recipient by the sender's email service - original_recipients, recipients = recipients, [] - - # send email to the sender of the previous email in the thread which this email is a reply to - #provides erratic results and can send external - #if doc.previous_email_sender: - # recipients.append(doc.previous_email_sender) - - # cc that was received in the email - original_cc = split_emails(doc.cc) - - # don't cc to people who already received the mail from sender's email service - cc = list(set(cc) - set(original_cc) - set(original_recipients)) - remove_administrator_from_email_list(cc) - - original_bcc = split_emails(doc.bcc) - bcc = list(set(bcc) - set(original_bcc) - set(original_recipients)) - remove_administrator_from_email_list(bcc) - - remove_administrator_from_email_list(recipients) - - return recipients, cc, bcc - -def remove_administrator_from_email_list(email_list): - administrator_email = list(filter(lambda emails: "Administrator" in emails, email_list)) - if administrator_email: - email_list.remove(administrator_email[0]) - -def prepare_to_notify(doc, print_html=None, print_format=None, attachments=None): - """Prepare to make multipart MIME Email - - :param print_html: Send given value as HTML attachment. - :param print_format: Attach print format of parent document.""" - - view_link = frappe.utils.cint(frappe.db.get_value("System Settings", "System Settings", "attach_view_link")) - - if print_format and view_link: - doc.content += get_attach_link(doc, print_format) - - set_incoming_outgoing_accounts(doc) - - if not doc.sender: - doc.sender = doc.outgoing_email_account.email_id - - if not doc.sender_full_name: - doc.sender_full_name = doc.outgoing_email_account.name or _("Notification") - - if doc.sender: - # combine for sending to get the format 'Jane ' - doc.sender = get_formatted_email(doc.sender_full_name, mail=doc.sender) - - doc.attachments = [] - - if print_html or print_format: - doc.attachments.append({"print_format_attachment":1, "doctype":doc.reference_doctype, - "name":doc.reference_name, "print_format":print_format, "html":print_html}) - - if attachments: - if isinstance(attachments, str): - attachments = json.loads(attachments) - - for a in attachments: - if isinstance(a, str): - # is it a filename? - try: - # check for both filename and file id - file_id = frappe.db.get_list('File', or_filters={'file_name': a, 'name': a}, limit=1) - if not file_id: - frappe.throw(_("Unable to find attachment {0}").format(a)) - file_id = file_id[0]['name'] - _file = frappe.get_doc("File", file_id) - _file.get_content() - # these attachments will be attached on-demand - # and won't be stored in the message - doc.attachments.append({"fid": file_id}) - except IOError: - frappe.throw(_("Unable to find attachment {0}").format(a)) - else: - doc.attachments.append(a) - def set_incoming_outgoing_accounts(doc): from frappe.email.doctype.email_account.email_account import EmailAccount incoming_email_account = EmailAccount.find_incoming( @@ -280,74 +130,6 @@ def set_incoming_outgoing_accounts(doc): if doc.sent_or_received == "Sent": doc.db_set("email_account", doc.outgoing_email_account.name) -def get_recipients(doc, fetched_from_email_account=False): - """Build a list of email addresses for To""" - # [EDGE CASE] doc.recipients can be None when an email is sent as BCC - recipients = split_emails(doc.recipients) - - #if fetched_from_email_account and doc.in_reply_to: - # add sender of previous reply - #doc.previous_email_sender = frappe.db.get_value("Communication", doc.in_reply_to, "sender") - #recipients.append(doc.previous_email_sender) - - if recipients: - recipients = filter_email_list(doc, recipients, []) - - return recipients - -def get_cc(doc, recipients=None, fetched_from_email_account=False): - """Build a list of email addresses for CC""" - # get a copy of CC list - cc = split_emails(doc.cc) - - if doc.reference_doctype and doc.reference_name: - if fetched_from_email_account: - # if it is a fetched email, add follows to CC - cc.append(get_owner_email(doc)) - cc += get_assignees(doc) - - if getattr(doc, "send_me_a_copy", False) and doc.sender not in cc: - cc.append(doc.sender) - - if cc: - # exclude unfollows, recipients and unsubscribes - exclude = [] #added to remove account check - exclude += [d[0] for d in frappe.db.get_all("User", ["email"], {"thread_notify": 0}, as_list=True)] - exclude += [(parse_addr(email)[1] or "").lower() for email in recipients] - - if fetched_from_email_account: - # exclude sender when pulling email - exclude += [parse_addr(doc.sender)[1]] - - if doc.reference_doctype and doc.reference_name: - exclude += [d[0] for d in frappe.db.get_all("Email Unsubscribe", ["email"], - {"reference_doctype": doc.reference_doctype, "reference_name": doc.reference_name}, as_list=True)] - - cc = filter_email_list(doc, cc, exclude, is_cc=True) - - return cc - -def get_bcc(doc, recipients=None, fetched_from_email_account=False): - """Build a list of email addresses for BCC""" - bcc = split_emails(doc.bcc) - - if bcc: - exclude = [] - exclude += [d[0] for d in frappe.db.get_all("User", ["email"], {"thread_notify": 0}, as_list=True)] - exclude += [(parse_addr(email)[1] or "").lower() for email in recipients] - - if fetched_from_email_account: - # exclude sender when pulling email - exclude += [parse_addr(doc.sender)[1]] - - if doc.reference_doctype and doc.reference_name: - exclude += [d[0] for d in frappe.db.get_all("Email Unsubscribe", ["email"], - {"reference_doctype": doc.reference_doctype, "reference_name": doc.reference_name}, as_list=True)] - - bcc = filter_email_list(doc, bcc, exclude, is_bcc=True) - - return bcc - def add_attachments(name, attachments): '''Add attachments to the given Communication''' # loop through attachments @@ -355,7 +137,6 @@ def add_attachments(name, attachments): if isinstance(a, str): attach = frappe.db.get_value("File", {"name":a}, ["file_name", "file_url", "is_private"], as_dict=1) - # save attachments to new doc _file = frappe.get_doc({ "doctype": "File", @@ -367,103 +148,6 @@ def add_attachments(name, attachments): }) _file.save(ignore_permissions=True) -def filter_email_list(doc, email_list, exclude, is_cc=False, is_bcc=False): - # temp variables - filtered = [] - email_address_list = [] - - for email in list(set(email_list)): - email_address = (parse_addr(email)[1] or "").lower() - if not email_address: - continue - - # this will be used to eventually find email addresses that aren't sent to - doc.all_email_addresses.append(email_address) - - if (email in exclude) or (email_address in exclude): - continue - - if is_cc: - is_user_enabled = frappe.db.get_value("User", email_address, "enabled") - if is_user_enabled==0: - # don't send to disabled users - continue - - if is_bcc: - is_user_enabled = frappe.db.get_value("User", email_address, "enabled") - if is_user_enabled==0: - continue - - # make sure of case-insensitive uniqueness of email address - if email_address not in email_address_list: - # append the full email i.e. "Human " - filtered.append(email) - email_address_list.append(email_address) - - doc.sent_email_addresses.extend(email_address_list) - - return filtered - -def get_owner_email(doc): - owner = get_parent_doc(doc).owner - return get_formatted_email(owner) or owner - -def get_assignees(doc): - return [( get_formatted_email(d.owner) or d.owner ) for d in - frappe.db.get_all("ToDo", filters={ - "reference_type": doc.reference_doctype, - "reference_name": doc.reference_name, - "status": "Open" - }, fields=["owner"]) - ] - -def get_attach_link(doc, print_format): - """Returns public link for the attachment via `templates/emails/print_link.html`.""" - return frappe.get_template("templates/emails/print_link.html").render({ - "url": get_url(), - "doctype": doc.reference_doctype, - "name": doc.reference_name, - "print_format": print_format, - "key": get_parent_doc(doc).get_signature() - }) - -def sendmail(communication_name, print_html=None, print_format=None, attachments=None, - recipients=None, cc=None, bcc=None, lang=None, session=None, print_letterhead=None): - try: - - if lang: - frappe.local.lang = lang - - if session: - # hack to enable access to private files in PDF - session['data'] = frappe._dict(session['data']) - frappe.local.session.update(session) - - if print_letterhead: - frappe.flags.print_letterhead = print_letterhead - - # upto 3 retries - for i in range(3): - try: - communication = frappe.get_doc("Communication", communication_name) - communication._notify(print_html=print_html, print_format=print_format, attachments=attachments, - recipients=recipients, cc=cc, bcc=bcc) - - except frappe.db.InternalError as e: - # deadlock, try again - if frappe.db.is_deadlocked(e): - frappe.db.rollback() - time.sleep(1) - continue - else: - raise - else: - break - - except: - traceback = frappe.log_error("frappe.core.doctype.communication.email.sendmail") - raise - @frappe.whitelist(allow_guest=True) def mark_email_as_seen(name=None): try: diff --git a/frappe/core/doctype/communication/mixins.py b/frappe/core/doctype/communication/mixins.py new file mode 100644 index 0000000000..82a47d24d9 --- /dev/null +++ b/frappe/core/doctype/communication/mixins.py @@ -0,0 +1,297 @@ +import frappe +from frappe import _ +from frappe.core.utils import get_parent_doc +from frappe.utils import parse_addr, get_formatted_email, get_url +from frappe.email.doctype.email_account.email_account import EmailAccount + +class CommunicationEmailMixin: + """Mixin class to handle communication mails. + """ + def is_email_communication(self): + return self.communication_type=="Communication" and self.communication_medium == "Email" + + def get_owner(self): + """Get owner of the communication docs parent. + """ + parent_doc = get_parent_doc(self) + return parent_doc.owner if parent_doc else None + + def get_all_email_addresses(self, exclude_displayname=False): + """Get all Email addresses mentioned in the doc along with display name. + """ + return self.to_list(exclude_displayname=exclude_displayname) + \ + self.cc_list(exclude_displayname=exclude_displayname) + \ + self.bcc_list(exclude_displayname=exclude_displayname) + + def get_email_with_displayname(self, email_address): + """Returns email address after adding displayname. + """ + display_name, email = parse_addr(email_address) + if display_name and display_name != email: + return email_address + + # emailid to emailid with display name map. + email_map = {parse_addr(email)[1]: email for email in self.get_all_email_addresses()} + return email_map.get(email, email) + + def mail_recipients(self, is_inbound_mail_communcation=False): + """Build to(recipient) list to send an email. + """ + # Incase of inbound mail, recipients already received the mail, no need to send again. + if is_inbound_mail_communcation: + return [] + + if hasattr(self, '_final_recipients'): + return self._final_recipients + + to = self.to_list() + self._final_recipients = list(filter(lambda id: id != 'Administrator', to)) + return self._final_recipients + + def get_mail_recipients_with_displayname(self, is_inbound_mail_communcation=False): + """Build to(recipient) list to send an email including displayname in email. + """ + to_list = self.mail_recipients(is_inbound_mail_communcation=is_inbound_mail_communcation) + return [self.get_email_with_displayname(email) for email in to_list] + + def mail_cc(self, is_inbound_mail_communcation=False, include_sender = False): + """Build cc list to send an email. + + * if email copy is requested by sender, then add sender to CC. + * If this doc is created through inbound mail, then add doc owner to cc list + * remove all the thread_notify disabled users. + * Make sure that all users enabled in the system + * Remove admin from email list + + * FixMe: Removed adding TODO owners to cc list. Check if that is needed. + """ + if hasattr(self, '_final_cc'): + return self._final_cc + + cc = self.cc_list() + + # Need to inform parent document owner incase communication is created through inbound mail + if include_sender: + cc.append(self.sender_mailid) + if is_inbound_mail_communcation: + cc.append(self.get_owner()) + cc = set(cc) - {self.sender_mailid} + + cc = set(cc) - set(self.filter_thread_notification_disbled_users(cc)) + cc = cc - set(self.mail_recipients(is_inbound_mail_communcation=is_inbound_mail_communcation)) + cc = cc - set(self.filter_disabled_users(cc)) + + # # Incase of inbound mail, to and cc already received the mail, no need to send again. + if is_inbound_mail_communcation: + cc = cc - set(self.cc_list() + self.to_list()) + + self._final_cc = list(filter(lambda id: id != 'Administrator', cc)) + return self._final_cc + + def get_mail_cc_with_displayname(self, is_inbound_mail_communcation=False, include_sender = False): + cc_list = self.mail_cc(is_inbound_mail_communcation=False, include_sender = False) + return [self.get_email_with_displayname(email) for email in cc_list] + + def mail_bcc(self, is_inbound_mail_communcation=False): + """ + * Thread_notify check + * Email unsubscribe list + * User must be enabled in the system + * remove_administrator_from_email_list + """ + if hasattr(self, '_final_bcc'): + return self._final_bcc + + bcc = set(self.bcc_list()) + if is_inbound_mail_communcation: + bcc = bcc - {self.sender_mailid} + bcc = bcc - set(self.filter_thread_notification_disbled_users(bcc)) + bcc = bcc - set(self.mail_recipients(is_inbound_mail_communcation=is_inbound_mail_communcation)) + bcc = bcc - set(self.filter_disabled_users(bcc)) + + # Incase of inbound mail, to and cc & bcc already received the mail, no need to send again. + if is_inbound_mail_communcation: + bcc = bcc - set(self.bcc_list() + self.to_list()) + + self._final_bcc = list(filter(lambda id: id != 'Administrator', bcc)) + return self._final_bcc + + def get_mail_bcc_with_displayname(self, is_inbound_mail_communcation=False): + bcc_list = self.mail_bcc(is_inbound_mail_communcation=is_inbound_mail_communcation) + return [self.get_email_with_displayname(email) for email in bcc_list] + + def mail_sender(self): + email_account = self.get_outgoing_email_account() + if not self.sender_mailid and email_account: + return email_account.email_id + return self.sender_mailid + + def mail_sender_fullname(self): + email_account = self.get_outgoing_email_account() + if not self.sender_full_name: + return (email_account and email_account.name) or _("Notification") + return self.sender_full_name + + def get_mail_sender_with_displayname(self): + return get_formatted_email(self.mail_sender_fullname(), mail=self.mail_sender()) + + def get_content(self, print_format=None): + if print_format: + return self.content + self.get_attach_link(print_format) + return self.content + + def get_attach_link(self, print_format): + """Returns public link for the attachment via `templates/emails/print_link.html`.""" + return frappe.get_template("templates/emails/print_link.html").render({ + "url": get_url(), + "doctype": self.reference_doctype, + "name": self.reference_name, + "print_format": print_format, + "key": get_parent_doc(self).get_signature() + }) + + def get_outgoing_email_account(self): + if not hasattr(self, '_outgoing_email_account'): + if self.email_account: + self._outgoing_email_account = EmailAccount.find(self.email_account) + else: + self._outgoing_email_account = EmailAccount.find_outgoing( + match_by_email=self.sender_mailid, + match_by_doctype=self.reference_doctype + ) + + if self.sent_or_received == "Sent" and self._outgoing_email_account: + self.db_set("email_account", self._outgoing_email_account.name) + + return self._outgoing_email_account + + def get_incoming_email_account(self): + if not hasattr(self, '_incoming_email_account'): + self._incoming_email_account = EmailAccount.find_incoming( + match_by_email=self.sender_mailid, + match_by_doctype=self.reference_doctype + ) + return self._incoming_email_account + + def mail_attachments(self, print_format=None, print_html=None): + final_attachments = [] + + if print_format and print_html: + d = {'print_format': print_format, 'print_html': print_html, 'print_format_attachment': 1, + 'doctype': self.reference_doctype, 'name': self.reference_name} + final_attachments.append(d) + + for a in self.get_attachments() or []: + final_attachments.append({"fid": a['name']}) + + return final_attachments + + def get_unsubscribe_message(self): + email_account = self.get_outgoing_email_account() + if email_account and email_account.send_unsubscribe_message: + return _("Leave this conversation") + return '' + + def exclude_emails_list(self, is_inbound_mail_communcation=False, include_sender=False): + """List of mail id's excluded while sending mail. + """ + all_ids = self.get_all_email_addresses(exclude_displayname=True) + final_ids = self.mail_recipients(is_inbound_mail_communcation = is_inbound_mail_communcation) + \ + self.mail_bcc(is_inbound_mail_communcation = is_inbound_mail_communcation) + \ + self.mail_cc(is_inbound_mail_communcation = is_inbound_mail_communcation, include_sender=include_sender) + return set(all_ids) - set(final_ids) + + @staticmethod + def filter_thread_notification_disbled_users(emails): + """Filter users based on notifications for email threads setting is disabled. + """ + if not emails: + return [] + + disabled_users = frappe.db.sql_list(""" + SELECT + email + FROM + `tabUser` + where + email in %(emails)s + and + thread_notify=0 + """, {'emails': tuple(emails)}) + return disabled_users + + @staticmethod + def filter_disabled_users(emails): + """ + """ + if not emails: + return [] + + disabled_users = frappe.db.sql_list(""" + SELECT + email + FROM + `tabUser` + where + email in %(emails)s + and + enabled=0 + """, {'emails': tuple(emails)}) + return disabled_users + + def sendmail_input_dict(self, print_html=None, print_format=None, + send_me_a_copy=None, print_letterhead=None, is_inbound_mail_communcation=None): + + outgoing_email_account = self.get_outgoing_email_account() + if not outgoing_email_account: + return {} + + recipients = self.get_mail_recipients_with_displayname( + is_inbound_mail_communcation=is_inbound_mail_communcation + ) + cc = self.get_mail_cc_with_displayname( + is_inbound_mail_communcation=is_inbound_mail_communcation, + include_sender = send_me_a_copy + ) + bcc = self.get_mail_bcc_with_displayname( + is_inbound_mail_communcation=is_inbound_mail_communcation + ) + + if not (recipients or cc): + return {} + + final_attachments = self.mail_attachments(print_format=print_format, print_html=print_html) + incoming_email_account = self.get_incoming_email_account() + return { + "recipients": recipients, + "cc": cc, + "bcc": bcc, + "expose_recipients": "header", + "sender": self.get_mail_sender_with_displayname(), + "reply_to": incoming_email_account and incoming_email_account.email_id, + "subject": self.subject, + "content": self.get_content(print_format=print_format), + "reference_doctype": self.reference_doctype, + "reference_name": self.reference_name, + "attachments": final_attachments, + "message_id": self.message_id, + "unsubscribe_message": self.get_unsubscribe_message(), + "delayed": True, + "communication": self.name, + "read_receipt": self.read_receipt, + "is_notification": (self.sent_or_received =="Received" and True) or False, + "print_letterhead": print_letterhead + } + + def send_email(self, print_html=None, print_format=None, + send_me_a_copy=None, print_letterhead=None, is_inbound_mail_communcation=None): + input_dict = self.sendmail_input_dict( + print_html=print_html, + print_format=print_format, + send_me_a_copy=send_me_a_copy, + print_letterhead=print_letterhead, + is_inbound_mail_communcation=is_inbound_mail_communcation + ) + + if input_dict: + frappe.sendmail(**input_dict) diff --git a/frappe/core/doctype/communication/test_communication.py b/frappe/core/doctype/communication/test_communication.py index 5b400398a5..d50a4db88a 100644 --- a/frappe/core/doctype/communication/test_communication.py +++ b/frappe/core/doctype/communication/test_communication.py @@ -1,10 +1,12 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt -import frappe import unittest from urllib.parse import quote -test_records = frappe.get_test_records('Communication') +import frappe +from frappe.email.doctype.email_queue.email_queue import EmailQueue + +test_records = frappe.get_test_records('Communication') class TestCommunication(unittest.TestCase): @@ -199,6 +201,70 @@ class TestCommunication(unittest.TestCase): self.assertIn(("Note", note.name), doc_links) +class TestCommunicationEmailMixin(unittest.TestCase): + def new_communication(self, recipients=None, cc=None, bcc=None): + recipients = ', '.join(recipients or []) + cc = ', '.join(cc or []) + bcc = ', '.join(bcc or []) + + comm = frappe.get_doc({ + "doctype": "Communication", + "communication_type": "Communication", + "communication_medium": "Email", + "content": "Test content", + "recipients": recipients, + "cc": cc, + "bcc": bcc + }).insert(ignore_permissions=True) + return comm + + def new_user(self, email, **user_data): + user_data.setdefault('first_name', 'first_name') + user = frappe.new_doc('User') + user.email = email + user.update(user_data) + user.insert(ignore_permissions=True, ignore_if_duplicate=True) + return user + + def test_recipients(self): + to_list = ['to@test.com', 'receiver ', 'to@test.com'] + comm = self.new_communication(recipients = to_list) + res = comm.get_mail_recipients_with_displayname() + self.assertCountEqual(res, ['to@test.com', 'receiver ']) + comm.delete() + + def test_cc(self): + to_list = ['to@test.com'] + cc_list = ['cc+1@test.com', 'cc ', 'to@test.com'] + user = self.new_user(email='cc+1@test.com', thread_notify=0) + comm = self.new_communication(recipients=to_list, cc=cc_list) + res = comm.get_mail_cc_with_displayname() + self.assertCountEqual(res, ['cc ']) + user.delete() + comm.delete() + + def test_bcc(self): + bcc_list = ['bcc+1@test.com', 'cc ', ] + user = self.new_user(email='bcc+2@test.com', enabled=0) + comm = self.new_communication(bcc=bcc_list) + res = comm.get_mail_bcc_with_displayname() + self.assertCountEqual(res, ['bcc+1@test.com']) + user.delete() + comm.delete() + + def test_sendmail(self): + to_list = ['to '] + cc_list = ['cc ', 'cc '] + + comm = self.new_communication(recipients=to_list, cc=cc_list) + comm.send_email() + doc = EmailQueue.find_one_by_filters(communication=comm.name) + mail_receivers = [each.recipient for each in doc.recipients] + self.assertIsNotNone(doc) + self.assertCountEqual(to_list+cc_list, mail_receivers) + doc.delete() + comm.delete() + def create_email_account(): frappe.delete_doc_if_exists("Email Account", "_Test Comm Account 1") @@ -229,4 +295,4 @@ def create_email_account(): "enable_automatic_linking": 1 }).insert(ignore_permissions=True) - return email_account \ No newline at end of file + return email_account diff --git a/frappe/core/doctype/data_import/importer.py b/frappe/core/doctype/data_import/importer.py index fed90b75ce..bb922f1f5d 100644 --- a/frappe/core/doctype/data_import/importer.py +++ b/frappe/core/doctype/data_import/importer.py @@ -449,7 +449,7 @@ class ImportFile: for row in data_without_first_row: row_values = row.get_values(parent_column_indexes) # if the row is blank, it's a child row doc - if all([v in INVALID_VALUES for v in row_values]): + if all(v in INVALID_VALUES for v in row_values): rows.append(row) continue # if we encounter a row which has values in parent columns, @@ -606,7 +606,7 @@ class Row: if df.fieldtype == "Select": select_options = get_select_options(df) if select_options and value not in select_options: - options_string = ", ".join([frappe.bold(d) for d in select_options]) + options_string = ", ".join(frappe.bold(d) for d in select_options) msg = _("Value must be one of {0}").format(options_string) self.warnings.append( {"row": self.row_number, "field": df_as_json(df), "message": msg,} @@ -902,7 +902,7 @@ class Column: if self.df.fieldtype == "Link": # find all values that dont exist - values = list(set([cstr(v) for v in self.column_values[1:] if v])) + values = list({cstr(v) for v in self.column_values[1:] if v}) exists = [ d.name for d in frappe.db.get_all(self.df.options, filters={"name": ("in", values)}) ] @@ -935,11 +935,11 @@ class Column: elif self.df.fieldtype == "Select": options = get_select_options(self.df) if options: - values = list(set([cstr(v) for v in self.column_values[1:] if v])) - invalid = list(set(values) - set(options)) + values = {cstr(v) for v in self.column_values[1:] if v} + invalid = values - set(options) if invalid: - valid_values = ", ".join([frappe.bold(o) for o in options]) - invalid_values = ", ".join([frappe.bold(i) for i in invalid]) + valid_values = ", ".join(frappe.bold(o) for o in options) + invalid_values = ", ".join(frappe.bold(i) for i in invalid) self.warnings.append( { "col": self.column_number, diff --git a/frappe/core/doctype/data_import_legacy/importer.py b/frappe/core/doctype/data_import_legacy/importer.py index 4080e70418..ceefff4410 100644 --- a/frappe/core/doctype/data_import_legacy/importer.py +++ b/frappe/core/doctype/data_import_legacy/importer.py @@ -177,7 +177,7 @@ def upload(rows = None, submit_after_import=None, ignore_encoding_errors=False, if d.get("name") and d["name"].startswith('"'): d["name"] = d["name"][1:-1] - if sum([0 if not val else 1 for val in d.values()]): + if sum(0 if not val else 1 for val in d.values()): d['doctype'] = dt if dt == doctype: doc.update(d) @@ -533,6 +533,6 @@ def get_parent_field(doctype, parenttype): def delete_child_rows(rows, doctype): """delete child rows for all parents""" - for p in list(set([r[1] for r in rows])): + for p in list(set(r[1] for r in rows)): if p: frappe.db.sql("""delete from `tab{0}` where parent=%s""".format(doctype), p) diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index 8a96fc89f6..3cdc45ea08 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -8,7 +8,6 @@ from frappe.cache_manager import clear_user_cache, clear_controller_cache # imports - module imports import frappe -import frappe.website.render from frappe import _ from frappe.utils import now, cint from frappe.model import no_value_fields, default_fields, data_fieldtypes, table_fields, data_field_options @@ -23,6 +22,7 @@ from frappe.model.docfield import supports_translation from frappe.modules.import_file import get_file_path from frappe.model.meta import Meta from frappe.desk.utils import validate_route_conflict +from frappe.website.utils import clear_cache class InvalidFieldNameError(frappe.ValidationError): pass class UniqueFieldnameError(frappe.ValidationError): pass @@ -193,7 +193,7 @@ class DocType(Document): self.flags.update_fields_to_fetch_queries = [] - if set(old_fields_to_fetch) != set([df.fieldname for df in new_meta.get_fields_to_fetch()]): + if set(old_fields_to_fetch) != set(df.fieldname for df in new_meta.get_fields_to_fetch()): for df in new_meta.get_fields_to_fetch(): if df.fieldname not in old_fields_to_fetch: link_fieldname, source_fieldname = df.fetch_from.split('.', 1) @@ -248,7 +248,7 @@ class DocType(Document): frappe.throw(_('Field "route" is mandatory for Web Views'), title='Missing Field') # clear website cache - frappe.website.render.clear_cache() + clear_cache() def change_modified_of_parent(self): """Change the timestamp of parent DocType if the current one is a child to clear caches.""" @@ -550,11 +550,6 @@ class DocType(Document): from frappe.modules.export_file import export_to_files export_to_files(record_list=[['DocType', self.name]], create_init=True) - def import_doc(self): - """Import from standard folder `[module]/doctype/[name]/[name].json`.""" - from frappe.modules.import_module import import_from_files - import_from_files(record_list=[[self.module, 'doctype', self.name]]) - def make_controller_template(self): """Make boilerplate controller template.""" make_boilerplate("controller._py", self) @@ -762,7 +757,7 @@ def validate_fields(meta): invalid_fields = ('doctype',) if fieldname in invalid_fields: frappe.throw(_("{0}: Fieldname cannot be one of {1}") - .format(docname, ", ".join([frappe.bold(d) for d in invalid_fields]))) + .format(docname, ", ".join(frappe.bold(d) for d in invalid_fields))) def check_unique_fieldname(docname, fieldname): duplicates = list(filter(None, map(lambda df: df.fieldname==fieldname and str(df.idx) or None, fields))) @@ -996,7 +991,7 @@ def validate_fields(meta): if docfield.options and (docfield.options not in data_field_options): df_str = frappe.bold(_(docfield.label)) text_str = _("{0} is an invalid Data field.").format(df_str) + "
" * 2 + _("Only Options allowed for Data field are:") + "
" - df_options_str = "
  • " + "
  • ".join([_(x) for x in data_field_options]) + "
" + df_options_str = "
  • " + "
  • ".join(_(x) for x in data_field_options) + "
" frappe.msgprint(text_str + df_options_str, title="Invalid Data Field", raise_exception=True) diff --git a/frappe/core/doctype/domain/domain.py b/frappe/core/doctype/domain/domain.py index 681824bb02..bbd20f3b70 100644 --- a/frappe/core/doctype/domain/domain.py +++ b/frappe/core/doctype/domain/domain.py @@ -110,7 +110,7 @@ class Domain(Document): # enable frappe.db.sql('''update `tabPortal Menu Item` set enabled=1 - where route in ({0})'''.format(', '.join(['"{0}"'.format(d) for d in self.data.allow_sidebar_items]))) + where route in ({0})'''.format(', '.join('"{0}"'.format(d) for d in self.data.allow_sidebar_items))) if self.data.remove_sidebar_items: # disable all @@ -118,4 +118,4 @@ class Domain(Document): # enable frappe.db.sql('''update `tabPortal Menu Item` set enabled=0 - where route in ({0})'''.format(', '.join(['"{0}"'.format(d) for d in self.data.remove_sidebar_items]))) + where route in ({0})'''.format(', '.join('"{0}"'.format(d) for d in self.data.remove_sidebar_items))) diff --git a/frappe/patches/v4_0/__init__.py b/frappe/core/doctype/feedback/__init__.py similarity index 100% rename from frappe/patches/v4_0/__init__.py rename to frappe/core/doctype/feedback/__init__.py diff --git a/frappe/core/doctype/feedback/feedback.js b/frappe/core/doctype/feedback/feedback.js new file mode 100644 index 0000000000..131f0e19d8 --- /dev/null +++ b/frappe/core/doctype/feedback/feedback.js @@ -0,0 +1,8 @@ +// Copyright (c) 2021, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Feedback', { + // refresh: function(frm) { + + // } +}); diff --git a/frappe/core/doctype/feedback/feedback.json b/frappe/core/doctype/feedback/feedback.json new file mode 100644 index 0000000000..cf8a180e27 --- /dev/null +++ b/frappe/core/doctype/feedback/feedback.json @@ -0,0 +1,86 @@ +{ + "actions": [], + "creation": "2021-06-03 19:02:55.328423", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "reference_doctype", + "reference_name", + "column_break_3", + "email", + "rating", + "section_break_6", + "feedback" + ], + "fields": [ + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fieldname": "email", + "fieldtype": "Data", + "label": "Email", + "reqd": 1 + }, + { + "fieldname": "rating", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Rating", + "precision": "1", + "reqd": 1 + }, + { + "fieldname": "section_break_6", + "fieldtype": "Section Break" + }, + { + "fieldname": "feedback", + "fieldtype": "Small Text", + "label": "Feedback", + "reqd": 1 + }, + { + "fieldname": "reference_doctype", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Reference Document Type", + "options": "\nBlog Post" + }, + { + "fieldname": "reference_name", + "fieldtype": "Dynamic Link", + "in_list_view": 1, + "label": "Reference Name", + "options": "reference_doctype", + "reqd": 1 + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2021-06-14 15:11:26.005805", + "modified_by": "Administrator", + "module": "Core", + "name": "Feedback", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "reference_name", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/core/doctype/feedback/feedback.py b/frappe/core/doctype/feedback/feedback.py new file mode 100644 index 0000000000..655bed6eb1 --- /dev/null +++ b/frappe/core/doctype/feedback/feedback.py @@ -0,0 +1,8 @@ +# Copyright (c) 2021, Frappe Technologies and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + +class Feedback(Document): + pass diff --git a/frappe/core/doctype/feedback/test_feedback.py b/frappe/core/doctype/feedback/test_feedback.py new file mode 100644 index 0000000000..702f9d8ac1 --- /dev/null +++ b/frappe/core/doctype/feedback/test_feedback.py @@ -0,0 +1,27 @@ +# Copyright (c) 2021, Frappe Technologies and Contributors +# See license.txt + +import frappe +import unittest + +class TestFeedback(unittest.TestCase): + def test_feedback_creation_updation(self): + from frappe.website.doctype.blog_post.test_blog_post import make_test_blog + test_blog = make_test_blog() + + frappe.db.sql("delete from `tabFeedback` where reference_doctype = 'Blog Post'") + + from frappe.templates.includes.feedback.feedback import add_feedback, update_feedback + feedback = add_feedback('Blog Post', test_blog.name, 5, 'New feedback','test@test.com') + + self.assertEqual(feedback.feedback, 'New feedback') + self.assertEqual(feedback.rating, 5) + + updated_feedback = update_feedback('Blog Post', test_blog.name, 6, 'Updated feedback', 'test@test.com') + + self.assertEqual(updated_feedback.feedback, 'Updated feedback') + self.assertEqual(updated_feedback.rating, 6) + + frappe.db.sql("delete from `tabFeedback` where reference_doctype = 'Blog Post'") + + test_blog.delete() \ No newline at end of file diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index 3fa31cbf80..5b605504e8 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -13,7 +13,7 @@ from frappe.utils.password import update_password as _update_password, check_pas from frappe.desk.notifications import clear_notifications from frappe.desk.doctype.notification_settings.notification_settings import create_notification_settings, toggle_notifications from frappe.utils.user import get_system_managers -from frappe.website.utils import is_signup_enabled +from frappe.website.utils import is_signup_disabled from frappe.rate_limiter import rate_limit from frappe.utils.background_jobs import enqueue from frappe.core.doctype.user_type.user_type import user_linked_with_permission_on_doctype @@ -839,7 +839,7 @@ def verify_password(password): @frappe.whitelist(allow_guest=True) def sign_up(email, full_name, redirect_to): - if not is_signup_enabled(): + if is_signup_disabled(): frappe.throw(_('Sign Up is disabled'), title='Not Allowed') user = frappe.db.get("User", {"email": email}) @@ -931,7 +931,7 @@ def user_query(doctype, txt, searchfield, start, page_len, filters): LIMIT %(page_len)s OFFSET %(start)s """.format( user_type_condition = user_type_condition, - standard_users=", ".join([frappe.db.escape(u) for u in STANDARD_USERS]), + standard_users=", ".join(frappe.db.escape(u) for u in STANDARD_USERS), key=searchfield, fcond=get_filters_cond(doctype, filters, conditions), mcond=get_match_cond(doctype) diff --git a/frappe/core/doctype/user_permission/user_permission.py b/frappe/core/doctype/user_permission/user_permission.py index 42ca4d7a14..4aa5797c7f 100644 --- a/frappe/core/doctype/user_permission/user_permission.py +++ b/frappe/core/doctype/user_permission/user_permission.py @@ -16,11 +16,11 @@ class UserPermission(Document): self.validate_default_permission() def on_update(self): - frappe.cache().delete_value('user_permissions') + frappe.cache().hdel('user_permissions', self.user) frappe.publish_realtime('update_user_permissions') def on_trash(self): # pylint: disable=no-self-use - frappe.cache().delete_value('user_permissions') + frappe.cache().hdel('user_permissions', self.user) frappe.publish_realtime('update_user_permissions') def validate_user_permission(self): diff --git a/frappe/core/doctype/user_type/user_type.py b/frappe/core/doctype/user_type/user_type.py index e7d06c45f2..82ffb090f1 100644 --- a/frappe/core/doctype/user_type/user_type.py +++ b/frappe/core/doctype/user_type/user_type.py @@ -112,7 +112,7 @@ class UserType(Document): self.select_doctypes = [] select_doctypes = [] - user_doctypes = tuple([row.document_type for row in self.user_doctypes]) + user_doctypes = [row.document_type for row in self.user_doctypes] for doctype in user_doctypes: doc = frappe.get_meta(doctype) @@ -265,4 +265,4 @@ def apply_permissions_for_non_standard_user_type(doc, method=None): user_doc.update_children() add_user_permission(doc.doctype, doc.name, doc.get(data[1])) else: - frappe.db.set_value('User Permission', perm_data[0], 'user', doc.get(data[1])) \ No newline at end of file + frappe.db.set_value('User Permission', perm_data[0], 'user', doc.get(data[1])) diff --git a/frappe/custom/doctype/customize_form/customize_form.py b/frappe/custom/doctype/customize_form/customize_form.py index 8bcc6cf059..1b8977acc4 100644 --- a/frappe/custom/doctype/customize_form/customize_form.py +++ b/frappe/custom/doctype/customize_form/customize_form.py @@ -355,9 +355,9 @@ class CustomizeForm(Document): def delete_custom_fields(self): meta = frappe.get_meta(self.doc_type) - fields_to_remove = (set([df.fieldname for df in meta.get("fields")]) - - set(df.fieldname for df in self.get("fields"))) - + fields_to_remove = ( + {df.fieldname for df in meta.get("fields")} - {df.fieldname for df in self.get("fields")} + ) for fieldname in fields_to_remove: df = meta.get("fields", {"fieldname": fieldname})[0] if df.get("is_custom_field"): diff --git a/frappe/database/database.py b/frappe/database/database.py index 7e8d2da43b..81e24cc7ad 100644 --- a/frappe/database/database.py +++ b/frappe/database/database.py @@ -335,7 +335,7 @@ class Database(object): values[key] = value[1] if isinstance(value[1], (tuple, list)): # value is a list in tuple ("in", ("A", "B")) - _rhs = " ({0})".format(", ".join([self.escape(v) for v in value[1]])) + _rhs = " ({0})".format(", ".join(self.escape(v) for v in value[1])) del values[key] if _operator not in ["=", "!=", ">", ">=", "<", "<=", "like", "in", "not in", "not like"]: @@ -1010,7 +1010,7 @@ class Database(object): :params values: list of list of values """ insert_list = [] - fields = ", ".join(["`"+field+"`" for field in fields]) + fields = ", ".join("`"+field+"`" for field in fields) for idx, value in enumerate(values): insert_list.append(tuple(value)) diff --git a/frappe/patches/v4_1/__init__.py b/frappe/desk/doctype/form_tour/__init__.py similarity index 100% rename from frappe/patches/v4_1/__init__.py rename to frappe/desk/doctype/form_tour/__init__.py diff --git a/frappe/desk/doctype/form_tour/form_tour.js b/frappe/desk/doctype/form_tour/form_tour.js new file mode 100644 index 0000000000..94c6806b50 --- /dev/null +++ b/frappe/desk/doctype/form_tour/form_tour.js @@ -0,0 +1,24 @@ +// Copyright (c) 2021, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Form Tour', { + setup: function(frm) { + frm.set_query("reference_doctype", function() { + return { + filters: { + istable: 0 + } + }; + }); + + frm.set_query("field", "steps", function() { + return { + query: "frappe.desk.doctype.form_tour.form_tour.get_docfield_list", + filters: { + doctype: frm.doc.reference_doctype, + hidden: 0 + } + }; + }); + } +}); diff --git a/frappe/desk/doctype/form_tour/form_tour.json b/frappe/desk/doctype/form_tour/form_tour.json new file mode 100644 index 0000000000..8e09a5d63a --- /dev/null +++ b/frappe/desk/doctype/form_tour/form_tour.json @@ -0,0 +1,75 @@ +{ + "actions": [], + "autoname": "field:title", + "creation": "2021-05-21 23:02:52.242721", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "title", + "reference_doctype", + "completed", + "section_break_3", + "steps" + ], + "fields": [ + { + "fieldname": "reference_doctype", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Reference Document", + "options": "DocType", + "reqd": 1, + "unique": 1 + }, + { + "fieldname": "steps", + "fieldtype": "Table", + "label": "Steps", + "options": "Form Tour Step", + "reqd": 1 + }, + { + "default": "0", + "depends_on": "eval: doc.__islocal != 1", + "fieldname": "completed", + "fieldtype": "Check", + "label": "Mark as Completed" + }, + { + "fieldname": "section_break_3", + "fieldtype": "Section Break" + }, + { + "fieldname": "title", + "fieldtype": "Data", + "label": "Title", + "reqd": 1, + "unique": 1 + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2021-05-26 19:36:59.093753", + "modified_by": "Administrator", + "module": "Desk", + "name": "Form Tour", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/desk/doctype/form_tour/form_tour.py b/frappe/desk/doctype/form_tour/form_tour.py new file mode 100644 index 0000000000..dd762395c4 --- /dev/null +++ b/frappe/desk/doctype/form_tour/form_tour.py @@ -0,0 +1,32 @@ +# Copyright (c) 2021, Frappe Technologies and contributors +# For license information, please see license.txt + +import frappe +from frappe.model.document import Document + +class FormTour(Document): + pass + +@frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs +def get_docfield_list(doctype, txt, searchfield, start, page_len, filters): + or_filters = [ + ['fieldname', 'like', '%' + txt + '%'], + ['label', 'like', '%' + txt + '%'], + ['fieldtype', 'like', '%' + txt + '%'] + ] + + parent_doctype = filters.pop('doctype') + excluded_fieldtypes = ['Column Break'] + excluded_fieldtypes += filters.get('excluded_fieldtypes', []) + + docfields = frappe.get_all( + doctype, + fields=["name as value", "label", "fieldtype"], + filters={'parent': parent_doctype, 'fieldtype': ['not in', excluded_fieldtypes]}, + or_filters=or_filters, + limit_start=start, + limit_page_length=page_len, + as_list=1, + ) + return docfields diff --git a/frappe/desk/doctype/form_tour/test_form_tour.py b/frappe/desk/doctype/form_tour/test_form_tour.py new file mode 100644 index 0000000000..a4a796ce41 --- /dev/null +++ b/frappe/desk/doctype/form_tour/test_form_tour.py @@ -0,0 +1,8 @@ +# Copyright (c) 2021, Frappe Technologies and Contributors +# See license.txt + +# import frappe +import unittest + +class TestFormTour(unittest.TestCase): + pass diff --git a/frappe/patches/v4_2/__init__.py b/frappe/desk/doctype/form_tour_step/__init__.py similarity index 100% rename from frappe/patches/v4_2/__init__.py rename to frappe/desk/doctype/form_tour_step/__init__.py diff --git a/frappe/desk/doctype/form_tour_step/form_tour_step.json b/frappe/desk/doctype/form_tour_step/form_tour_step.json new file mode 100644 index 0000000000..a772a2498a --- /dev/null +++ b/frappe/desk/doctype/form_tour_step/form_tour_step.json @@ -0,0 +1,85 @@ +{ + "actions": [], + "creation": "2021-05-21 23:05:45.342114", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "field", + "title", + "description", + "column_break_2", + "position", + "fieldname", + "label", + "condition" + ], + "fields": [ + { + "fieldname": "title", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Title", + "reqd": 1 + }, + { + "columns": 4, + "fieldname": "description", + "fieldtype": "HTML Editor", + "in_list_view": 1, + "label": "Description", + "reqd": 1 + }, + { + "fieldname": "field", + "fieldtype": "Link", + "label": "Field", + "options": "DocField", + "reqd": 1 + }, + { + "fetch_from": "field.fieldname", + "fieldname": "fieldname", + "fieldtype": "Data", + "hidden": 1, + "label": "Fieldname", + "read_only": 1 + }, + { + "fetch_from": "field.label", + "fieldname": "label", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Label", + "read_only": 1 + }, + { + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, + { + "default": "Bottom", + "fieldname": "position", + "fieldtype": "Select", + "label": "Position", + "options": "Left\nLeft Center\nLeft Bottom\nTop\nTop Center\nTop Right\nRight\nRight Center\nRight Bottom\nBottom\nBottom Center\nBottom Right\nMid Center" + }, + { + "fieldname": "next_step_condition", + "fieldtype": "Code", + "label": "Next Step Condition", + "options": "JS" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-05-26 19:44:48.737453", + "modified_by": "Administrator", + "module": "Desk", + "name": "Form Tour Step", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} diff --git a/frappe/desk/doctype/form_tour_step/form_tour_step.py b/frappe/desk/doctype/form_tour_step/form_tour_step.py new file mode 100644 index 0000000000..0df5665c63 --- /dev/null +++ b/frappe/desk/doctype/form_tour_step/form_tour_step.py @@ -0,0 +1,8 @@ +# Copyright (c) 2021, Frappe Technologies and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + +class FormTourStep(Document): + pass diff --git a/frappe/desk/doctype/global_search_settings/global_search_settings.py b/frappe/desk/doctype/global_search_settings/global_search_settings.py index 28a1ed8239..9112349c1b 100644 --- a/frappe/desk/doctype/global_search_settings/global_search_settings.py +++ b/frappe/desk/doctype/global_search_settings/global_search_settings.py @@ -21,7 +21,7 @@ class GlobalSearchSettings(Document): dts.append(dt.document_type) if core_dts: - core_dts = (", ".join([frappe.bold(dt) for dt in core_dts])) + core_dts = ", ".join(frappe.bold(dt) for dt in core_dts) frappe.throw(_("Core Modules {0} cannot be searched in Global Search.").format(core_dts)) if repeated_dts: @@ -60,7 +60,7 @@ def update_global_search_doctypes(): if search_doctypes.get(domain): global_search_doctypes.extend(search_doctypes.get(domain)) - doctype_list = set([dt.name for dt in frappe.get_all("DocType")]) + doctype_list = {dt.name for dt in frappe.get_all("DocType")} allowed_in_global_search = [] for dt in global_search_doctypes: diff --git a/frappe/desk/doctype/tag/tag.py b/frappe/desk/doctype/tag/tag.py index 3c67bb4668..4ea5c9cd7e 100644 --- a/frappe/desk/doctype/tag/tag.py +++ b/frappe/desk/doctype/tag/tag.py @@ -131,7 +131,7 @@ def update_tags(doc, tags): :param doc: Document to be added to global tags """ - new_tags = list(set([tag.strip() for tag in tags.split(",") if tag])) + new_tags = {tag.strip() for tag in tags.split(",") if tag} for tag in new_tags: if not frappe.db.exists("Tag Link", {"parenttype": doc.doctype, "parent": doc.name, "tag": tag}): @@ -186,4 +186,4 @@ def get_documents_for_tag(tag): @frappe.whitelist() def get_tags_list_for_awesomebar(): - return [t.name for t in frappe.get_list("Tag")] \ No newline at end of file + return [t.name for t in frappe.get_list("Tag")] diff --git a/frappe/desk/doctype/workspace/workspace.py b/frappe/desk/doctype/workspace/workspace.py index 0329e0f7d2..0b5babc8d9 100644 --- a/frappe/desk/doctype/workspace/workspace.py +++ b/frappe/desk/doctype/workspace/workspace.py @@ -55,8 +55,7 @@ class Workspace(Document): for link in self.links: link = link.as_dict() if link.type == "Card Break": - - if card_links: + if card_links and (not current_card.only_for or current_card.only_for == frappe.get_system_settings('country')): current_card['links'] = card_links cards.append(current_card) diff --git a/frappe/email/doctype/email_account/email_account.py b/frappe/email/doctype/email_account/email_account.py index c77ba00021..ecd59f42bb 100755 --- a/frappe/email/doctype/email_account/email_account.py +++ b/frappe/email/doctype/email_account/email_account.py @@ -1,32 +1,25 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt -import frappe +import email.utils +import functools import imaplib -import re -import json import socket import time -import functools - -import email.utils - -from frappe import _, are_emails_muted -from frappe.model.document import Document -from frappe.utils import (validate_email_address, cint, cstr, get_datetime, - DATE_FORMAT, strip, comma_or, sanitize_html, add_days, parse_addr) -from frappe.utils.user import is_system_user -from frappe.utils.jinja import render_template -from frappe.email.smtp import SMTPServer -from frappe.email.receive import EmailServer, InboundMail, SentEmailInInboxError -from poplib import error_proto -from dateutil.relativedelta import relativedelta from datetime import datetime, timedelta +from poplib import error_proto + +import frappe +from frappe import _, are_emails_muted, safe_encode from frappe.desk.form import assign_to -from frappe.utils.user import get_system_managers -from frappe.utils.background_jobs import enqueue, get_jobs -from frappe.utils.html_utils import clean_email_html -from frappe.utils.error import raise_error_on_no_output +from frappe.email.receive import EmailServer, InboundMail, SentEmailInInboxError +from frappe.email.smtp import SMTPServer from frappe.email.utils import get_port +from frappe.model.document import Document +from frappe.utils import cint, comma_or, cstr, parse_addr, validate_email_address +from frappe.utils.background_jobs import enqueue, get_jobs +from frappe.utils.error import raise_error_on_no_output +from frappe.utils.jinja import render_template +from frappe.utils.user import get_system_managers OUTGOING_EMAIL_ACCOUNT_MISSING = _("Please setup default Email Account from Setup > Email > Email Account") @@ -441,10 +434,7 @@ class EmailAccount(Document): if self.enable_auto_reply: self.send_auto_reply(communication, mail) - attachments = [] - if hasattr(communication, '_attachments'): - attachments = [d.file_name for d in communication._attachments] - communication.notify(attachments=attachments, fetched_from_email_account=True) + communication.send_email(is_inbound_mail_communcation=True) except SentEmailInInboxError: frappe.db.rollback() except Exception: @@ -453,6 +443,8 @@ class EmailAccount(Document): if self.use_imap: self.handle_bad_emails(mail.uid, mail.raw_message, frappe.get_traceback()) exceptions.append(frappe.get_traceback()) + else: + frappe.db.commit() #notify if user is linked to account if len(inbound_mails)>0 and not frappe.local.flags.in_test: @@ -478,14 +470,13 @@ class EmailAccount(Document): email_server = self.get_incoming_server(in_receive=True, email_sync_rule=email_sync_rule) messages = email_server.get_messages() or {} except Exception: - raise frappe.log_error(title=_("Error while connecting to email account {0}").format(self.name)) return [] mails = [] for index, message in enumerate(messages.get("latest_messages", [])): - uid = messages['uid_list'][index] - seen_status = 1 if messages['seen_status'][uid]=='SEEN' else 0 + uid = messages['uid_list'][index] if messages.get('uid_list') else None + seen_status = 1 if messages.get('seen_status', {}).get(uid)=='SEEN' else 0 mails.append(InboundMail(message, self, uid, seen_status)) return mails @@ -579,8 +570,8 @@ class EmailAccount(Document): email_server.update_flag(uid_list=uid_list) # mark communication as read - docnames = ",".join([ "'%s'"%flag.get("communication") for flag in flags \ - if flag.get("action") == "Read" ]) + docnames = ",".join("'%s'"%flag.get("communication") for flag in flags \ + if flag.get("action") == "Read") self.set_communication_seen_status(docnames, seen=1) # mark communication as unread @@ -610,7 +601,6 @@ class EmailAccount(Document): def append_email_to_sent_folder(self, message): - email_server = None try: email_server = self.get_incoming_server(in_receive=True) @@ -624,7 +614,8 @@ class EmailAccount(Document): if email_server.imap: try: - email_server.imap.append("Sent", "\\Seen", imaplib.Time2Internaldate(time.time()), message.encode()) + message = safe_encode(message) + email_server.imap.append("Sent", "\\Seen", imaplib.Time2Internaldate(time.time()), message) except Exception: frappe.log_error() diff --git a/frappe/email/doctype/email_group/email_group.json b/frappe/email/doctype/email_group/email_group.json index c49de841e6..cb74249143 100644 --- a/frappe/email/doctype/email_group/email_group.json +++ b/frappe/email/doctype/email_group/email_group.json @@ -1,6 +1,7 @@ { "actions": [], "allow_import": 1, + "allow_rename": 1, "autoname": "field:title", "creation": "2015-03-18 06:08:32.729800", "doctype": "DocType", @@ -50,7 +51,7 @@ "link_fieldname": "email_group" } ], - "modified": "2020-09-24 16:41:55.286377", + "modified": "2021-06-15 11:25:13.556201", "modified_by": "Administrator", "module": "Email", "name": "Email Group", diff --git a/frappe/email/doctype/email_queue/email_queue.py b/frappe/email/doctype/email_queue/email_queue.py index dad473b8aa..e1e332f978 100644 --- a/frappe/email/doctype/email_queue/email_queue.py +++ b/frappe/email/doctype/email_queue/email_queue.py @@ -179,7 +179,14 @@ class SendMailContext: else: email_status = self.is_mail_sent_to_all() and 'Sent' email_status = email_status or (self.sent_to and 'Partially Sent') or 'Not Sent' - self.queue_doc.update_status(status = email_status, commit = True) + + update_fields = {'status': email_status} + if self.email_account_doc.is_exists_in_db(): + update_fields['email_account'] = self.email_account_doc.name + else: + update_fields['email_account'] = None + + self.queue_doc.update_status(**update_fields, commit = True) def log_exception(self, exc_type, exc_val, exc_tb): if exc_type: diff --git a/frappe/email/doctype/newsletter/test_newsletter.py b/frappe/email/doctype/newsletter/test_newsletter.py index cfd0df53a9..3abd339ed9 100644 --- a/frappe/email/doctype/newsletter/test_newsletter.py +++ b/frappe/email/doctype/newsletter/test_newsletter.py @@ -42,7 +42,7 @@ class TestNewsletter(unittest.TestCase): email_queue_list = [frappe.get_doc("Email Queue", e.name) for e in frappe.get_all("Email Queue")] self.assertEqual(len(email_queue_list), 4) - recipients = set([e.recipients[0].recipient for e in email_queue_list]) + recipients = {e.recipients[0].recipient for e in email_queue_list} self.assertTrue(set(emails).issubset(recipients)) def test_unsubscribe(self): diff --git a/frappe/email/inbox.py b/frappe/email/inbox.py index 5f8f516772..c6020e14e4 100644 --- a/frappe/email/inbox.py +++ b/frappe/email/inbox.py @@ -18,7 +18,7 @@ def get_email_accounts(user=None): "all_accounts": "" } - all_accounts = ",".join([ account.get("email_account") for account in accounts ]) + all_accounts = ",".join(account.get("email_account") for account in accounts) if len(accounts) > 1: email_accounts.append({ "email_account": all_accounts, diff --git a/frappe/email/queue.py b/frappe/email/queue.py index ca96981aa8..885a306cfb 100755 --- a/frappe/email/queue.py +++ b/frappe/email/queue.py @@ -6,15 +6,61 @@ from frappe import msgprint, _ from frappe.utils.verified_command import get_signed_params, verify_request from frappe.utils import get_url, now_datetime, cint -def get_emails_sent_this_month(): - return frappe.db.sql(""" - SELECT COUNT(*) FROM `tabEmail Queue` - WHERE `status`='Sent' AND EXTRACT(YEAR_MONTH FROM `creation`) = EXTRACT(YEAR_MONTH FROM NOW()) - """)[0][0] +def get_emails_sent_this_month(email_account=None): + """Get count of emails sent from a specific email account. -def get_emails_sent_today(): - return frappe.db.sql("""SELECT COUNT(`name`) FROM `tabEmail Queue` WHERE - `status` in ('Sent', 'Not Sent', 'Sending') AND `creation` > (NOW() - INTERVAL '24' HOUR)""")[0][0] + :param email_account: name of the email account used to send mail + + if email_account=None, email account filter is not applied while counting + """ + q = """ + SELECT + COUNT(*) + FROM + `tabEmail Queue` + WHERE + `status`='Sent' + AND + EXTRACT(YEAR_MONTH FROM `creation`) = EXTRACT(YEAR_MONTH FROM NOW()) + """ + + q_args = {} + if email_account is not None: + if email_account: + q += " AND email_account = %(email_account)s" + q_args['email_account'] = email_account + else: + q += " AND (email_account is null OR email_account='')" + + return frappe.db.sql(q, q_args)[0][0] + +def get_emails_sent_today(email_account=None): + """Get count of emails sent from a specific email account. + + :param email_account: name of the email account used to send mail + + if email_account=None, email account filter is not applied while counting + """ + q = """ + SELECT + COUNT(`name`) + FROM + `tabEmail Queue` + WHERE + `status` in ('Sent', 'Not Sent', 'Sending') + AND + `creation` > (NOW() - INTERVAL '24' HOUR) + """ + + q_args = {} + if email_account is not None: + if email_account: + q += " AND email_account = %(email_account)s" + q_args['email_account'] = email_account + else: + q += " AND (email_account is null OR email_account='')" + + return frappe.db.sql(q, q_args)[0][0] def get_unsubscribe_message(unsubscribe_message, expose_recipients): if unsubscribe_message: diff --git a/frappe/email/receive.py b/frappe/email/receive.py index 7da4840df1..2e42008951 100644 --- a/frappe/email/receive.py +++ b/frappe/email/receive.py @@ -738,9 +738,6 @@ class InboundMail(Email): if not reference_document and self.email_account.append_to: reference_document = self.match_record_by_subject_and_sender(self.email_account.append_to) - # if not reference_document: - # reference_document = Create_reference_document(self.email_account.append_to) - self._reference_document = reference_document or '' return self._reference_document @@ -805,7 +802,7 @@ class InboundMail(Email): except frappe.DuplicateEntryError: # try and find matching parent parent_name = frappe.db.get_value(self.email_account.append_to, - {email_fileds.sender_field: email.from_email} + {email_fileds.sender_field: self.from_email} ) if parent_name: parent.name = parent_name diff --git a/frappe/installer.py b/frappe/installer.py index d7d885d60e..d4d8117fcb 100755 --- a/frappe/installer.py +++ b/frappe/installer.py @@ -282,10 +282,10 @@ def remove_app(app_name, dry_run=False, yes=False, no_backup=False, force=False) def post_install(rebuild_website=False): - from frappe.website import render + from frappe.website.utils import clear_website_cache if rebuild_website: - render.clear_cache() + clear_website_cache() init_singles() frappe.db.commit() @@ -537,7 +537,7 @@ def is_downgrade(sql_file_path, verbose=False): def is_partial(sql_file_path): with open(sql_file_path) as f: - header = " ".join([f.readline() for _ in range(5)]) + header = " ".join(f.readline() for _ in range(5)) if "Partial Backup" in header: return True return False diff --git a/frappe/integrations/doctype/ldap_settings/ldap_settings.py b/frappe/integrations/doctype/ldap_settings/ldap_settings.py index 122096cf6f..acc8b96679 100644 --- a/frappe/integrations/doctype/ldap_settings/ldap_settings.py +++ b/frappe/integrations/doctype/ldap_settings/ldap_settings.py @@ -79,7 +79,7 @@ class LDAPSettings(Document): def sync_roles(self, user, additional_groups=None): - current_roles = set([d.role for d in user.get("roles")]) + current_roles = set(d.role for d in user.get("roles")) needed_roles = set() needed_roles.add(self.default_role) diff --git a/frappe/migrate.py b/frappe/migrate.py index d19e255639..061e4c98d7 100644 --- a/frappe/migrate.py +++ b/frappe/migrate.py @@ -13,7 +13,7 @@ from frappe.utils.connections import check_connection from frappe.utils.dashboard import sync_dashboards from frappe.cache_manager import clear_global_cache from frappe.desk.notifications import clear_notifications -from frappe.website import render +from frappe.website.utils import clear_website_cache from frappe.core.doctype.language.language import sync_languages from frappe.modules.utils import sync_customizations from frappe.core.doctype.scheduled_job_type.scheduled_job_type import sync_jobs @@ -76,7 +76,7 @@ Otherwise, check the server logs and ensure that all the required services are r frappe.get_doc('Portal Settings', 'Portal Settings').sync_menu() # syncs statics - render.clear_cache() + clear_website_cache() # updating installed applications data frappe.get_single('Installed Applications').update_versions() diff --git a/frappe/model/__init__.py b/frappe/model/__init__.py index dd93fbcc18..75122f5aba 100644 --- a/frappe/model/__init__.py +++ b/frappe/model/__init__.py @@ -165,7 +165,7 @@ def delete_fields(args_dict, delete=0): frappe.db.sql(""" DELETE FROM `tabSingles` WHERE doctype='%s' AND field IN (%s) - """ % (dt, ", ".join(["'{}'".format(f) for f in fields]))) + """ % (dt, ", ".join("'{}'".format(f) for f in fields))) else: existing_fields = frappe.db.multisql({ "mariadb": "DESC `tab%s`" % dt, @@ -188,7 +188,7 @@ def delete_fields(args_dict, delete=0): frappe.db.commit() query = "ALTER TABLE `tab%s` " % dt + \ - ", ".join(["DROP COLUMN `%s`" % f for f in fields_need_to_delete]) + ", ".join("DROP COLUMN `%s`" % f for f in fields_need_to_delete) frappe.db.sql(query) if frappe.db.db_type == 'postgres': diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index 2f5154cfd9..af696e116d 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -354,7 +354,7 @@ class BaseDocument(object): frappe.db.sql("""INSERT INTO `tab{doctype}` ({columns}) VALUES ({values})""".format( doctype = self.doctype, - columns = ", ".join(["`"+c+"`" for c in columns]), + columns = ", ".join("`"+c+"`" for c in columns), values = ", ".join(["%s"] * len(columns)) ), list(d.values())) except Exception as e: @@ -397,7 +397,7 @@ class BaseDocument(object): frappe.db.sql("""UPDATE `tab{doctype}` SET {values} WHERE `name`=%s""".format( doctype = self.doctype, - values = ", ".join(["`"+c+"`=%s" for c in columns]) + values = ", ".join("`"+c+"`=%s" for c in columns) ), list(d.values()) + [name]) except Exception as e: if frappe.db.is_unique_key_violation(e): diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py index 7f22282acf..7ed681644f 100644 --- a/frappe/model/db_query.py +++ b/frappe/model/db_query.py @@ -43,8 +43,14 @@ class DatabaseQuery(object): # filters and fields swappable # its hard to remember what comes first - if (isinstance(fields, dict) - or (isinstance(fields, list) and fields and isinstance(fields[0], list))): + if ( + isinstance(fields, dict) + or ( + fields + and isinstance(fields, list) + and isinstance(fields[0], list) + ) + ): # if fields is given as dict/list of list, its probably filters filters, fields = fields, filters @@ -56,10 +62,7 @@ class DatabaseQuery(object): if fields: self.fields = fields else: - if pluck: - self.fields = ["`tab{0}`.`{1}`".format(self.doctype, pluck)] - else: - self.fields = ["`tab{0}`.`name`".format(self.doctype)] + self.fields = [f"`tab{self.doctype}`.`{pluck or 'name'}`"] if start: limit_start = start if page_length: limit_page_length = page_length @@ -70,7 +73,7 @@ class DatabaseQuery(object): self.docstatus = docstatus or [] self.group_by = group_by self.order_by = order_by - self.limit_start = 0 if (limit_start is False) else cint(limit_start) + self.limit_start = cint(limit_start) self.limit_page_length = cint(limit_page_length) if limit_page_length else None self.with_childnames = with_childnames self.debug = debug @@ -157,11 +160,10 @@ class DatabaseQuery(object): # left join parent, child tables for child in self.tables[1:]: - args.tables += " {join} {child} on ({child}.parent = {main}.name)".format(join=self.join, - child=child, main=self.tables[0]) + args.tables += f" {self.join} {child} on ({child}.parent = {self.tables[0]}.name)" if self.grouped_or_conditions: - self.conditions.append("({0})".format(" or ".join(self.grouped_or_conditions))) + self.conditions.append(f"({' or '.join(self.grouped_or_conditions)})") args.conditions = ' and '.join(self.conditions) @@ -186,9 +188,9 @@ class DatabaseQuery(object): fields.append(field) elif "as" in field.lower().split(" "): col, _, new = field.split() - fields.append("`{0}` as {1}".format(col, new)) + fields.append(f"`{col}` as {new}") else: - fields.append("`{0}`".format(field)) + fields.append(f"`{field}`") args.fields = ", ".join(fields) @@ -260,10 +262,10 @@ class DatabaseQuery(object): if any(keyword in field.lower().split() for keyword in blacklisted_keywords): _raise_exception() - if any("({0}".format(keyword) in field.lower() for keyword in blacklisted_keywords): + if any(f"({keyword}" in field.lower() for keyword in blacklisted_keywords): _raise_exception() - if any("{0}(".format(keyword) in field.lower() for keyword in blacklisted_functions): + if any(f"{keyword}(" in field.lower() for keyword in blacklisted_functions): _raise_exception() if '@' in field.lower(): @@ -287,22 +289,30 @@ class DatabaseQuery(object): def extract_tables(self): """extract tables from fields""" - self.tables = ['`tab' + self.doctype + '`'] - + self.tables = [f"`tab{self.doctype}`"] + sql_functions = [ + "dayofyear(", + "extract(", + "locate(", + "strpos(", + "count(", + "sum(", + "avg(", + ] # add tables from fields if self.fields: - for f in self.fields: - if ( not ("tab" in f and "." in f) ) or ("locate(" in f) or ("strpos(" in f) or \ - ("count(" in f) or ("avg(" in f) or ("sum(" in f) or ("extract(" in f) or ("dayofyear(" in f): + for field in self.fields: + if not ("tab" in field and "." in field) or any(x for x in sql_functions if x in field): continue - table_name = f.split('.')[0] + table_name = field.split('.')[0] + if table_name.lower().startswith('group_concat('): table_name = table_name[13:] if table_name.lower().startswith('ifnull('): table_name = table_name[7:] if not table_name[0]=='`': - table_name = '`' + table_name + '`' + table_name = f"`{table_name}`" if not table_name in self.tables: self.append_table(table_name) @@ -311,8 +321,7 @@ class DatabaseQuery(object): doctype = table_name[4:-1] ptype = 'select' if frappe.only_has_select_perm(doctype) else 'read' - if (not self.flags.ignore_permissions) and\ - (not frappe.has_permission(doctype, ptype=ptype)): + if not self.flags.ignore_permissions and not frappe.has_permission(doctype, ptype=ptype): frappe.flags.error_message = _('Insufficient Permission for {0}').format(frappe.bold(doctype)) raise frappe.PermissionError(doctype) @@ -326,7 +335,7 @@ class DatabaseQuery(object): if len(self.tables) > 1: for idx, field in enumerate(self.fields): if '.' not in field and not _in_standard_sql_methods(field): - self.fields[idx] = '{0}.{1}'.format(self.tables[0], field) + self.fields[idx] = f"{self.tables[0]}.{field}" def get_table_columns(self): try: @@ -375,7 +384,7 @@ class DatabaseQuery(object): if not self.flags.ignore_permissions: match_conditions = self.build_match_conditions() if match_conditions: - self.conditions.append("(" + match_conditions + ")") + self.conditions.append(f"({match_conditions})") def build_filter_conditions(self, filters, conditions, ignore_permissions=None): """build conditions from user filters""" @@ -407,8 +416,7 @@ class DatabaseQuery(object): if 'ifnull(' in f.fieldname: column_name = f.fieldname else: - column_name = '{tname}.{fname}'.format(tname=tname, - fname=f.fieldname) + column_name = f"{tname}.{f.fieldname}" can_be_null = True @@ -450,7 +458,7 @@ class DatabaseQuery(object): fallback = "''" value = [frappe.db.escape((v.name or '').strip(), percent=False) for v in result] if len(value): - value = "({0})".format(", ".join(value)) + value = f"({', '.join(value)})" else: value = "('')" # changing operator to IN as the above code fetches all the parent / child values and convert into tuple @@ -466,7 +474,7 @@ class DatabaseQuery(object): fallback = "''" value = [frappe.db.escape((v or '').strip(), percent=False) for v in values] if len(value): - value = "({0})".format(", ".join(value)) + value = f"({', '.join(value)})" else: value = "('')" else: @@ -503,7 +511,7 @@ class DatabaseQuery(object): can_be_null = True if 'ifnull' not in column_name: - column_name = 'ifnull({}, {})'.format(column_name, fallback) + column_name = f'ifnull({column_name}, {fallback})' elif df and df.fieldtype=="Date": value = frappe.db.format_date(f.value) @@ -540,21 +548,19 @@ class DatabaseQuery(object): # escape value if isinstance(value, str) and not f.operator.lower() == 'between': - value = "{0}".format(frappe.db.escape(value, percent=False)) + value = f"{frappe.db.escape(value, percent=False)}" - if (self.ignore_ifnull + if ( + self.ignore_ifnull or not can_be_null or (f.value and f.operator.lower() in ('=', 'like')) - or 'ifnull(' in column_name.lower()): + or 'ifnull(' in column_name.lower() + ): if f.operator.lower() == 'like' and frappe.conf.get('db_type') == 'postgres': f.operator = 'ilike' - condition = '{column_name} {operator} {value}'.format( - column_name=column_name, operator=f.operator, - value=value) + condition = f'{column_name} {f.operator} {value}' else: - condition = 'ifnull({column_name}, {fallback}) {operator} {value}'.format( - column_name=column_name, fallback=fallback, operator=f.operator, - value=value) + condition = f'ifnull({column_name}, {fallback}) {f.operator} {value}' return condition @@ -572,10 +578,12 @@ class DatabaseQuery(object): role_permissions = frappe.permissions.get_role_permissions(meta, user=self.user) self.shared = frappe.share.get_shared(self.doctype, self.user) - if (not meta.istable and + if ( + not meta.istable and not (role_permissions.get("select") or role_permissions.get("read")) and not self.flags.ignore_permissions and - not has_any_user_permission_for_doctype(self.doctype, self.user, self.reference_doctype)): + not has_any_user_permission_for_doctype(self.doctype, self.user, self.reference_doctype) + ): only_if_shared = True if not self.shared: frappe.throw(_("No permission to read {0}").format(self.doctype), frappe.PermissionError) @@ -585,8 +593,10 @@ class DatabaseQuery(object): else: #if has if_owner permission skip user perm check if role_permissions.get("has_if_owner_enabled") and role_permissions.get("if_owner", {}): - self.match_conditions.append("`tab{0}`.`owner` = {1}".format(self.doctype, - frappe.db.escape(self.user, percent=False))) + self.match_conditions.append( + f"`tab{self.doctype}`.`owner` = {frappe.db.escape(self.user, percent=False)}" + ) + # add user permission only if role has read perm elif role_permissions.get("read") or role_permissions.get("select"): # get user permissions @@ -605,8 +615,7 @@ class DatabaseQuery(object): # share is an OR condition, if there is a role permission if not only_if_shared and self.shared and conditions: - conditions = "({conditions}) or ({shared_condition})".format( - conditions=conditions, shared_condition=self.get_share_condition()) + conditions = f"({conditions}) or ({self.get_share_condition()})" return conditions @@ -614,8 +623,7 @@ class DatabaseQuery(object): return self.match_filters def get_share_condition(self): - return """`tab{0}`.name in ({1})""".format(self.doctype, ", ".join(["%s"] * len(self.shared))) % \ - tuple([frappe.db.escape(s, percent=False) for s in self.shared]) + return f"`tab{self.doctype}`.name in ({', '.join(frappe.db.escape(s, percent=False) for s in self.shared)})" def add_user_permissions(self, user_permissions): meta = frappe.get_meta(self.doctype) @@ -640,9 +648,7 @@ class DatabaseQuery(object): if frappe.get_system_settings("apply_strict_user_permissions"): condition = "" else: - empty_value_condition = "ifnull(`tab{doctype}`.`{fieldname}`, '')=''".format( - doctype=self.doctype, fieldname=df.get('fieldname') - ) + empty_value_condition = f"ifnull(`tab{self.doctype}`.`{df.get('fieldname')}`, '')=''" condition = empty_value_condition + " or " for permission in user_permission_values: @@ -650,9 +656,7 @@ class DatabaseQuery(object): docs.append(permission.get('doc')) # append docs based on user permission applicable on reference doctype - # this is useful when getting list of docs from a link field - # in this case parent doctype of the link # will be the reference doctype @@ -664,14 +668,9 @@ class DatabaseQuery(object): docs.append(permission.get('doc')) if docs: - condition += "`tab{doctype}`.`{fieldname}` in ({values})".format( - doctype=self.doctype, - fieldname=df.get('fieldname'), - values=", ".join( - [(frappe.db.escape(doc, percent=False)) for doc in docs]) - ) - - match_conditions.append("({condition})".format(condition=condition)) + values = ", ".join(frappe.db.escape(doc, percent=False) for doc in docs) + condition += f"`tab{self.doctype}`.`{df.get('fieldname')}` in ({values})" + match_conditions.append(f"({condition})") match_filters[df.get('options')] = docs if match_conditions: @@ -721,17 +720,17 @@ class DatabaseQuery(object): # `idx desc, modified desc` # will covert to # `tabItem`.`idx` desc, `tabItem`.`modified` desc - args.order_by = ', '.join(['`tab{0}`.`{1}` {2}'.format(self.doctype, - f.split()[0].strip(), f.split()[1].strip()) for f in meta.sort_field.split(',')]) + args.order_by = ', '.join( + f"`tab{self.doctype}`.`{f.split()[0].strip()}` {f.split()[1].strip()}" for f in meta.sort_field.split(',') + ) else: sort_field = meta.sort_field or 'modified' sort_order = (meta.sort_field and meta.sort_order) or 'desc' - - args.order_by = "`tab{0}`.`{1}` {2}".format(self.doctype, sort_field or "modified", sort_order or "desc") + args.order_by = f"`tab{self.doctype}`.`{sort_field or 'modified'}` {sort_order or 'desc'}" # draft docs always on top - if meta.is_submittable: - args.order_by = "`tab{0}`.docstatus asc, {1}".format(self.doctype, args.order_by) + if hasattr(meta, 'is_submittable') and meta.is_submittable: + args.order_by = f"`tab{self.doctype}`.docstatus asc, {args.order_by}" def validate_order_by_and_group_by(self, parameters): """Check order by, group by so that atleast one column is selected and does not have subquery""" @@ -802,17 +801,16 @@ def get_order_by(doctype, meta): # `idx desc, modified desc` # will covert to # `tabItem`.`idx` desc, `tabItem`.`modified` desc - order_by = ', '.join(['`tab{0}`.`{1}` {2}'.format(doctype, - f.split()[0].strip(), f.split()[1].strip()) for f in meta.sort_field.split(',')]) + order_by = ', '.join(f"`tab{doctype}`.`{f.split()[0].strip()}` {f.split()[1].strip()}" for f in meta.sort_field.split(',')) + else: sort_field = meta.sort_field or 'modified' sort_order = (meta.sort_field and meta.sort_order) or 'desc' - - order_by = "`tab{0}`.`{1}` {2}".format(doctype, sort_field or "modified", sort_order or "desc") + order_by = f"`tab{doctype}`.`{sort_field or 'modified'}` {sort_order or 'desc'}" # draft docs always on top if meta.is_submittable: - order_by = "`tab{0}`.docstatus asc, {1}".format(doctype, order_by) + order_by = f"`tab{doctype}`.docstatus asc, {order_by}" return order_by diff --git a/frappe/model/document.py b/frappe/model/document.py index 8f57aae475..61160e1f01 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -14,8 +14,8 @@ from frappe.model.workflow import set_workflow_state_on_action from frappe.utils.global_search import update_global_search from frappe.integrations.doctype.webhook import run_webhooks from frappe.desk.form.document_follow import follow_document -from frappe.desk.utils import slug from frappe.core.doctype.server_script.server_script_utils import run_server_script_for_doc_event +from frappe.utils.data import get_absolute_url # once_only validation # methods @@ -1200,8 +1200,8 @@ class Document(BaseDocument): doc.set(fieldname, flt(doc.get(fieldname), self.precision(fieldname, doc.parentfield))) def get_url(self): - """Returns Desk URL for this document. `/app/{doctype}/{name}`""" - return f"/app/{slug(self.doctype)}/{self.name}" + """Returns Desk URL for this document.""" + return get_absolute_url(self.doctype, self.name) def add_comment(self, comment_type='Comment', text=None, comment_email=None, link_doctype=None, link_name=None, comment_by=None): """Add a comment to this document. diff --git a/frappe/model/meta.py b/frappe/model/meta.py index b67c41c990..b212324208 100644 --- a/frappe/model/meta.py +++ b/frappe/model/meta.py @@ -664,7 +664,7 @@ def trim_tables(doctype=None): and not f.startswith("_")] if columns_to_remove: print(doctype, "columns removed:", columns_to_remove) - columns_to_remove = ", ".join(["drop `{0}`".format(c) for c in columns_to_remove]) + columns_to_remove = ", ".join("drop `{0}`".format(c) for c in columns_to_remove) query = """alter table `tab{doctype}` {columns}""".format( doctype=doctype, columns=columns_to_remove) frappe.db.sql_ddl(query) diff --git a/frappe/model/rename_doc.py b/frappe/model/rename_doc.py index fc5b3ca9fe..9b8ac2574d 100644 --- a/frappe/model/rename_doc.py +++ b/frappe/model/rename_doc.py @@ -141,7 +141,7 @@ def update_user_settings(old, new, link_fields): if not link_fields: return # find the user settings for the linked doctypes - linked_doctypes = set([d.parent for d in link_fields if not d.issingle]) + linked_doctypes = {d.parent for d in link_fields if not d.issingle} user_settings_details = frappe.db.sql('''SELECT `user`, `doctype`, `data` FROM `__UserSettings` WHERE `data` like %s diff --git a/frappe/parallel_test_runner.py b/frappe/parallel_test_runner.py index 1dbb24f191..2f83b88572 100644 --- a/frappe/parallel_test_runner.py +++ b/frappe/parallel_test_runner.py @@ -114,13 +114,30 @@ class ParallelTestRunner(): # Generate coverage report only for app that is being tested source_path = os.path.join(get_bench_path(), 'apps', self.app) - omit=['*.html', '*.js', '*.xml', '*.css', '*.less', '*.scss', - '*.vue', '*/doctype/*/*_dashboard.py', '*/patches/*'] + incl = [ + '*.py', + ] + omit = [ + '*.js', + '*.xml', + '*.pyc', + '*.css', + '*.less', + '*.scss', + '*.vue', + '*.pyc', + '*.html', + '*/test_*', + '*/node_modules/*', + '*/doctype/*/*_dashboard.py', + '*/patches/*', + ] if self.app == 'frappe': + omit.append('*/tests/*') omit.append('*/commands/*') - self.coverage = Coverage(source=[source_path], omit=omit) + self.coverage = Coverage(source=[source_path], omit=omit, include=incl) self.coverage.start() def save_coverage(self): diff --git a/frappe/patches.txt b/frappe/patches.txt index e70be0a37b..7605d8ea2b 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -1,11 +1,5 @@ frappe.patches.v12_0.remove_deprecated_fields_from_doctype #3 -execute:frappe.db.sql("""update `tabPatch Log` set patch=replace(patch, '.4_0.', '.v4_0.')""") #2014-05-12 -frappe.patches.v5_0.convert_to_barracuda_and_utf8mb4 execute:frappe.utils.global_search.setup_global_search_table() -frappe.patches.v8_0.update_global_search_table -frappe.patches.v7_0.update_auth -frappe.patches.v8_0.drop_in_dialog #2017-09-22 -frappe.patches.v7_2.remove_in_filter execute:frappe.reload_doc('core', 'doctype', 'doctype_action', force=True) #2019-09-23 execute:frappe.reload_doc('core', 'doctype', 'doctype_link', force=True) #2020-10-17 execute:frappe.reload_doc('core', 'doctype', 'doctype', force=True) #2017-09-22 @@ -14,7 +8,6 @@ frappe.patches.v11_0.drop_column_apply_user_permissions execute:frappe.reload_doc('core', 'doctype', 'custom_docperm') execute:frappe.reload_doc('core', 'doctype', 'docperm') #2018-05-29 execute:frappe.reload_doc('core', 'doctype', 'comment') -frappe.patches.v8_0.drop_is_custom_from_docperm execute:frappe.reload_doc('core', 'doctype', 'document_naming_rule', force=True) execute:frappe.reload_doc('core', 'doctype', 'module_def') #2020-08-28 execute:frappe.reload_doc('core', 'doctype', 'version') #2017-04-01 @@ -25,190 +18,40 @@ execute:frappe.reload_doc('core', 'doctype', 'communication') #2019-10-02 execute:frappe.reload_doc('core', 'doctype', 'server_script') frappe.patches.v11_0.replicate_old_user_permissions frappe.patches.v11_0.reload_and_rename_view_log #2019-01-03 -frappe.patches.v7_1.rename_scheduler_log_to_error_log -frappe.patches.v6_1.rename_file_data -frappe.patches.v7_0.re_route #2016-06-27 -frappe.patches.v8_0.update_records_in_global_search #11-05-2017 -frappe.patches.v8_0.update_published_in_global_search frappe.patches.v11_0.copy_fetch_data_from_options frappe.patches.v11_0.change_email_signature_fieldtype execute:frappe.reload_doc('core', 'doctype', 'activity_log') execute:frappe.reload_doc('core', 'doctype', 'deleted_document') execute:frappe.reload_doc('core', 'doctype', 'domain_settings') frappe.patches.v13_0.rename_custom_client_script -frappe.patches.v8_0.rename_page_role_to_has_role #2017-03-16 -frappe.patches.v7_2.setup_custom_perms #2017-01-19 -frappe.patches.v8_0.set_user_permission_for_page_and_report #2017-03-20 execute:frappe.reload_doc('core', 'doctype', 'role') #2017-05-23 execute:frappe.reload_doc('core', 'doctype', 'user') #2017-10-27 -execute:frappe.reload_doc('custom', 'doctype', 'custom_field') #2015-10-19 -execute:frappe.reload_doc('core', 'doctype', 'page') #2013-13-26 execute:frappe.reload_doc('core', 'doctype', 'report_column') execute:frappe.reload_doc('core', 'doctype', 'report_filter') execute:frappe.reload_doc('core', 'doctype', 'report') #2020-08-25 -execute:frappe.reload_doc('core', 'doctype', 'translation') #2016-03-03 -execute:frappe.reload_doc('email', 'doctype', 'email_alert') #2014-07-15 -execute:frappe.reload_doc('desk', 'doctype', 'todo') #2014-12-31-1 -execute:frappe.reload_doc('custom', 'doctype', 'property_setter') #2014-12-31-1 -execute:frappe.reload_doc('core', 'doctype', 'patch_log') #2016-10-31 -execute:frappe.reload_doctype("File") # 2015-10-19 execute:frappe.reload_doc('core', 'doctype', 'error_snapshot') -execute:frappe.clear_cache() -frappe.patches.v7_1.rename_scheduler_log_to_error_log -frappe.patches.v7_1.sync_language_doctype -frappe.patches.v7_0.rename_bulk_email_to_email_queue -frappe.patches.v7_1.rename_chinese_language_codes - -execute:frappe.db.sql("alter table `tabSessions` modify `user` varchar(255), engine=InnoDB") -execute:frappe.db.sql("delete from `tabDocField` where parent='0'") -frappe.patches.v4_0.change_varchar_length -frappe.patches.v6_4.reduce_varchar_length -frappe.patches.v5_2.change_checks_to_not_null -frappe.patches.v6_9.int_float_not_null #2015-11-25 -frappe.patches.v5_0.v4_to_v5 - -frappe.patches.v5_0.remove_shopping_cart_app -frappe.patches.v4_0.webnotes_to_frappe -execute:frappe.permissions.reset_perms("Module Def") -execute:import frappe.installer;frappe.installer.make_site_dirs() #2014-02-19 -frappe.patches.v4_0.rename_profile_to_user -frappe.patches.v4_0.deprecate_control_panel -frappe.patches.v4_0.remove_old_parent -frappe.patches.v4_0.rename_sitemap_to_route -frappe.patches.v4_0.website_sitemap_hierarchy -frappe.patches.v4_0.remove_index_sitemap -frappe.patches.v4_0.set_website_route_idx -frappe.patches.v4_0.add_delete_permission -frappe.patches.v4_0.set_todo_checked_as_closed -frappe.patches.v4_0.private_backups -frappe.patches.v4_0.set_module_in_report -frappe.patches.v4_0.update_datetime -frappe.patches.v4_0.file_manager_hooks execute:frappe.get_doc("User", "Guest").save() -frappe.patches.v4_0.update_custom_field_insert_after -frappe.patches.v4_0.deprecate_link_selects -frappe.patches.v4_0.set_user_gravatar -frappe.patches.v4_0.set_user_permissions -frappe.patches.v4_0.create_custom_field_for_owner_match -frappe.patches.v4_0.enable_scheduler_in_system_settings -execute:frappe.db.sql("update tabReport set apply_user_permissions=1") #2014-06-03 -frappe.patches.v4_0.replace_deprecated_timezones -execute:import frappe.website.render; frappe.website.render.clear_cache("login"); #2014-06-10 -frappe.patches.v4_0.fix_attach_field_file_url -execute:frappe.permissions.reset_perms("User") #2015-03-24 -execute:frappe.db.sql("""delete from `tabUserRole` where ifnull(parentfield, '')='' or ifnull(`role`, '')=''""") #2014-08-18 -frappe.patches.v4_0.remove_user_owner_custom_field -execute:frappe.delete_doc("DocType", "Website Template") -execute:frappe.db.sql("""update `tabProperty Setter` set property_type='Text' where property in ('options', 'default')""") #2014-06-20 -frappe.patches.v4_1.enable_outgoing_email_settings -execute:frappe.db.sql("""update `tabSingles` set `value`=`doctype` where `field`='name'""") #2014-07-04 -frappe.patches.v4_1.enable_print_as_pdf #2014-06-17 -execute:frappe.db.sql("""update `tabDocPerm` set email=1 where parent='User' and permlevel=0 and `role`='All' and `read`=1 and apply_user_permissions=1""") #2014-07-15 -execute:frappe.db.sql("""update `tabPrint Format` set print_format_type='Client' where ifnull(print_format_type, '')=''""") #2014-07-28 -frappe.patches.v4_1.file_manager_fix -frappe.patches.v4_2.print_with_letterhead execute:frappe.delete_doc("DocType", "Control Panel", force=1) -execute:frappe.reload_doc('website', 'doctype', 'web_form') #2014-09-04 -execute:frappe.reload_doc('website', 'doctype', 'web_form_field') #2014-09-04 -frappe.patches.v4_2.refactor_website_routing -frappe.patches.v4_2.set_assign_in_doc -frappe.patches.v4_3.remove_allow_on_submit_customization -frappe.patches.v5_0.rename_table_fieldnames -frappe.patches.v5_0.communication_parent -frappe.patches.v5_0.clear_website_group_and_notifications -frappe.patches.v5_0.update_shared -execute:frappe.reload_doc("core", "doctype", "docshare") #2015-07-21 -frappe.patches.v6_19.comment_feed_communication -frappe.patches.v6_16.star_to_like -frappe.patches.v5_0.bookmarks_to_stars -frappe.patches.v5_0.style_settings_to_website_theme -frappe.patches.v5_0.rename_ref_type_fieldnames -frappe.patches.v5_0.fix_email_alert -frappe.patches.v5_0.fix_null_date_datetime -frappe.patches.v5_0.force_sync_website execute:frappe.delete_doc("DocType", "Tag") execute:frappe.db.sql("delete from `tabProperty Setter` where `property` in ('idx', '_idx')") -frappe.patches.v5_0.move_scheduler_last_event_to_system_settings execute:frappe.db.sql("update tabUser set new_password='' where ifnull(new_password, '')!=''") -frappe.patches.v5_0.fix_text_editor_file_urls -frappe.patches.v5_0.modify_session -frappe.patches.v5_0.expire_old_scheduler_logs execute:frappe.permissions.reset_perms("DocType") execute:frappe.db.sql("delete from `tabProperty Setter` where `property` = 'idx'") -frappe.patches.v6_0.communication_status_and_permission -frappe.patches.v6_0.make_task_log_folder -frappe.patches.v6_0.document_type_rename -frappe.patches.v6_0.fix_ghana_currency -frappe.patches.v6_2.ignore_user_permissions_if_missing execute:frappe.db.sql("delete from tabSessions where user is null") -frappe.patches.v6_2.rename_backup_manager execute:frappe.delete_doc("DocType", "Backup Manager") -execute:frappe.db.sql("""update `tabCommunication` set parenttype=null, parent=null, parentfield=null""") #2015-10-22 execute:frappe.permissions.reset_perms("Web Page") -frappe.patches.v6_6.user_last_active -frappe.patches.v6_6.fix_file_url -frappe.patches.v6_11.rename_field_in_email_account -frappe.patches.v7_0.create_private_file_folder -frappe.patches.v6_15.remove_property_setter_for_previous_field #2015-12-29 -frappe.patches.v6_15.set_username execute:frappe.permissions.reset_perms("Error Snapshot") -frappe.patches.v6_16.feed_doc_owner -frappe.patches.v6_21.print_settings_repeat_header_footer -frappe.patches.v6_24.set_language_as_code -frappe.patches.v6_20x.update_insert_after -frappe.patches.v6_20x.set_allow_draft_for_print -frappe.patches.v6_20x.remove_roles_from_website_user -frappe.patches.v7_0.set_user_fullname -frappe.patches.v7_0.add_communication_in_doc -frappe.patches.v7_0.update_send_after_in_bulk_email -execute:frappe.db.sql('''delete from `tabSingles` where doctype="Email Settings"''') # 2016-06-13 execute:frappe.db.sql("delete from `tabWeb Page` where ifnull(template_path, '')!=''") -frappe.patches.v7_0.rename_newsletter_list_to_email_group -frappe.patches.v7_0.set_email_group -frappe.patches.v7_1.setup_integration_services #2016-10-27 -frappe.patches.v7_1.rename_chinese_language_codes execute:frappe.core.doctype.language.language.update_language_names() # 2017-04-12 execute:frappe.db.set_value("Print Settings", "Print Settings", "add_draft_heading", 1) -frappe.patches.v7_0.cleanup_list_settings execute:frappe.db.set_default('language', '') -frappe.patches.v7_1.refactor_integration_broker -frappe.patches.v7_1.set_backup_limit -frappe.patches.v7_2.set_doctype_engine -frappe.patches.v7_2.merge_knowledge_base -frappe.patches.v7_0.update_report_builder_json -frappe.patches.v7_2.set_in_standard_filter_property #1 -frappe.patches.v8_0.drop_unwanted_indexes execute:frappe.db.sql("update tabCommunication set communication_date = creation where time(communication_date) = 0") -frappe.patches.v7_2.fix_email_queue_recipient -frappe.patches.v7_2.update_feedback_request # 2017-02-27 execute:frappe.rename_doc('Country', 'Macedonia, Republic of', 'Macedonia', ignore_if_exists=True) execute:frappe.rename_doc('Country', 'Iran, Islamic Republic of', 'Iran', ignore_if_exists=True) execute:frappe.rename_doc('Country', 'Tanzania, United Republic of', 'Tanzania', ignore_if_exists=True) execute:frappe.rename_doc('Country', 'Syrian Arab Republic', 'Syria', ignore_if_exists=True) -frappe.patches.v8_0.rename_listsettings_to_usersettings -frappe.patches.v7_2.update_communications -frappe.patches.v8_0.deprecate_integration_broker -frappe.patches.v8_0.update_gender_and_salutation -frappe.patches.v8_0.setup_email_inbox #2017-03-29 -frappe.patches.v8_0.newsletter_childtable_migrate -frappe.patches.v8_0.set_doctype_values_in_custom_role -frappe.patches.v8_0.install_new_build_system_requirements -frappe.patches.v8_0.set_currency_field_precision # 2017-05-09 execute:frappe.reload_doc('desk', 'doctype', 'notification_log') -frappe.patches.v8_0.rename_print_to_printing -frappe.patches.v7_1.disabled_print_settings_for_custom_print_format execute:frappe.db.sql('update tabReport set module="Desk" where name="ToDo"') -frappe.patches.v8_1.enable_allow_error_traceback_in_system_settings -frappe.patches.v8_1.update_format_options_in_auto_email_report -frappe.patches.v8_1.delete_custom_docperm_if_doctype_not_exists -frappe.patches.v8_5.delete_email_group_member_with_invalid_emails -frappe.patches.v8_x.update_user_permission -frappe.patches.v8_5.patch_event_colors -frappe.patches.v8_10.delete_static_web_page_from_global_search -frappe.patches.v9_1.add_sms_sender_name_as_parameters -frappe.patches.v9_1.resave_domain_settings -frappe.patches.v9_1.revert_domain_settings -frappe.patches.v9_1.move_feed_to_activity_log execute:frappe.delete_doc('Page', 'data-import-tool', ignore_missing=True) frappe.patches.v10_0.reload_countries_and_currencies # 2021-02-03 frappe.patches.v10_0.refactor_social_login_keys diff --git a/frappe/patches/v11_0/create_contact_for_user.py b/frappe/patches/v11_0/create_contact_for_user.py index 21e681a83e..5a483b630e 100644 --- a/frappe/patches/v11_0/create_contact_for_user.py +++ b/frappe/patches/v11_0/create_contact_for_user.py @@ -8,7 +8,6 @@ def execute(): frappe.reload_doc('integrations', 'doctype', 'google_contacts') frappe.reload_doc('contacts', 'doctype', 'contact') frappe.reload_doc('core', 'doctype', 'dynamic_link') - frappe.reload_doc('communication', 'doctype', 'call_log') contact_meta = frappe.get_meta("Contact") if contact_meta.has_field("phone_nos") and contact_meta.has_field("email_ids"): diff --git a/frappe/patches/v11_0/rename_google_maps_doctype.py b/frappe/patches/v11_0/rename_google_maps_doctype.py index 4e8aee6280..8091154b9c 100644 --- a/frappe/patches/v11_0/rename_google_maps_doctype.py +++ b/frappe/patches/v11_0/rename_google_maps_doctype.py @@ -5,4 +5,3 @@ from frappe.model.rename_doc import rename_doc def execute(): if frappe.db.exists("DocType","Google Maps") and not frappe.db.exists("DocType","Google Maps Settings"): rename_doc('DocType', 'Google Maps', 'Google Maps Settings') - frappe.reload_doc('integrations', 'doctype', 'google_maps_settings') \ No newline at end of file diff --git a/frappe/patches/v4_0/add_delete_permission.py b/frappe/patches/v4_0/add_delete_permission.py deleted file mode 100644 index 9e375a431d..0000000000 --- a/frappe/patches/v4_0/add_delete_permission.py +++ /dev/null @@ -1,13 +0,0 @@ - -import frappe - -def execute(): - frappe.reload_doc("core", "doctype", "docperm") - - # delete same as cancel (map old permissions) - frappe.db.sql("""update tabDocPerm set `delete`=ifnull(`cancel`,0)""") - - # can't cancel if can't submit - frappe.db.sql("""update tabDocPerm set `cancel`=0 where ifnull(`submit`,0)=0""") - - frappe.clear_cache() \ No newline at end of file diff --git a/frappe/patches/v4_0/change_varchar_length.py b/frappe/patches/v4_0/change_varchar_length.py deleted file mode 100644 index 914034ccba..0000000000 --- a/frappe/patches/v4_0/change_varchar_length.py +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# MIT License. See license.txt - -import frappe - -def execute(): - frappe.db.sql('update tabDocField set search_index=0 where fieldtype="Small Text"') - frappe.db.sql('update tabDocField set in_list_view=0 where fieldtype="Image"') - - for dt in frappe.db.sql_list("""select name from `tabDocType` where issingle=0"""): - desc = dict((d["Field"], d) for d in frappe.db.sql("desc `tab{}`".format(dt), as_dict=True)) - alter_table = [] - - if desc["name"]["Type"] != "varchar(255)": - alter_table.append("change `name` `name` varchar(255) not null") - - for fieldname in ("modified_by", "owner", "parent", "parentfield", "parenttype"): - if desc[fieldname]["Type"] != "varchar(255)": - alter_table.append("change `{fieldname}` `{fieldname}` varchar(255)".format(fieldname=fieldname)) - - if alter_table: - alter_table_query = "alter table `tab{doctype}` {alter_table}".format(doctype=dt, alter_table=",\n".join(alter_table)) - # print alter_table_query - frappe.db.sql_ddl(alter_table_query) - diff --git a/frappe/patches/v4_0/create_custom_field_for_owner_match.py b/frappe/patches/v4_0/create_custom_field_for_owner_match.py deleted file mode 100644 index 438e280669..0000000000 --- a/frappe/patches/v4_0/create_custom_field_for_owner_match.py +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# MIT License. See license.txt -import frappe -from frappe.custom.doctype.custom_field.custom_field import create_custom_field - -def execute(): - if "match" in frappe.db.get_table_columns("DocPerm"): - create_custom_field_for_owner_match() - -def create_custom_field_for_owner_match(): - docperm_meta = frappe.get_meta('DocPerm') - if docperm_meta.get_field('apply_user_permissions'): - frappe.db.sql("""update `tabDocPerm` set apply_user_permissions=1 where `match`='owner'""") - - for dt in frappe.db.sql_list("""select distinct parent from `tabDocPerm` - where `match`='owner' and permlevel=0 and parent != 'User'"""): - - # a link field pointing to User already exists - if (frappe.db.get_value("DocField", {"parent": dt, "fieldtype": "Link", "options": "User", "default": "__user"}) - or frappe.db.get_value("Custom Field", {"dt": dt, "fieldtype": "Link", "options": "User", "default": "__user"})): - print("User link field already exists for", dt) - continue - - fieldname = "{}_owner".format(frappe.scrub(dt)) - - create_custom_field(dt, frappe._dict({ - "permlevel": 0, - "label": "{} Owner".format(dt), - "fieldname": fieldname, - "fieldtype": "Link", - "options": "User", - "default": "__user" - })) - - frappe.db.sql("""update `tab{doctype}` set `{fieldname}`=owner""".format(doctype=dt, - fieldname=fieldname)) - - # commit is required so that we don't lose these changes because of an error in next loop's ddl - frappe.db.commit() diff --git a/frappe/patches/v4_0/deprecate_control_panel.py b/frappe/patches/v4_0/deprecate_control_panel.py deleted file mode 100644 index 29ec8d35f6..0000000000 --- a/frappe/patches/v4_0/deprecate_control_panel.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# MIT License. See license.txt - -import frappe - -def execute(): - frappe.db.sql("update `tabDefaultValue` set parenttype='__default' where parenttype='Control Panel'") - frappe.db.sql("update `tabDefaultValue` set parent='__default' where parent='Control Panel'") - frappe.clear_cache() diff --git a/frappe/patches/v4_0/deprecate_link_selects.py b/frappe/patches/v4_0/deprecate_link_selects.py deleted file mode 100644 index 837144a6ba..0000000000 --- a/frappe/patches/v4_0/deprecate_link_selects.py +++ /dev/null @@ -1,12 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# MIT License. See license.txt - -import frappe - -def execute(): - for name in frappe.db.sql_list("""select name from `tabCustom Field` - where fieldtype="Select" and options like "link:%" """): - custom_field = frappe.get_doc("Custom Field", name) - custom_field.fieldtype = "Link" - custom_field.options = custom_field.options[5:] - custom_field.save() diff --git a/frappe/patches/v4_0/enable_scheduler_in_system_settings.py b/frappe/patches/v4_0/enable_scheduler_in_system_settings.py deleted file mode 100644 index 68c74edb4f..0000000000 --- a/frappe/patches/v4_0/enable_scheduler_in_system_settings.py +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# MIT License. See license.txt - -import frappe -from frappe.utils.scheduler import disable_scheduler, enable_scheduler -from frappe.utils import cint - -def execute(): - frappe.reload_doc("core", "doctype", "system_settings") - if cint(frappe.db.get_global("disable_scheduler")): - disable_scheduler() - else: - enable_scheduler() diff --git a/frappe/patches/v4_0/file_manager_hooks.py b/frappe/patches/v4_0/file_manager_hooks.py deleted file mode 100644 index ccf95d1d31..0000000000 --- a/frappe/patches/v4_0/file_manager_hooks.py +++ /dev/null @@ -1,33 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# MIT License. See license.txt -import frappe -import os -from frappe.core.doctype.file.file import get_content_hash - - -def execute(): - frappe.reload_doc('core', 'doctype', 'file_data') - for name, file_name, file_url in frappe.db.sql( - """select name, file_name, file_url from `tabFile` - where file_name is not null"""): - b = frappe.get_doc('File', name) - old_file_name = b.file_name - b.file_name = os.path.basename(old_file_name) - if old_file_name.startswith('files/') or old_file_name.startswith('/files/'): - b.file_url = os.path.normpath('/' + old_file_name) - else: - b.file_url = os.path.normpath('/files/' + old_file_name) - try: - _file = frappe.get_doc("File", {"file_name": name}) - content = _file.get_content() - b.content_hash = get_content_hash(content) - except IOError: - print('Warning: Error processing ', name) - _file_name = old_file_name - b.content_hash = None - - try: - b.save() - except frappe.DuplicateEntryError: - frappe.delete_doc(b.doctype, b.name) - diff --git a/frappe/patches/v4_0/fix_attach_field_file_url.py b/frappe/patches/v4_0/fix_attach_field_file_url.py deleted file mode 100644 index 61c35b120d..0000000000 --- a/frappe/patches/v4_0/fix_attach_field_file_url.py +++ /dev/null @@ -1,12 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# MIT License. See license.txt - -import frappe - -def execute(): - attach_fields = (frappe.db.sql("""select parent, fieldname from `tabDocField` where fieldtype in ('Attach', 'Attach Image')""") + - frappe.db.sql("""select dt, fieldname from `tabCustom Field` where fieldtype in ('Attach', 'Attach Image')""")) - - for doctype, fieldname in attach_fields: - frappe.db.sql("""update `tab{doctype}` set `{fieldname}`=concat("/", `{fieldname}`) - where `{fieldname}` like 'files/%'""".format(doctype=doctype, fieldname=fieldname)) diff --git a/frappe/patches/v4_0/private_backups.py b/frappe/patches/v4_0/private_backups.py deleted file mode 100644 index 7920564677..0000000000 --- a/frappe/patches/v4_0/private_backups.py +++ /dev/null @@ -1,10 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# MIT License. See license.txt - -import frappe -from frappe.installer import make_site_dirs - -def execute(): - make_site_dirs() - if frappe.local.conf.backup_path and frappe.local.conf.backup_path.startswith("public"): - raise Exception("Backups path in conf set to public directory") diff --git a/frappe/patches/v4_0/remove_index_sitemap.py b/frappe/patches/v4_0/remove_index_sitemap.py deleted file mode 100644 index 8f48729276..0000000000 --- a/frappe/patches/v4_0/remove_index_sitemap.py +++ /dev/null @@ -1,5 +0,0 @@ - -import frappe - -def execute(): - pass diff --git a/frappe/patches/v4_0/remove_old_parent.py b/frappe/patches/v4_0/remove_old_parent.py deleted file mode 100644 index f2d125953a..0000000000 --- a/frappe/patches/v4_0/remove_old_parent.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# MIT License. See license.txt - -import frappe - -def execute(): - for doctype in frappe.db.sql_list("""select name from `tabDocType` where istable=1"""): - frappe.db.sql("""delete from `tab{0}` where parent like "old_par%:%" """.format(doctype)) - frappe.db.sql("""delete from `tabDocField` where parent="0" """) diff --git a/frappe/patches/v4_0/remove_user_owner_custom_field.py b/frappe/patches/v4_0/remove_user_owner_custom_field.py deleted file mode 100644 index 4620f055d9..0000000000 --- a/frappe/patches/v4_0/remove_user_owner_custom_field.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# MIT License. See license.txt - -import frappe - -def execute(): - user_owner = frappe.db.get_value("Custom Field", {"fieldname": "user_owner"}) - if user_owner: - frappe.delete_doc("Custom Field", user_owner) diff --git a/frappe/patches/v4_0/rename_profile_to_user.py b/frappe/patches/v4_0/rename_profile_to_user.py deleted file mode 100644 index 3e6f269329..0000000000 --- a/frappe/patches/v4_0/rename_profile_to_user.py +++ /dev/null @@ -1,15 +0,0 @@ - -import frappe - -from frappe.model.utils.rename_field import rename_field -from frappe.model.meta import get_table_columns - -def execute(): - tables = frappe.db.sql_list("show tables") - if "tabUser" not in tables: - frappe.rename_doc("DocType", "Profile", "User", force=True) - - frappe.reload_doc("website", "doctype", "blogger") - - if "profile" in get_table_columns("Blogger"): - rename_field("Blogger", "profile", "user") diff --git a/frappe/patches/v4_0/rename_sitemap_to_route.py b/frappe/patches/v4_0/rename_sitemap_to_route.py deleted file mode 100644 index b4606672bc..0000000000 --- a/frappe/patches/v4_0/rename_sitemap_to_route.py +++ /dev/null @@ -1,25 +0,0 @@ - -import frappe - -from frappe.model.utils.rename_field import rename_field - -def execute(): - tables = frappe.db.sql_list("show tables") - for doctype in ("Website Sitemap", "Website Sitemap Config"): - if "tab{}".format(doctype) in tables: - frappe.delete_doc("DocType", doctype, force=1) - frappe.db.sql("drop table `tab{}`".format(doctype)) - - for d in ("Blog Category", "Blog Post", "Web Page"): - frappe.reload_doc("website", "doctype", frappe.scrub(d)) - rename_field_if_exists(d, "parent_website_sitemap", "parent_website_route") - - for d in ("blog_category", "blog_post", "web_page", "post", "user_vote"): - frappe.reload_doc("website", "doctype", d) - -def rename_field_if_exists(doctype, old_fieldname, new_fieldname): - try: - rename_field(doctype, old_fieldname, new_fieldname) - except frappe.db.ProgrammingError as e: - if not frappe.db.is_column_missing(e): - raise diff --git a/frappe/patches/v4_0/replace_deprecated_timezones.py b/frappe/patches/v4_0/replace_deprecated_timezones.py deleted file mode 100644 index a3d8ecbf2b..0000000000 --- a/frappe/patches/v4_0/replace_deprecated_timezones.py +++ /dev/null @@ -1,20 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# MIT License. See license.txt - -import frappe -from frappe.utils.momentjs import data as momentjs_data - -def execute(): - frappe.reload_doc("core", "doctype", "user") - - ss = frappe.get_doc("System Settings", "System Settings") - if ss.time_zone in momentjs_data.get("links"): - ss.time_zone = momentjs_data["links"][ss.time_zone] - ss.flags.ignore_mandatory = True - ss.save() - - for user, time_zone in frappe.db.sql("select name, time_zone from `tabUser` where ifnull(time_zone, '')!=''"): - if time_zone in momentjs_data.get("links"): - user = frappe.get_doc("User", user) - user.time_zone = momentjs_data["links"][user.time_zone] - user.save() diff --git a/frappe/patches/v4_0/set_module_in_report.py b/frappe/patches/v4_0/set_module_in_report.py deleted file mode 100644 index 6c670f4c8e..0000000000 --- a/frappe/patches/v4_0/set_module_in_report.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# MIT License. See license.txt - -import frappe - -def execute(): - frappe.reload_doc("core", "doctype", "report") - frappe.db.sql("""update `tabReport` r set r.module=(select d.module from `tabDocType` d - where d.name=r.ref_doctype) where ifnull(r.module, '')=''""") \ No newline at end of file diff --git a/frappe/patches/v4_0/set_todo_checked_as_closed.py b/frappe/patches/v4_0/set_todo_checked_as_closed.py deleted file mode 100644 index 5f02e1447b..0000000000 --- a/frappe/patches/v4_0/set_todo_checked_as_closed.py +++ /dev/null @@ -1,9 +0,0 @@ - -import frappe - -def execute(): - frappe.reload_doc("core", "doctype", "todo") - try: - frappe.db.sql("""update tabToDo set status = if(ifnull(checked,0)=0, 'Open', 'Closed')""") - except: - pass diff --git a/frappe/patches/v4_0/set_user_gravatar.py b/frappe/patches/v4_0/set_user_gravatar.py deleted file mode 100644 index 348991c320..0000000000 --- a/frappe/patches/v4_0/set_user_gravatar.py +++ /dev/null @@ -1,10 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# MIT License. See license.txt - -import frappe - -def execute(): - for name in frappe.db.sql_list("select name from `tabUser` where ifnull(user_image, '')=''"): - user = frappe.get_doc("User", name) - user.update_gravatar() - user.db_set("user_image", user.user_image) diff --git a/frappe/patches/v4_0/set_user_permissions.py b/frappe/patches/v4_0/set_user_permissions.py deleted file mode 100644 index ef6f3a27e5..0000000000 --- a/frappe/patches/v4_0/set_user_permissions.py +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# MIT License. See license.txt - -import frappe -import frappe.permissions - -def execute(): - frappe.reload_doc("core", "doctype", "docperm") - table_columns = frappe.db.get_table_columns("DocPerm") - - if "restricted" in table_columns: - frappe.db.sql("""update `tabDocPerm` set apply_user_permissions=1 where apply_user_permissions=0 - and restricted=1""") - - if "match" in table_columns: - frappe.db.sql("""update `tabDocPerm` set apply_user_permissions=1 - where apply_user_permissions=0 and ifnull(`match`, '')!=''""") - - # change Restriction to User Permission in tabDefaultValue - frappe.db.sql("""update `tabDefaultValue` set parenttype='User Permission' where parenttype='Restriction'""") - - frappe.clear_cache() - diff --git a/frappe/patches/v4_0/set_website_route_idx.py b/frappe/patches/v4_0/set_website_route_idx.py deleted file mode 100644 index c3dba712d8..0000000000 --- a/frappe/patches/v4_0/set_website_route_idx.py +++ /dev/null @@ -1,25 +0,0 @@ - -import frappe - -def execute(): - pass - # from frappe.website.doctype.website_template.website_template import \ - # get_pages_and_generators, get_template_controller - # - # frappe.reload_doc("website", "doctype", "website_template") - # frappe.reload_doc("website", "doctype", "website_route") - # - # for app in frappe.get_installed_apps(): - # pages, generators = get_pages_and_generators(app) - # for g in generators: - # doctype = frappe.get_attr(get_template_controller(app, g["path"], g["fname"]) + ".doctype") - # module = frappe.db.get_value("DocType", doctype, "module") - # frappe.reload_doc(frappe.scrub(module), "doctype", frappe.scrub(doctype)) - # - # frappe.db.sql("""update `tabBlog Category` set `title`=`name` where ifnull(`title`, '')=''""") - # frappe.db.sql("""update `tabWebsite Route` set idx=null""") - # for doctype in ["Blog Category", "Blog Post", "Web Page", "Website Group"]: - # frappe.db.sql("""update `tab{}` set idx=null""".format(doctype)) - # - # from frappe.website.doctype.website_template.website_template import rebuild_website_template - # rebuild_website_template() diff --git a/frappe/patches/v4_0/update_custom_field_insert_after.py b/frappe/patches/v4_0/update_custom_field_insert_after.py deleted file mode 100644 index 4cb50956d6..0000000000 --- a/frappe/patches/v4_0/update_custom_field_insert_after.py +++ /dev/null @@ -1,17 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# MIT License. See license.txt - -import frappe - -def execute(): - for d in frappe.db.sql("""select name, dt, insert_after from `tabCustom Field` - where docstatus < 2""", as_dict=1): - dt_meta = frappe.get_meta(d.dt) - if not dt_meta.get_field(d.insert_after): - cf = frappe.get_doc("Custom Field", d.name) - df = dt_meta.get("fields", {"label": d.insert_after}) - if df: - cf.insert_after = df[0].fieldname - else: - cf.insert_after = None - cf.save() diff --git a/frappe/patches/v4_0/update_datetime.py b/frappe/patches/v4_0/update_datetime.py deleted file mode 100644 index 4034d8f665..0000000000 --- a/frappe/patches/v4_0/update_datetime.py +++ /dev/null @@ -1,12 +0,0 @@ - -import frappe - -def execute(): - for table in frappe.db.sql_list("show tables"): - for field in frappe.db.sql("desc `%s`" % table): - if field[1]=="datetime": - frappe.db.sql("alter table `%s` change `%s` `%s` datetime(6)" % \ - (table, field[0], field[0])) - elif field[1]=="time": - frappe.db.sql("alter table `%s` change `%s` `%s` time(6)" % \ - (table, field[0], field[0])) diff --git a/frappe/patches/v4_0/webnotes_to_frappe.py b/frappe/patches/v4_0/webnotes_to_frappe.py deleted file mode 100644 index c29f6f603e..0000000000 --- a/frappe/patches/v4_0/webnotes_to_frappe.py +++ /dev/null @@ -1,12 +0,0 @@ - -import frappe, json - -def execute(): - frappe.clear_cache() - installed = frappe.get_installed_apps() - if "webnotes" in installed: - installed.remove("webnotes") - if "frappe" not in installed: - installed = ["frappe"] + installed - frappe.db.set_global("installed_apps", json.dumps(installed)) - frappe.clear_cache() diff --git a/frappe/patches/v4_0/website_sitemap_hierarchy.py b/frappe/patches/v4_0/website_sitemap_hierarchy.py deleted file mode 100644 index 6404986245..0000000000 --- a/frappe/patches/v4_0/website_sitemap_hierarchy.py +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# MIT License. See license.txt - -import frappe - -def execute(): - # frappe.db.sql("""update `tabWebsite Route` ws set ref_doctype=(select wsc.ref_doctype - # from `tabWebsite Template` wsc where wsc.name=ws.website_template) - # where ifnull(page_or_generator, '')!='Page'""") - - frappe.reload_doc("website", "doctype", "website_settings") - - # original_home_page = frappe.db.get_value("Website Settings", "Website Settings", "home_page") - # - # home_page = frappe.db.sql("""select name from `tabWebsite Route` - # where (name=%s or docname=%s) and name!='index'""", (original_home_page, original_home_page)) - # home_page = home_page[0][0] if home_page else original_home_page - # - # frappe.db.set_value("Website Settings", "Website Settings", "home_page", home_page) diff --git a/frappe/patches/v4_1/enable_outgoing_email_settings.py b/frappe/patches/v4_1/enable_outgoing_email_settings.py deleted file mode 100644 index ffa891ae7c..0000000000 --- a/frappe/patches/v4_1/enable_outgoing_email_settings.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# MIT License. See license.txt - -import frappe - -def execute(): - frappe.reload_doc("core", "doctype", "outgoing_email_settings") - if (frappe.db.get_value("Outgoing Email Settings", "Outgoing Email Settings", "mail_server") or "").strip(): - frappe.db.set_value("Outgoing Email Settings", "Outgoing Email Settings", "enabled", 1) diff --git a/frappe/patches/v4_1/enable_print_as_pdf.py b/frappe/patches/v4_1/enable_print_as_pdf.py deleted file mode 100644 index e5a8f830f6..0000000000 --- a/frappe/patches/v4_1/enable_print_as_pdf.py +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# MIT License. See license.txt - -import frappe - -def execute(): - frappe.reload_doc("core", "doctype", "print_settings") - print_settings = frappe.get_doc("Print Settings") - print_settings.print_style = "Modern" - - try: - import pdfkit - except ImportError: - pass - else: - # if someone has already configured in Outgoing Email Settings - outgoing_email_settings = frappe.db.get_singles_dict("Outgoing Email Settings") - if "send_print_as_pdf" in outgoing_email_settings: - print_settings.send_print_as_pdf = outgoing_email_settings.send_print_as_pdf - print_settings.pdf_page_size = outgoing_email_settings.pdf_page_size - - else: - print_settings.send_print_as_pdf = 1 - - print_settings.save() diff --git a/frappe/patches/v4_1/file_manager_fix.py b/frappe/patches/v4_1/file_manager_fix.py deleted file mode 100644 index 18f44203f2..0000000000 --- a/frappe/patches/v4_1/file_manager_fix.py +++ /dev/null @@ -1,97 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# MIT License. See license.txt -import frappe -import os -from frappe.core.doctype.file.file import get_content_hash, get_file_name -from frappe.utils import get_files_path, get_site_path - -# The files missed by the previous patch might have been replaced with new files -# with the same filename -# -# This patch does the following, -# * Detect which files were replaced and rename them with name{hash:5}.extn and -# update filedata record for the new file -# -# * make missing_files.txt in site dir with files that should be recovered from -# a backup from a time before version 3 migration -# -# * Patch remaining unpatched File records. - - -def execute(): - frappe.db.auto_commit_on_many_writes = True - rename_replacing_files() - for name, file_name, file_url in frappe.db.sql( - """select name, file_name, file_url from `tabFile` - where ifnull(file_name, '')!='' and ifnull(content_hash, '')=''"""): - b = frappe.get_doc('File', name) - old_file_name = b.file_name - b.file_name = os.path.basename(old_file_name) - if old_file_name.startswith('files/') or old_file_name.startswith('/files/'): - b.file_url = os.path.normpath('/' + old_file_name) - else: - b.file_url = os.path.normpath('/files/' + old_file_name) - try: - _file = frappe.get_doc("File", {"file_name": name}) - content = _file.get_content() - b.content_hash = get_content_hash(content) - except IOError: - print('Warning: Error processing ', name) - b.content_hash = None - b.flags.ignore_duplicate_entry_error = True - b.save() - frappe.db.auto_commit_on_many_writes = False - -def get_replaced_files(): - ret = [] - new_files = dict(frappe.db.sql("select name, file_name from `tabFile` where file_name not like 'files/%'")) - old_files = dict(frappe.db.sql("select name, file_name from `tabFile` where ifnull(content_hash, '')=''")) - invfiles = invert_dict(new_files) - - for nname, nfilename in new_files.items(): - if 'files/' + nfilename in old_files.values(): - ret.append((nfilename, invfiles[nfilename])) - return ret - -def rename_replacing_files(): - replaced_files = get_replaced_files() - if len(replaced_files): - missing_files = [v[0] for v in replaced_files] - with open(get_site_path('missing_files.txt'), 'w') as f: - f.write(('\n'.join(missing_files) + '\n').encode('utf-8')) - - for file_name, file_datas in replaced_files: - print ('processing ' + file_name) - content_hash = frappe.db.get_value('File', file_datas[0], 'content_hash') - if not content_hash: - continue - new_file_name = get_file_name(file_name, content_hash) - if os.path.exists(get_files_path(new_file_name)): - continue - print('skipping ' + file_name) - try: - os.rename(get_files_path(file_name), get_files_path(new_file_name)) - except OSError: - print('Error renaming ', file_name) - for name in file_datas: - f = frappe.get_doc('File', name) - f.file_name = new_file_name - f.file_url = '/files/' + new_file_name - f.save() - -def invert_dict(ddict): - ret = {} - for k,v in ddict.items(): - if not ret.get(v): - ret[v] = [k] - else: - ret[v].append(k) - return ret - -def get_file_name(fname, hash): - if '.' in fname: - partial, extn = fname.rsplit('.', 1) - else: - partial = fname - extn = '' - return '{partial}{suffix}.{extn}'.format(partial=partial, extn=extn, suffix=hash[:5]) diff --git a/frappe/patches/v4_2/print_with_letterhead.py b/frappe/patches/v4_2/print_with_letterhead.py deleted file mode 100644 index 111f6c762e..0000000000 --- a/frappe/patches/v4_2/print_with_letterhead.py +++ /dev/null @@ -1,10 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# MIT License. See license.txt - -import frappe - -def execute(): - frappe.reload_doc("core", "doctype", "print_settings") - print_settings = frappe.get_doc("Print Settings") - print_settings.with_letterhead = 1 - print_settings.save() diff --git a/frappe/patches/v4_2/refactor_website_routing.py b/frappe/patches/v4_2/refactor_website_routing.py deleted file mode 100644 index 77eea3d429..0000000000 --- a/frappe/patches/v4_2/refactor_website_routing.py +++ /dev/null @@ -1,8 +0,0 @@ - -import frappe - -def execute(): - # clear all static web pages - frappe.delete_doc("DocType", "Website Route", force=1) - frappe.delete_doc("Page", "sitemap-browser", force=1) - frappe.db.sql("drop table if exists `tabWebsite Route`") diff --git a/frappe/patches/v4_2/set_assign_in_doc.py b/frappe/patches/v4_2/set_assign_in_doc.py deleted file mode 100644 index 8fbd37c5c5..0000000000 --- a/frappe/patches/v4_2/set_assign_in_doc.py +++ /dev/null @@ -1,11 +0,0 @@ - -import frappe - -def execute(): - for name in frappe.db.sql_list("""select name from `tabToDo` - where ifnull(reference_type, '')!='' and ifnull(reference_name, '')!=''"""): - try: - frappe.get_doc("ToDo", name).on_update() - except Exception as e: - if not frappe.db.is_table_missing(e): - raise diff --git a/frappe/patches/v4_3/remove_allow_on_submit_customization.py b/frappe/patches/v4_3/remove_allow_on_submit_customization.py deleted file mode 100644 index a762fd10ab..0000000000 --- a/frappe/patches/v4_3/remove_allow_on_submit_customization.py +++ /dev/null @@ -1,10 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# MIT License. See license.txt - -import frappe - -def execute(): - for d in frappe.get_all("Property Setter", fields=["name", "doc_type"], - filters={"doctype_or_field": "DocField", "property": "allow_on_submit", "value": "1"}): - frappe.delete_doc("Property Setter", d.name) - frappe.clear_cache(doctype=d.doc_type) diff --git a/frappe/patches/v5_0/bookmarks_to_stars.py b/frappe/patches/v5_0/bookmarks_to_stars.py deleted file mode 100644 index 0d2c13525e..0000000000 --- a/frappe/patches/v5_0/bookmarks_to_stars.py +++ /dev/null @@ -1,32 +0,0 @@ - -import json -import frappe -import frappe.defaults -from frappe.desk.like import _toggle_like - -def execute(): - for user in frappe.get_all("User"): - username = user["name"] - bookmarks = frappe.db.get_default("_bookmarks", username) - - if not bookmarks: - continue - - if isinstance(bookmarks, str): - bookmarks = json.loads(bookmarks) - - for opts in bookmarks: - route = (opts.get("route") or "").strip("#/ ") - - if route and route.startswith("Form"): - try: - view, doctype, docname = opts["route"].split("/") - except ValueError: - continue - - if frappe.db.exists(doctype, docname): - if (doctype=="DocType" - or int(frappe.db.get_value("DocType", doctype, "issingle") or 0) - or not frappe.db.table_exists(doctype)): - continue - _toggle_like(doctype, docname, add="Yes", user=username) diff --git a/frappe/patches/v5_0/clear_website_group_and_notifications.py b/frappe/patches/v5_0/clear_website_group_and_notifications.py deleted file mode 100644 index 3d3d0c0d16..0000000000 --- a/frappe/patches/v5_0/clear_website_group_and_notifications.py +++ /dev/null @@ -1,9 +0,0 @@ - -import frappe - -def execute(): - frappe.delete_doc("DocType", "Post") - frappe.delete_doc("DocType", "Website Group") - frappe.delete_doc("DocType", "Website Route Permission") - frappe.delete_doc("DocType", "User Vote") - frappe.delete_doc("DocType", "Notification Count") diff --git a/frappe/patches/v5_0/communication_parent.py b/frappe/patches/v5_0/communication_parent.py deleted file mode 100644 index 3c73d91972..0000000000 --- a/frappe/patches/v5_0/communication_parent.py +++ /dev/null @@ -1,6 +0,0 @@ - -import frappe - -def execute(): - frappe.reload_doc("core", "doctype", "communication") - frappe.db.sql("""update tabCommunication set reference_doctype = parenttype, reference_name = parent""") diff --git a/frappe/patches/v5_0/convert_to_barracuda_and_utf8mb4.py b/frappe/patches/v5_0/convert_to_barracuda_and_utf8mb4.py deleted file mode 100644 index 6fa6434f98..0000000000 --- a/frappe/patches/v5_0/convert_to_barracuda_and_utf8mb4.py +++ /dev/null @@ -1,16 +0,0 @@ - -import frappe -from frappe.database.mariadb.setup_db import check_database_settings -from frappe.model.meta import trim_tables - -def execute(): - check_database_settings() - - for table in frappe.db.get_tables(): - frappe.db.sql_ddl("""alter table `{0}` ENGINE=InnoDB ROW_FORMAT=COMPRESSED""".format(table)) - try: - frappe.db.sql_ddl("""alter table `{0}` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci""".format(table)) - except: - # if row size gets too large, let it be old charset! - pass - diff --git a/frappe/patches/v5_0/expire_old_scheduler_logs.py b/frappe/patches/v5_0/expire_old_scheduler_logs.py deleted file mode 100644 index 0262acd346..0000000000 --- a/frappe/patches/v5_0/expire_old_scheduler_logs.py +++ /dev/null @@ -1,8 +0,0 @@ - -import frappe - -def execute(): - frappe.reload_doctype("Error Log") - - from frappe.core.doctype.error_log.error_log import set_old_logs_as_seen - set_old_logs_as_seen() diff --git a/frappe/patches/v5_0/fix_email_alert.py b/frappe/patches/v5_0/fix_email_alert.py deleted file mode 100644 index e7366e8b66..0000000000 --- a/frappe/patches/v5_0/fix_email_alert.py +++ /dev/null @@ -1,14 +0,0 @@ -import frappe - -def execute(): - frappe.reload_doctype("Notification") - for e in frappe.get_all("Notification"): - notification = frappe.get_doc("Notification", e.name) - if notification.event == "Date Change": - if notification.days_in_advance < 0: - notification.event = "Days After" - notification.days_in_advance = -email_alert.days_in_advance - else: - notification.event = "Days Before" - - notification.save() diff --git a/frappe/patches/v5_0/fix_null_date_datetime.py b/frappe/patches/v5_0/fix_null_date_datetime.py deleted file mode 100644 index 078cba079a..0000000000 --- a/frappe/patches/v5_0/fix_null_date_datetime.py +++ /dev/null @@ -1,20 +0,0 @@ - -import frappe - -def execute(): - for table in frappe.db.get_tables(): - changed = False - desc = frappe.db.sql("desc `{table}`".format(table=table), as_dict=True) - for field in desc: - if field["Type"] == "date": - frappe.db.sql("""update `{table}` set `{fieldname}`=null where `{fieldname}`='0000-00-00'""".format( - table=table, fieldname=field["Field"])) - changed = True - - elif field["Type"] == "datetime(6)": - frappe.db.sql("""update `{table}` set `{fieldname}`=null where `{fieldname}`='0000-00-00 00:00:00.000000'""".format( - table=table, fieldname=field["Field"])) - changed = True - - if changed: - frappe.db.commit() diff --git a/frappe/patches/v5_0/fix_text_editor_file_urls.py b/frappe/patches/v5_0/fix_text_editor_file_urls.py deleted file mode 100644 index 43f0c9d8a5..0000000000 --- a/frappe/patches/v5_0/fix_text_editor_file_urls.py +++ /dev/null @@ -1,43 +0,0 @@ -import frappe -import re - -def execute(): - """Fix relative urls for image src="files/" to src="/files/" in DocTypes with text editor fields""" - doctypes_with_text_fields = frappe.get_all("DocField", fields=["parent", "fieldname"], - filters={"fieldtype": "Text Editor"}) - - done = [] - for opts in doctypes_with_text_fields: - if opts in done: - continue - - try: - result = frappe.get_all(opts.parent, fields=["name", opts.fieldname]) - except frappe.db.SQLError: - # bypass single tables - continue - - for data in result: - old_value = data[opts.fieldname] - if not old_value: - continue - - html = scrub_relative_urls(old_value) - if html != old_value: - # print_diff(html, old_value) - frappe.db.set_value(opts.parent, data.name, opts.fieldname, html, update_modified=False) - - done.append(opts) - -def scrub_relative_urls(html): - """prepend a slash before a relative url""" - try: - return re.sub(r'src[\s]*=[\s]*[\'"]files/([^\'"]*)[\'"]', r'src="/files/\g<1>"', html) - except: - print("Error", html) - raise - -def print_diff(html, old_value): - import difflib - diff = difflib.unified_diff(old_value.splitlines(1), html.splitlines(1), lineterm='') - print('\n'.join(list(diff))) diff --git a/frappe/patches/v5_0/force_sync_website.py b/frappe/patches/v5_0/force_sync_website.py deleted file mode 100644 index 8f48729276..0000000000 --- a/frappe/patches/v5_0/force_sync_website.py +++ /dev/null @@ -1,5 +0,0 @@ - -import frappe - -def execute(): - pass diff --git a/frappe/patches/v5_0/modify_session.py b/frappe/patches/v5_0/modify_session.py deleted file mode 100644 index 1c2ff0d6e6..0000000000 --- a/frappe/patches/v5_0/modify_session.py +++ /dev/null @@ -1,6 +0,0 @@ - -import frappe - -def execute(): - if "device" not in frappe.db.get_table_columns("Sessions"): - frappe.db.sql("alter table tabSessions add column `device` varchar(255) default 'desktop'") diff --git a/frappe/patches/v5_0/move_scheduler_last_event_to_system_settings.py b/frappe/patches/v5_0/move_scheduler_last_event_to_system_settings.py deleted file mode 100644 index bdc52e6152..0000000000 --- a/frappe/patches/v5_0/move_scheduler_last_event_to_system_settings.py +++ /dev/null @@ -1,8 +0,0 @@ - -import frappe - -def execute(): - frappe.reload_doctype('System Settings') - last = frappe.db.get_global('scheduler_last_event') - frappe.db.set_value('System Settings', 'System Settings', 'scheduler_last_event', last) - diff --git a/frappe/patches/v5_0/remove_shopping_cart_app.py b/frappe/patches/v5_0/remove_shopping_cart_app.py deleted file mode 100644 index ed9414159e..0000000000 --- a/frappe/patches/v5_0/remove_shopping_cart_app.py +++ /dev/null @@ -1,5 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# License: GNU General Public License v3. See license.txt -def execute(): - from frappe.installer import remove_from_installed_apps - remove_from_installed_apps("shopping_cart") diff --git a/frappe/patches/v5_0/rename_ref_type_fieldnames.py b/frappe/patches/v5_0/rename_ref_type_fieldnames.py deleted file mode 100644 index 01e36af8a9..0000000000 --- a/frappe/patches/v5_0/rename_ref_type_fieldnames.py +++ /dev/null @@ -1,18 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt - -import frappe - -def execute(): - try: - frappe.db.sql("alter table `tabEmail Queue` change `ref_docname` `reference_name` varchar(255)") - except Exception as e: - if not frappe.db.is_table_or_column_missing(e): - raise - - try: - frappe.db.sql("alter table `tabEmail Queue` change `ref_doctype` `reference_doctype` varchar(255)") - except Exception as e: - if not frappe.db.is_table_or_column_missing(e): - raise - frappe.reload_doctype("Email Queue") diff --git a/frappe/patches/v5_0/rename_table_fieldnames.py b/frappe/patches/v5_0/rename_table_fieldnames.py deleted file mode 100644 index 79703bbba2..0000000000 --- a/frappe/patches/v5_0/rename_table_fieldnames.py +++ /dev/null @@ -1,29 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# License: GNU General Public License v3. See license.txt - -import frappe -from frappe.model.utils.rename_field import rename_field -from frappe.modules import scrub, get_doctype_module - -rename_map = { - "Customize Form": [ - ["customize_form_fields", "fields"] - ], - "Email Alert": [ - ["email_alert_recipients", "recipients"] - ], - "Workflow": [ - ["workflow_document_states", "states"], - ["workflow_transitions", "transitions"] - ] -} - -def execute(): - frappe.reload_doc("custom", "doctype", "customize_form") - frappe.reload_doc("email", "doctype", "notification") - frappe.reload_doc("desk", "doctype", "event") - frappe.reload_doc("workflow", "doctype", "workflow") - - for dt, field_list in rename_map.items(): - for field in field_list: - rename_field(dt, field[0], field[1]) diff --git a/frappe/patches/v5_0/style_settings_to_website_theme.py b/frappe/patches/v5_0/style_settings_to_website_theme.py deleted file mode 100644 index 73ee28c1fc..0000000000 --- a/frappe/patches/v5_0/style_settings_to_website_theme.py +++ /dev/null @@ -1,59 +0,0 @@ - -import frappe -from frappe import _ -from frappe.utils import cint - -def execute(): - frappe.reload_doc("website", "doctype", "website_theme") - frappe.reload_doc("website", "website_theme", "standard") - frappe.reload_doctype("Website Settings") - migrate_style_settings() - frappe.delete_doc("website", "doctype", "style_settings") - -def migrate_style_settings(): - style_settings = frappe.db.get_singles_dict("Style Settings") - standard_website_theme = frappe.get_doc("Website Theme", "Standard") - - website_theme = frappe.copy_doc(standard_website_theme) - website_theme.custom = 1 - website_theme.theme = _("Custom") - - if style_settings: - map_color_fields(style_settings, website_theme) - map_other_fields(style_settings, website_theme) - - website_theme.no_sidebar = cint(frappe.db.get_single_value("Website Settings", "no_sidebar")) - - website_theme.save() - website_theme.set_as_default() - -def map_color_fields(style_settings, website_theme): - color_fields_map = { - "page_text": "text_color", - "page_links": "link_color", - "top_bar_background": "top_bar_color", - "top_bar_foreground": "top_bar_text_color", - "footer_background": "footer_color", - "footer_color": "footer_text_color", - } - - for from_fieldname, to_fieldname in color_fields_map.items(): - from_value = style_settings.get(from_fieldname) - - if from_value: - website_theme.set(to_fieldname, "#{0}".format(from_value)) - -def map_other_fields(style_settings, website_theme): - other_fields_map = { - "heading_text_as": "heading_style", - "google_web_font_for_heading": "heading_webfont", - "google_web_font_for_text": "text_webfont", - "add_css": "css" - } - - for from_fieldname, to_fieldname in other_fields_map.items(): - website_theme.set(to_fieldname, style_settings.get(from_fieldname)) - - for fieldname in ("apply_style", "background_image", "background_color", - "font_size"): - website_theme.set(fieldname, style_settings.get(fieldname)) diff --git a/frappe/patches/v5_0/update_shared.py b/frappe/patches/v5_0/update_shared.py deleted file mode 100644 index e549d7271d..0000000000 --- a/frappe/patches/v5_0/update_shared.py +++ /dev/null @@ -1,37 +0,0 @@ - -import frappe -import frappe.share - -def execute(): - frappe.reload_doc("core", "doctype", "docperm") - frappe.reload_doc("core", "doctype", "docshare") - frappe.reload_doc('email', 'doctype', 'email_account') - - # default share to all writes - frappe.db.sql("""update tabDocPerm set `share`=1 where ifnull(`write`,0)=1 and ifnull(`permlevel`,0)=0""") - - # every user must have access to his / her own detail - users = frappe.get_all("User", filters={"user_type": "System User"}) - usernames = [user.name for user in users] - for user in usernames: - frappe.share.add("User", user, user, write=1, share=1) - - # move event user to shared - if frappe.db.exists("DocType", "Event User"): - for event in frappe.get_all("Event User", fields=["parent", "person"]): - if event.person in usernames: - if not frappe.db.exists("Event", event.parent): - frappe.db.sql("delete from `tabEvent User` where parent = %s",event.parent) - else: - frappe.share.add("Event", event.parent, event.person, write=1) - - frappe.delete_doc("DocType", "Event User") - - # move note user to shared - if frappe.db.exists("DocType", "Note User"): - for note in frappe.get_all("Note User", fields=["parent", "user", "permission"]): - perm = {"read": 1} if note.permission=="Read" else {"write": 1} - if note.user in usernames: - frappe.share.add("Note", note.parent, note.user, **perm) - - frappe.delete_doc("DocType", "Note User") diff --git a/frappe/patches/v5_0/v4_to_v5.py b/frappe/patches/v5_0/v4_to_v5.py deleted file mode 100644 index 479acc6d63..0000000000 --- a/frappe/patches/v5_0/v4_to_v5.py +++ /dev/null @@ -1,17 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# MIT License. See license.txt - -import frappe - -def execute(): - changed = ( - ("desk", ("feed", "event", "todo", "note")), - ("custom", ("custom_field", "custom_script", "customize_form", - "customize_form_field", "property_setter")), - ("email", ("email_queue", "notification", "notification_recipient", "standard_reply")), - ("geo", ("country", "currency")), - ("print", ("letter_head", "print_format", "print_settings")) - ) - for module in changed: - for doctype in module[1]: - frappe.reload_doc(module[0], "doctype", doctype) diff --git a/frappe/patches/v5_2/change_checks_to_not_null.py b/frappe/patches/v5_2/change_checks_to_not_null.py deleted file mode 100644 index 32be3aa752..0000000000 --- a/frappe/patches/v5_2/change_checks_to_not_null.py +++ /dev/null @@ -1,34 +0,0 @@ - -import frappe -from frappe.utils import cint -from frappe.model import default_fields - -def execute(): - for table in frappe.db.get_tables(): - doctype = table[3:] - if frappe.db.exists("DocType", doctype): - fieldnames = [df["fieldname"] for df in - frappe.get_all("DocField", fields=["fieldname"], filters={"parent": doctype})] - custom_fieldnames = [df["fieldname"] for df in - frappe.get_all("Custom Field", fields=["fieldname"], filters={"dt": doctype})] - - else: - fieldnames = custom_fieldnames = [] - - for column in frappe.db.sql("""desc `{0}`""".format(table), as_dict=True): - if column["Type"]=="int(1)": - fieldname = column["Field"] - - # only change for defined fields, ignore old fields that don't exist in meta - if not (fieldname in default_fields or fieldname in fieldnames or fieldname in custom_fieldnames): - continue - - # set 0 - frappe.db.sql("""update `{table}` set `{column}`=0 where `{column}` is null"""\ - .format(table=table, column=fieldname)) - frappe.db.commit() - - # change definition - frappe.db.sql_ddl("""alter table `{table}` - modify `{column}` int(1) not null default {default}"""\ - .format(table=table, column=fieldname, default=cint(column["Default"]))) diff --git a/frappe/patches/v5_3/__init__.py b/frappe/patches/v5_3/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/frappe/patches/v5_3/rename_chinese_languages.py b/frappe/patches/v5_3/rename_chinese_languages.py deleted file mode 100644 index f720fb7538..0000000000 --- a/frappe/patches/v5_3/rename_chinese_languages.py +++ /dev/null @@ -1,12 +0,0 @@ -# -*- coding: utf-8 -*- -import frappe -from frappe.translate import rename_language - -def execute(): - language_map = { - "中国(简体)": "簡體中文", - "中國(繁體)": "正體中文" - } - - for old_name, new_name in language_map.items(): - rename_language(old_name, new_name) diff --git a/frappe/patches/v6_0/__init__.py b/frappe/patches/v6_0/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/frappe/patches/v6_0/communication_status_and_permission.py b/frappe/patches/v6_0/communication_status_and_permission.py deleted file mode 100644 index 435dcc21a5..0000000000 --- a/frappe/patches/v6_0/communication_status_and_permission.py +++ /dev/null @@ -1,19 +0,0 @@ - -import frappe -from frappe.permissions import reset_perms - -def execute(): - frappe.reload_doctype("Communication") - - # set status = "Linked" - frappe.db.sql("""update `tabCommunication` set status='Linked' - where ifnull(reference_doctype, '')!='' and ifnull(reference_name, '')!=''""") - - frappe.db.sql("""update `tabCommunication` set status='Closed' - where status='Archived'""") - - # reset permissions if owner of all DocPerms is Administrator - if not frappe.db.sql("""select name from `tabDocPerm` - where parent='Communication' and ifnull(owner, '')!='Administrator'"""): - - reset_perms("Communication") diff --git a/frappe/patches/v6_0/document_type_rename.py b/frappe/patches/v6_0/document_type_rename.py deleted file mode 100644 index 53eec5d85c..0000000000 --- a/frappe/patches/v6_0/document_type_rename.py +++ /dev/null @@ -1,8 +0,0 @@ - -import frappe - -def execute(): - frappe.db.sql("""update tabDocType set document_type='Document' - where document_type='Transaction'""") - frappe.db.sql("""update tabDocType set document_type='Setup' - where document_type='Master'""") diff --git a/frappe/patches/v6_0/fix_ghana_currency.py b/frappe/patches/v6_0/fix_ghana_currency.py deleted file mode 100644 index 50feb3ca3f..0000000000 --- a/frappe/patches/v6_0/fix_ghana_currency.py +++ /dev/null @@ -1,6 +0,0 @@ -def execute(): - from frappe.geo.country_info import get_all - import frappe.utils.install - - countries = get_all() - frappe.utils.install.add_country_and_currency("Ghana", frappe._dict(countries["Ghana"])) diff --git a/frappe/patches/v6_0/make_task_log_folder.py b/frappe/patches/v6_0/make_task_log_folder.py deleted file mode 100644 index b5ed547d71..0000000000 --- a/frappe/patches/v6_0/make_task_log_folder.py +++ /dev/null @@ -1,7 +0,0 @@ - -import frappe.utils, os - -def execute(): - path = frappe.utils.get_site_path('task-logs') - if not os.path.exists(path): - os.makedirs(path) diff --git a/frappe/patches/v6_1/__init__.py b/frappe/patches/v6_1/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/frappe/patches/v6_1/rename_file_data.py b/frappe/patches/v6_1/rename_file_data.py deleted file mode 100644 index 3c62217e8d..0000000000 --- a/frappe/patches/v6_1/rename_file_data.py +++ /dev/null @@ -1,37 +0,0 @@ -import frappe - -def execute(): - from frappe.core.doctype.file.file import make_home_folder - - if not frappe.db.exists("DocType", "File"): - frappe.rename_doc("DocType", "File Data", "File") - frappe.reload_doctype("File") - - if not frappe.db.exists("File", {"is_home_folder": 1}): - make_home_folder() - - # make missing folders and set parent folder - for file in frappe.get_all("File", filters={"is_folder": 0}): - file = frappe.get_doc("File", file.name) - file.flags.ignore_folder_validate = True - file.flags.ignore_file_validate = True - file.flags.ignore_duplicate_entry_error = True - file.flags.ignore_links = True - file.set_folder_name() - try: - file.save() - except: - print(frappe.get_traceback()) - raise - - from frappe.utils.nestedset import rebuild_tree - rebuild_tree("File", "folder") - - # reset file size - for folder in frappe.db.sql("""select name from tabFile f1 where is_folder = 1 and - (select count(*) from tabFile f2 where f2.folder = f1.name and f2.is_folder = 1) = 0"""): - folder = frappe.get_doc("File", folder[0]) - folder.save() - - - diff --git a/frappe/patches/v6_11/__init__.py b/frappe/patches/v6_11/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/frappe/patches/v6_11/rename_field_in_email_account.py b/frappe/patches/v6_11/rename_field_in_email_account.py deleted file mode 100644 index 8e600cc2b9..0000000000 --- a/frappe/patches/v6_11/rename_field_in_email_account.py +++ /dev/null @@ -1,7 +0,0 @@ - -import frappe - -def execute(): - frappe.reload_doc("email", "doctype", "email_account") - if frappe.db.has_column('Email Account', 'pop3_server'): - frappe.db.sql("update `tabEmail Account` set email_server = pop3_server") diff --git a/frappe/patches/v6_15/__init__.py b/frappe/patches/v6_15/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/frappe/patches/v6_15/remove_property_setter_for_previous_field.py b/frappe/patches/v6_15/remove_property_setter_for_previous_field.py deleted file mode 100644 index 9f0cd69489..0000000000 --- a/frappe/patches/v6_15/remove_property_setter_for_previous_field.py +++ /dev/null @@ -1,86 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# MIT License. See license.txt - -import frappe, json -from frappe.utils import cstr - -def execute(): - # deprecated on 2016-03-09 - # using insert_after instead - return - - frappe.db.sql("""delete from `tabProperty Setter` where property='previous_field'""") - - all_custom_fields = frappe._dict() - for d in frappe.db.sql("""select name, dt, fieldname, insert_after from `tabCustom Field` - where insert_after is not null and insert_after != ''""", as_dict=1): - all_custom_fields.setdefault(d.dt, frappe._dict()).setdefault(d.fieldname, d.insert_after) - - for dt, custom_fields in all_custom_fields.items(): - _idx = [] - existing_ps = frappe.db.get_value("Property Setter", - {"doc_type": dt, "property": "_idx"}, ["name", "value", "creation"], as_dict=1) - - # if no existsing property setter, build based on meta - if not existing_ps: - _idx = get_sorted_fields(dt, custom_fields) - else: - _idx = json.loads(existing_ps.value) - - idx_needs_to_be_fixed = False - for fieldname, insert_after in custom_fields.items(): - # Delete existing property setter if field is not there - if fieldname not in _idx: - idx_needs_to_be_fixed = True - break - else: - previous_field = _idx[_idx.index(fieldname) - 1] - - if previous_field != insert_after and cstr(existing_ps.creation) >= "2015-12-28": - idx_needs_to_be_fixed = True - break - - if idx_needs_to_be_fixed: - frappe.delete_doc("Property Setter", existing_ps.name) - _idx = get_sorted_fields(dt, custom_fields) - - if _idx: - frappe.make_property_setter({ - "doctype":dt, - "doctype_or_field": "DocType", - "property": "_idx", - "value": json.dumps(_idx), - "property_type": "Text" - }, validate_fields_for_doctype=False) - - -def get_sorted_fields(doctype, custom_fields): - """sort on basis of insert_after""" - fields_dict = frappe.get_meta(doctype).get("fields") - - standard_fields_count = frappe.db.sql("""select count(name) from `tabDocField` - where parent=%s""", doctype)[0][0] - - newlist = [] - pending = [d.fieldname for d in fields_dict] - - maxloops = len(custom_fields) + 20 - while (pending and maxloops>0): - maxloops -= 1 - for fieldname in pending[:]: - if fieldname in custom_fields and len(newlist) >= standard_fields_count: - # field already added - for n in newlist: - if n==custom_fields.get(fieldname): - newlist.insert(newlist.index(n)+1, fieldname) - pending.remove(fieldname) - break - else: - newlist.append(fieldname) - pending.remove(fieldname) - - # recurring at end - if pending: - newlist += pending - - return newlist diff --git a/frappe/patches/v6_15/set_username.py b/frappe/patches/v6_15/set_username.py deleted file mode 100644 index ebf01763d0..0000000000 --- a/frappe/patches/v6_15/set_username.py +++ /dev/null @@ -1,16 +0,0 @@ - -import frappe - -def execute(): - frappe.reload_doctype("User") - - # give preference to System Users - users = frappe.db.sql_list("""select name from `tabUser` order by if(user_type='System User', 0, 1)""") - for name in users: - user = frappe.get_doc("User", name) - if user.username or not user.first_name: - continue - - username = user.suggest_username() - if username: - user.db_set("username", username, update_modified=False) diff --git a/frappe/patches/v6_16/__init__.py b/frappe/patches/v6_16/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/frappe/patches/v6_16/feed_doc_owner.py b/frappe/patches/v6_16/feed_doc_owner.py deleted file mode 100644 index b7e738b6d9..0000000000 --- a/frappe/patches/v6_16/feed_doc_owner.py +++ /dev/null @@ -1,31 +0,0 @@ - -import frappe - -def execute(): - frappe.reload_doctype("Communication") - - for doctype, name in frappe.db.sql("""select distinct reference_doctype, reference_name - from `tabCommunication` - where - (reference_doctype is not null and reference_doctype != '') - and (reference_name is not null and reference_name != '') - and (reference_owner is null or reference_owner = '') - for update"""): - - owner = frappe.db.get_value(doctype, name, "owner") - - if not owner: - continue - - frappe.db.sql("""update `tabCommunication` - set reference_owner=%(owner)s - where - reference_doctype=%(doctype)s - and reference_name=%(name)s - and (reference_owner is null or reference_owner = '')""".format(doctype=doctype), { - "doctype": doctype, - "name": name, - "owner": owner - }) - - frappe.db.commit() diff --git a/frappe/patches/v6_16/star_to_like.py b/frappe/patches/v6_16/star_to_like.py deleted file mode 100644 index f3fc6310d9..0000000000 --- a/frappe/patches/v6_16/star_to_like.py +++ /dev/null @@ -1,15 +0,0 @@ - -import frappe -from frappe.database.schema import add_column - -def execute(): - frappe.db.sql("""update `tabSingles` set field='_liked_by' where field='_starred_by'""") - frappe.db.commit() - - for table in frappe.db.get_tables(): - columns = [r[0] for r in frappe.db.sql("DESC `{0}`".format(table))] - if "_starred_by" in columns and '_liked_by' not in columns: - frappe.db.sql_ddl("""alter table `{0}` change `_starred_by` `_liked_by` Text """.format(table)) - - if not frappe.db.has_column("Communication", "_liked_by"): - add_column("Communication", "_liked_by", "Text") diff --git a/frappe/patches/v6_19/__init__.py b/frappe/patches/v6_19/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/frappe/patches/v6_19/comment_feed_communication.py b/frappe/patches/v6_19/comment_feed_communication.py deleted file mode 100644 index 64c5ad9c4c..0000000000 --- a/frappe/patches/v6_19/comment_feed_communication.py +++ /dev/null @@ -1,307 +0,0 @@ - -import frappe -from frappe import _ -from frappe.model.rename_doc import get_link_fields -from frappe.model.dynamic_links import dynamic_link_queries -from frappe.permissions import reset_perms - -def execute(): - # comments stay comments in v12 - return - - frappe.reload_doctype("DocType") - frappe.reload_doctype("Communication") - reset_perms("Communication") - - migrate_comments() - frappe.delete_doc("DocType", "Comment") - # frappe.db.sql_ddl("drop table `tabComment`") - - migrate_feed() - frappe.delete_doc("DocType", "Feed") - # frappe.db.sql_ddl("drop table `tabFeed`") - - update_timeline_doc_for("Blogger") - -def migrate_comments(): - from_fields = "" - to_fields = "" - - if "reference_doctype" in frappe.db.get_table_columns("Comment"): - from_fields = "reference_doctype as link_doctype, reference_name as link_name," - to_fields = "link_doctype, link_name," - - # comments - frappe.db.sql("""insert ignore into `tabCommunication` ( - subject, - content, - sender, - sender_full_name, - comment_type, - communication_date, - reference_doctype, - reference_name, - {to_fields} - - name, - user, - owner, - creation, - modified_by, - modified, - status, - sent_or_received, - communication_type, - seen - ) - select - substring(comment, 1, 100) as subject, - comment as content, - comment_by as sender, - comment_by_fullname as sender_full_name, - comment_type, - ifnull(timestamp(comment_date, comment_time), creation) as communication_date, - comment_doctype as reference_doctype, - comment_docname as reference_name, - {from_fields} - - name, - owner as user, - owner, - creation, - modified_by, - modified, - 'Linked' as status, - 'Sent' as sent_or_received, - 'Comment' as communication_type, - 1 as seen - from `tabComment` where comment_doctype is not null and comment_doctype not in ('Message', 'My Company')""" - .format(to_fields=to_fields, from_fields=from_fields)) - - # chat and assignment notifications - frappe.db.sql("""insert ignore into `tabCommunication` ( - subject, - content, - sender, - sender_full_name, - comment_type, - communication_date, - reference_doctype, - reference_name, - {to_fields} - - name, - user, - owner, - creation, - modified_by, - modified, - status, - sent_or_received, - communication_type, - seen - ) - select - case - when parenttype='Assignment' then %(assignment)s - else substring(comment, 1, 100) - end - as subject, - comment as content, - comment_by as sender, - comment_by_fullname as sender_full_name, - comment_type, - ifnull(timestamp(comment_date, comment_time), creation) as communication_date, - 'User' as reference_doctype, - comment_docname as reference_name, - {from_fields} - - name, - owner as user, - owner, - creation, - modified_by, - modified, - 'Linked' as status, - 'Sent' as sent_or_received, - case - when parenttype='Assignment' then 'Notification' - else 'Chat' - end - as communication_type, - 1 as seen - from `tabComment` where comment_doctype in ('Message', 'My Company')""" - .format(to_fields=to_fields, from_fields=from_fields), {"assignment": _("Assignment")}) - -def migrate_feed(): - # migrate delete feed - for doctype in frappe.db.sql("""select distinct doc_type from `tabFeed` where subject=%(deleted)s""", {"deleted": _("Deleted")}): - frappe.db.sql("""insert ignore into `tabCommunication` ( - subject, - sender, - sender_full_name, - comment_type, - communication_date, - reference_doctype, - - name, - user, - owner, - creation, - modified_by, - modified, - status, - sent_or_received, - communication_type, - seen - ) - select - concat_ws(" ", %(_doctype)s, doc_name) as subject, - owner as sender, - full_name as sender_full_name, - 'Deleted' as comment_type, - creation as communication_date, - doc_type as reference_doctype, - - name, - owner as user, - owner, - creation, - modified_by, - modified, - 'Linked' as status, - 'Sent' as sent_or_received, - 'Comment' as communication_type, - 1 as seen - from `tabFeed` where subject=%(deleted)s and doc_type=%(doctype)s""", { - "deleted": _("Deleted"), - "doctype": doctype, - "_doctype": _(doctype) - }) - - # migrate feed type login or empty - frappe.db.sql("""insert ignore into `tabCommunication` ( - subject, - sender, - sender_full_name, - comment_type, - communication_date, - reference_doctype, - reference_name, - - name, - user, - owner, - creation, - modified_by, - modified, - status, - sent_or_received, - communication_type, - seen - ) - select - subject, - owner as sender, - full_name as sender_full_name, - case - when feed_type='Login' then 'Info' - else 'Updated' - end as comment_type, - creation as communication_date, - doc_type as reference_doctype, - doc_name as reference_name, - - name, - owner as user, - owner, - creation, - modified_by, - modified, - 'Linked' as status, - 'Sent' as sent_or_received, - 'Comment' as communication_type, - 1 as seen - from `tabFeed` where (feed_type in ('Login', '') or feed_type is null)""") - -def update_timeline_doc_for(timeline_doctype): - """NOTE: This method may be used by other apps for patching. It also has COMMIT after each update.""" - - # find linked doctypes - # link fields - update_for_linked_docs(timeline_doctype) - - # dynamic link fields - update_for_dynamically_linked_docs(timeline_doctype) - -def update_for_linked_docs(timeline_doctype): - for df in get_link_fields(timeline_doctype): - if df.issingle: - continue - - reference_doctype = df.parent - - if not is_valid_timeline_doctype(reference_doctype, timeline_doctype): - continue - - for doc in frappe.get_all(reference_doctype, fields=["name", df.fieldname]): - timeline_name = doc.get(df.fieldname) - update_communication(timeline_doctype, timeline_name, reference_doctype, doc.name) - -def update_for_dynamically_linked_docs(timeline_doctype): - dynamic_link_fields = [] - for query in dynamic_link_queries: - for df in frappe.db.sql(query, as_dict=True): - dynamic_link_fields.append(df) - - for df in dynamic_link_fields: - reference_doctype = df.parent - - if not is_valid_timeline_doctype(reference_doctype, timeline_doctype): - continue - - try: - docs = frappe.get_all(reference_doctype, fields=["name", df.fieldname], - filters={ df.options: timeline_doctype }) - except frappe.db.SQLError as e: - if frappe.db.is_table_missing(e): - # single - continue - else: - raise - - for doc in docs: - timeline_name = doc.get(df.fieldname) - update_communication(timeline_doctype, timeline_name, reference_doctype, doc.name) - -def update_communication(timeline_doctype, timeline_name, reference_doctype, reference_name): - if not timeline_name: - return - - frappe.db.sql("""update `tabCommunication` set timeline_doctype=%(timeline_doctype)s, timeline_name=%(timeline_name)s - where (reference_doctype=%(reference_doctype)s and reference_name=%(reference_name)s) - and (timeline_doctype is null or timeline_doctype='') - and (timeline_name is null or timeline_name='')""", { - "timeline_doctype": timeline_doctype, - "timeline_name": timeline_name, - "reference_doctype": reference_doctype, - "reference_name": reference_name - }) - - frappe.db.commit() - -def is_valid_timeline_doctype(reference_doctype, timeline_doctype): - # for reloading timeline_field - frappe.reload_doctype(reference_doctype) - - # make sure the timeline field's doctype is same as timeline doctype - meta = frappe.get_meta(reference_doctype) - if not meta.timeline_field: - return False - - doctype = meta.get_link_doctype(meta.timeline_field) - if doctype != timeline_doctype: - return False - - - return True diff --git a/frappe/patches/v6_2/__init__.py b/frappe/patches/v6_2/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/frappe/patches/v6_2/ignore_user_permissions_if_missing.py b/frappe/patches/v6_2/ignore_user_permissions_if_missing.py deleted file mode 100644 index e216dc36b6..0000000000 --- a/frappe/patches/v6_2/ignore_user_permissions_if_missing.py +++ /dev/null @@ -1,8 +0,0 @@ - -import frappe - -def execute(): - frappe.reload_doctype("System Settings") - system_settings = frappe.get_doc("System Settings") - system_settings.flags.ignore_mandatory = 1 - system_settings.save() diff --git a/frappe/patches/v6_2/rename_backup_manager.py b/frappe/patches/v6_2/rename_backup_manager.py deleted file mode 100644 index df2fa72c05..0000000000 --- a/frappe/patches/v6_2/rename_backup_manager.py +++ /dev/null @@ -1,20 +0,0 @@ - -import frappe - -def execute(): - unset = False - frappe.reload_doc("integrations", "doctype", "dropbox_backup") - - dropbox_backup = frappe.get_doc("Dropbox Backup", "Dropbox Backup") - for df in dropbox_backup.meta.fields: - value = frappe.db.get_single_value("Backup Manager", df.fieldname) - if value: - if df.fieldname=="upload_backups_to_dropbox" and value=="Never": - value = "Daily" - unset = True - dropbox_backup.set(df.fieldname, value) - - if unset: - dropbox_backup.set("send_backups_to_dropbox", 0) - - dropbox_backup.save() diff --git a/frappe/patches/v6_20x/__init__.py b/frappe/patches/v6_20x/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/frappe/patches/v6_20x/remove_roles_from_website_user.py b/frappe/patches/v6_20x/remove_roles_from_website_user.py deleted file mode 100644 index 19009ff455..0000000000 --- a/frappe/patches/v6_20x/remove_roles_from_website_user.py +++ /dev/null @@ -1,11 +0,0 @@ - -import frappe - -def execute(): - frappe.reload_doc("core", "doctype", "user_email") - frappe.reload_doc("core", "doctype", "user") - for user_name in frappe.get_all('User', filters={'user_type': 'Website User'}): - user = frappe.get_doc('User', user_name) - if user.roles: - user.roles = [] - user.save() diff --git a/frappe/patches/v6_20x/set_allow_draft_for_print.py b/frappe/patches/v6_20x/set_allow_draft_for_print.py deleted file mode 100644 index 0b604567ec..0000000000 --- a/frappe/patches/v6_20x/set_allow_draft_for_print.py +++ /dev/null @@ -1,5 +0,0 @@ - -import frappe - -def execute(): - frappe.db.set_value("Print Settings", "Print Settings", "allow_print_for_draft", 1) \ No newline at end of file diff --git a/frappe/patches/v6_20x/update_insert_after.py b/frappe/patches/v6_20x/update_insert_after.py deleted file mode 100644 index 37820b2437..0000000000 --- a/frappe/patches/v6_20x/update_insert_after.py +++ /dev/null @@ -1,27 +0,0 @@ - -import frappe, json - -def execute(): - for ps in frappe.get_all('Property Setter', filters={'property': '_idx'}, - fields = ['doc_type', 'value']): - custom_fields = frappe.get_all('Custom Field', - filters = {'dt': ps.doc_type}, fields=['name', 'fieldname']) - - if custom_fields: - _idx = json.loads(ps.value) - - for custom_field in custom_fields: - if custom_field.fieldname in _idx: - custom_field_idx = _idx.index(custom_field.fieldname) - if custom_field_idx == 0: - prev_fieldname = "" - - else: - prev_fieldname = _idx[custom_field_idx - 1] - - else: - prev_fieldname = _idx[-1] - custom_field_idx = len(_idx) - - frappe.db.set_value('Custom Field', custom_field.name, 'insert_after', prev_fieldname) - frappe.db.set_value('Custom Field', custom_field.name, 'idx', custom_field_idx) diff --git a/frappe/patches/v6_21/__init__.py b/frappe/patches/v6_21/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/frappe/patches/v6_21/print_settings_repeat_header_footer.py b/frappe/patches/v6_21/print_settings_repeat_header_footer.py deleted file mode 100644 index 0919c35903..0000000000 --- a/frappe/patches/v6_21/print_settings_repeat_header_footer.py +++ /dev/null @@ -1,6 +0,0 @@ - -import frappe - -def execute(): - frappe.reload_doctype('Print Settings') - frappe.db.set_value('Print Settings', 'Print Settings', 'repeat_header_footer', 1) diff --git a/frappe/patches/v6_24/__init__.py b/frappe/patches/v6_24/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/frappe/patches/v6_24/set_language_as_code.py b/frappe/patches/v6_24/set_language_as_code.py deleted file mode 100644 index 6f862ede2e..0000000000 --- a/frappe/patches/v6_24/set_language_as_code.py +++ /dev/null @@ -1,8 +0,0 @@ - -import frappe - -from frappe.translate import get_lang_dict - -# migrate language from name to code -def execute(): - return diff --git a/frappe/patches/v6_4/__init__.py b/frappe/patches/v6_4/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/frappe/patches/v6_4/reduce_varchar_length.py b/frappe/patches/v6_4/reduce_varchar_length.py deleted file mode 100644 index 7edde55778..0000000000 --- a/frappe/patches/v6_4/reduce_varchar_length.py +++ /dev/null @@ -1,36 +0,0 @@ -import frappe - -def execute(): - for doctype in frappe.get_all("DocType", filters={"issingle": 0}): - doctype = doctype.name - if not frappe.db.table_exists(doctype): - continue - - for column in frappe.db.sql("desc `tab{doctype}`".format(doctype=doctype), as_dict=True): - fieldname = column["Field"] - column_type = column["Type"] - - if not column_type.startswith("varchar"): - continue - - max_length = frappe.db.sql("""select max(char_length(`{fieldname}`)) from `tab{doctype}`"""\ - .format(fieldname=fieldname, doctype=doctype)) - - max_length = max_length[0][0] if max_length else None - - if max_length and 140 < max_length <= 255: - print( - "setting length of '{fieldname}' in '{doctype}' as {length}".format( - fieldname=fieldname, doctype=doctype, length=max_length) - ) - - # create property setter for length - frappe.make_property_setter({ - "doctype": doctype, - "fieldname": fieldname, - "property": "length", - "value": max_length, - "property_type": "Int" - }) - - frappe.clear_cache(doctype=doctype) diff --git a/frappe/patches/v6_4/rename_bengali_language.py b/frappe/patches/v6_4/rename_bengali_language.py deleted file mode 100644 index f872dea1b9..0000000000 --- a/frappe/patches/v6_4/rename_bengali_language.py +++ /dev/null @@ -1,6 +0,0 @@ -# -*- coding: utf-8 -*- -import frappe -from frappe.translate import rename_language - -def execute(): - rename_language("বাঙালি", "বাংলা") \ No newline at end of file diff --git a/frappe/patches/v6_6/__init__.py b/frappe/patches/v6_6/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/frappe/patches/v6_6/fix_file_url.py b/frappe/patches/v6_6/fix_file_url.py deleted file mode 100644 index 48e292f4d4..0000000000 --- a/frappe/patches/v6_6/fix_file_url.py +++ /dev/null @@ -1,36 +0,0 @@ - -import frappe -from frappe.model.meta import is_single - -def execute(): - """Fix old style file urls that start with files/""" - fix_file_urls() - fix_attach_field_urls() - -def fix_file_urls(): - for file in frappe.db.sql_list("""select name from `tabFile` where file_url like 'files/%'"""): - file = frappe.get_doc("File", file) - file.db_set("file_url", "/" + file.file_url, update_modified=False) - try: - file.validate_file() - file.db_set("file_name", file.file_name, update_modified=False) - if not file.content_hash: - file.generate_content_hash() - file.db_set("content_hash", file.content_hash, update_modified=False) - - except IOError: - pass - -def fix_attach_field_urls(): - # taken from an old patch - attach_fields = (frappe.db.sql("""select parent, fieldname from `tabDocField` where fieldtype in ('Attach', 'Attach Image')""") + - frappe.db.sql("""select dt, fieldname from `tabCustom Field` where fieldtype in ('Attach', 'Attach Image')""")) - - for doctype, fieldname in attach_fields: - if is_single(doctype): - frappe.db.sql("""update `tabSingles` set value=concat("/", `value`) - where doctype=%(doctype)s and field=%(fieldname)s - and value like 'files/%%'""", {"doctype": doctype, "fieldname": fieldname}) - else: - frappe.db.sql("""update `tab{doctype}` set `{fieldname}`=concat("/", `{fieldname}`) - where `{fieldname}` like 'files/%'""".format(doctype=doctype, fieldname=fieldname)) diff --git a/frappe/patches/v6_6/rename_slovak_language.py b/frappe/patches/v6_6/rename_slovak_language.py deleted file mode 100644 index 198949e79c..0000000000 --- a/frappe/patches/v6_6/rename_slovak_language.py +++ /dev/null @@ -1,6 +0,0 @@ -# -*- coding: utf-8 -*- -import frappe -from frappe.translate import rename_language - -def execute(): - rename_language("slovenčina", "slovenčina (Slovak)") diff --git a/frappe/patches/v6_6/user_last_active.py b/frappe/patches/v6_6/user_last_active.py deleted file mode 100644 index b9f63fa45e..0000000000 --- a/frappe/patches/v6_6/user_last_active.py +++ /dev/null @@ -1,6 +0,0 @@ - -import frappe - -def execute(): - frappe.reload_doctype("User") - frappe.db.sql("update `tabUser` set last_active=last_login") diff --git a/frappe/patches/v6_9/__init__.py b/frappe/patches/v6_9/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/frappe/patches/v6_9/int_float_not_null.py b/frappe/patches/v6_9/int_float_not_null.py deleted file mode 100644 index c414d6b583..0000000000 --- a/frappe/patches/v6_9/int_float_not_null.py +++ /dev/null @@ -1,30 +0,0 @@ - -import frappe -from frappe.utils import cint, flt - -def execute(): - for doctype in frappe.get_all("DocType", filters={"issingle": 0}): - doctype = doctype.name - meta = frappe.get_meta(doctype) - - for column in frappe.db.sql("desc `tab{doctype}`".format(doctype=doctype), as_dict=True): - fieldname = column["Field"] - column_type = column["Type"] - - if not (column_type.startswith("int") or column_type.startswith("decimal")): - continue - - frappe.db.sql("""update `tab{doctype}` set `{fieldname}`=0 where `{fieldname}` is null"""\ - .format(doctype=doctype, fieldname=fieldname)) - - # alter table - if column["Null"]=='YES': - if not meta.get_field(fieldname): - continue - - default = cint(column["Default"]) if column_type.startswith("int") else flt(column["Default"]) - frappe.db.sql_ddl("""alter table `tab{doctype}` - change `{fieldname}` `{fieldname}` {column_type} not null default '{default}'""".format( - doctype=doctype, fieldname=fieldname, column_type=column_type, default=default)) - - diff --git a/frappe/patches/v6_9/rename_burmese_language.py b/frappe/patches/v6_9/rename_burmese_language.py deleted file mode 100644 index 5e1333077e..0000000000 --- a/frappe/patches/v6_9/rename_burmese_language.py +++ /dev/null @@ -1,6 +0,0 @@ -# -*- coding: utf-8 -*- -import frappe -from frappe.translate import rename_language - -def execute(): - rename_language("Melayu", "မြန်မာ") diff --git a/frappe/patches/v7_0/__init__.py b/frappe/patches/v7_0/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/frappe/patches/v7_0/add_communication_in_doc.py b/frappe/patches/v7_0/add_communication_in_doc.py deleted file mode 100644 index 8be229fe3a..0000000000 --- a/frappe/patches/v7_0/add_communication_in_doc.py +++ /dev/null @@ -1,14 +0,0 @@ - -import frappe - -from frappe.core.doctype.comment.comment import update_comment_in_doc - -def execute(): - for d in frappe.db.get_all("Communication", - fields = ['name', 'reference_doctype', 'reference_name', 'SUBSTRING(content,1,102)', 'communication_type'], - filters = {"reference_name":None,"reference_doctype":None,'communication_type': 'Communication'}): - - try: - update_comment_in_doc(d) - except frappe.ImplicitCommitError: - pass diff --git a/frappe/patches/v7_0/cleanup_list_settings.py b/frappe/patches/v7_0/cleanup_list_settings.py deleted file mode 100644 index 9fe2e71ed1..0000000000 --- a/frappe/patches/v7_0/cleanup_list_settings.py +++ /dev/null @@ -1,20 +0,0 @@ - -import frappe, json - -def execute(): - if frappe.db.table_exists("__ListSettings"): - list_settings = frappe.db.sql("select user, doctype, data from __ListSettings", as_dict=1) - for ls in list_settings: - if ls and ls.data: - data = json.loads(ls.data) - if "fields" not in data: - continue - fields = data["fields"] - for field in fields: - if "name as" in field: - fields.remove(field) - data["fields"] = fields - - frappe.db.sql("update __ListSettings set data = %s where user=%s and doctype=%s", - (json.dumps(data), ls.user, ls.doctype)) - diff --git a/frappe/patches/v7_0/create_private_file_folder.py b/frappe/patches/v7_0/create_private_file_folder.py deleted file mode 100644 index e89beb5d0f..0000000000 --- a/frappe/patches/v7_0/create_private_file_folder.py +++ /dev/null @@ -1,6 +0,0 @@ - -import frappe, os - -def execute(): - if not os.path.exists(os.path.join(frappe.local.site_path, 'private', 'files')): - frappe.create_folder(os.path.join(frappe.local.site_path, 'private', 'files')) \ No newline at end of file diff --git a/frappe/patches/v7_0/re_route.py b/frappe/patches/v7_0/re_route.py deleted file mode 100644 index 8a4daaea86..0000000000 --- a/frappe/patches/v7_0/re_route.py +++ /dev/null @@ -1,23 +0,0 @@ - -import frappe -from frappe.model.base_document import get_controller - -def execute(): - update_routes(['Blog Post', 'Blog Category', 'Web Page']) - -def update_routes(doctypes): - """Patch old routing system""" - for d in doctypes: - frappe.reload_doctype(d) - c = get_controller(d) - - condition = '' - if c.website.condition_field: - condition = 'where {0}=1'.format(c.website.condition_field) - - try: - frappe.db.sql("""update ignore `tab{0}` set route = concat(ifnull(parent_website_route, ""), - if(ifnull(parent_website_route, "")="", "", "/"), page_name) {1}""".format(d, condition)) - - except Exception as e: - if not frappe.db.is_missing_column(e): raise diff --git a/frappe/patches/v7_0/rename_bulk_email_to_email_queue.py b/frappe/patches/v7_0/rename_bulk_email_to_email_queue.py deleted file mode 100644 index 42f2dfe4c2..0000000000 --- a/frappe/patches/v7_0/rename_bulk_email_to_email_queue.py +++ /dev/null @@ -1,5 +0,0 @@ - -import frappe - -def execute(): - frappe.rename_doc('DocType', 'Bulk Email', 'Email Queue') \ No newline at end of file diff --git a/frappe/patches/v7_0/rename_newsletter_list_to_email_group.py b/frappe/patches/v7_0/rename_newsletter_list_to_email_group.py deleted file mode 100644 index 5e40d9df35..0000000000 --- a/frappe/patches/v7_0/rename_newsletter_list_to_email_group.py +++ /dev/null @@ -1,6 +0,0 @@ - -import frappe - -def execute(): - frappe.rename_doc('DocType', 'Newsletter List', 'Email Group') - frappe.rename_doc('DocType', 'Newsletter List Subscriber', 'Email Group Member') \ No newline at end of file diff --git a/frappe/patches/v7_0/set_email_group.py b/frappe/patches/v7_0/set_email_group.py deleted file mode 100644 index 251e9a27b6..0000000000 --- a/frappe/patches/v7_0/set_email_group.py +++ /dev/null @@ -1,10 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# License: GNU General Public License v3. See license.txt - -import frappe - -def execute(): - frappe.reload_doc("email", "doctype", "email_group_member") - if "newsletter_list" in frappe.db.get_table_columns("Email Group Member"): - frappe.db.sql("""update `tabEmail Group Member` set email_group = newsletter_list - where email_group is null or email_group = ''""") \ No newline at end of file diff --git a/frappe/patches/v7_0/set_user_fullname.py b/frappe/patches/v7_0/set_user_fullname.py deleted file mode 100644 index e69c180c27..0000000000 --- a/frappe/patches/v7_0/set_user_fullname.py +++ /dev/null @@ -1,10 +0,0 @@ - -import frappe - -def execute(): - frappe.reload_doc("Core", "DocType", "User") - - for user in frappe.db.get_all('User'): - user = frappe.get_doc('User', user.name) - user.set_full_name() - user.db_set('full_name', user.full_name, update_modified = False) \ No newline at end of file diff --git a/frappe/patches/v7_0/update_auth.py b/frappe/patches/v7_0/update_auth.py deleted file mode 100644 index 098081563f..0000000000 --- a/frappe/patches/v7_0/update_auth.py +++ /dev/null @@ -1,42 +0,0 @@ - -import frappe -from frappe.utils.password import create_auth_table, set_encrypted_password - -def execute(): - if '__OldAuth' not in frappe.db.get_tables(): - frappe.db.sql_ddl('''alter table `__Auth` rename `__OldAuth`''') - - create_auth_table() - - # user passwords - frappe.db.sql('''insert ignore into `__Auth` (doctype, name, fieldname, `password`) - (select 'User', `name`, 'password', `password` from `__OldAuth`)''') - - frappe.db.commit() - - # other password fields - for doctype in frappe.db.sql_list('''select distinct parent from `tabDocField` - where fieldtype="Password" and parent != "User"'''): - - frappe.reload_doctype(doctype) - meta = frappe.get_meta(doctype) - - for df in meta.get('fields', {'fieldtype': 'Password'}): - if meta.issingle: - password = frappe.db.get_value(doctype, doctype, df.fieldname) - if password: - set_encrypted_password(doctype, doctype, password, fieldname=df.fieldname) - frappe.db.set_value(doctype, doctype, df.fieldname, '*'*len(password)) - - else: - for d in frappe.db.sql('''select name, `{fieldname}` from `tab{doctype}` - where `{fieldname}` is not null'''.format(fieldname=df.fieldname, doctype=doctype), as_dict=True): - - set_encrypted_password(doctype, d.name, d.get(df.fieldname), fieldname=df.fieldname) - - frappe.db.sql('''update `tab{doctype}` set `{fieldname}`=repeat("*", char_length(`{fieldname}`))''' - .format(doctype=doctype, fieldname=df.fieldname)) - - frappe.db.commit() - - frappe.db.sql_ddl('''drop table `__OldAuth`''') diff --git a/frappe/patches/v7_0/update_report_builder_json.py b/frappe/patches/v7_0/update_report_builder_json.py deleted file mode 100644 index 01a6126de7..0000000000 --- a/frappe/patches/v7_0/update_report_builder_json.py +++ /dev/null @@ -1,11 +0,0 @@ -# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors -# License: GNU General Public License v3. See license.txt - -import frappe - -def execute(): - for report in frappe.db.sql_list(""" select name from `tabReport` where report_type = 'Report Builder' - and is_standard = 'No' and `json` != '' and `json` is not null """): - doc = frappe.get_doc("Report", report) - doc.update_report_json() - doc.db_set("json", doc.json, update_modified=False) \ No newline at end of file diff --git a/frappe/patches/v7_0/update_send_after_in_bulk_email.py b/frappe/patches/v7_0/update_send_after_in_bulk_email.py deleted file mode 100644 index b9da83eaab..0000000000 --- a/frappe/patches/v7_0/update_send_after_in_bulk_email.py +++ /dev/null @@ -1,6 +0,0 @@ - -import frappe -from frappe.utils import now_datetime - -def execute(): - frappe.db.sql('update `tabEmail Queue` set send_after=%s where send_after is null', now_datetime()) \ No newline at end of file diff --git a/frappe/patches/v7_1/__init__.py b/frappe/patches/v7_1/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/frappe/patches/v7_1/disabled_print_settings_for_custom_print_format.py b/frappe/patches/v7_1/disabled_print_settings_for_custom_print_format.py deleted file mode 100644 index 6ab9340845..0000000000 --- a/frappe/patches/v7_1/disabled_print_settings_for_custom_print_format.py +++ /dev/null @@ -1,16 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# License: GNU General Public License v3. See license.txt - -import frappe - -def execute(): - frappe.reload_doctype('Print Format') - frappe.db.sql(""" - update - `tabPrint Format` - set - align_labels_right = 0, line_breaks = 0, show_section_headings = 0 - where - custom_format = 1 - """) diff --git a/frappe/patches/v7_1/refactor_integration_broker.py b/frappe/patches/v7_1/refactor_integration_broker.py deleted file mode 100644 index 05ccae5d46..0000000000 --- a/frappe/patches/v7_1/refactor_integration_broker.py +++ /dev/null @@ -1,51 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# License: GNU General Public License v3. See license.txt - -import frappe -import json - -def execute(): - for doctype_name in ["Razorpay Log", "Razorpay Payment", "Razorpay Settings"]: - delete_doc("DocType", doctype_name) - - reload_doctypes() - setup_services() - -def delete_doc(doctype, doctype_name): - frappe.delete_doc(doctype, doctype_name) - -def reload_doctypes(): - for doctype in ("razorpay_settings", "paypal_settings", "dropbox_settings", "ldap_settings"): - frappe.reload_doc("integrations", "doctype", doctype) - -def setup_services(): - for service in [{"old_name": "Razorpay", "new_name": "Razorpay"}, - {"old_name": "PayPal", "new_name": "PayPal"}, - {"old_name": "Dropbox Integration", "new_name": "Dropbox"}, - {"old_name": "LDAP Auth", "new_name": "LDAP"}]: - - try: - service_doc = frappe.get_doc("Integration Service", service["old_name"]) - settings = json.loads(service_doc.custom_settings_json) - - service_settings = frappe.new_doc("{0} Settings".format(service["new_name"])) - service_settings.update(settings) - - service_settings.flags.ignore_mandatory = True - service_settings.save(ignore_permissions=True) - - if service["old_name"] in ["Dropbox Integration", "LDAP Auth"]: - delete_doc("Integration Service", service["old_name"]) - - new_service_doc = frappe.get_doc({ - "doctype": "Integration Service", - "service": service["new_name"], - "enabled": 1 - }) - - new_service_doc.flags.ignore_mandatory = True - new_service_doc.save(ignore_permissions=True) - - except Exception: - pass diff --git a/frappe/patches/v7_1/rename_chinese_language_codes.py b/frappe/patches/v7_1/rename_chinese_language_codes.py deleted file mode 100644 index 91ed73ccae..0000000000 --- a/frappe/patches/v7_1/rename_chinese_language_codes.py +++ /dev/null @@ -1,11 +0,0 @@ - -import frappe - -def execute(): - frappe.rename_doc('Language', 'zh-cn', 'zh', force=True, - merge=True if frappe.db.exists('Language', 'zh') else False) - if frappe.db.get_value('Language', 'zh-tw') == 'zh-tw': - frappe.rename_doc('Language', 'zh-tw', 'zh-TW', force=True) - - frappe.db.set_value('Language', 'zh', 'language_code', 'zh') - frappe.db.set_value('Language', 'zh-TW', 'language_code', 'zh-TW') \ No newline at end of file diff --git a/frappe/patches/v7_1/rename_scheduler_log_to_error_log.py b/frappe/patches/v7_1/rename_scheduler_log_to_error_log.py deleted file mode 100644 index c0c9e03565..0000000000 --- a/frappe/patches/v7_1/rename_scheduler_log_to_error_log.py +++ /dev/null @@ -1,11 +0,0 @@ - -import frappe - -def execute(): - if not 'tabError Log' in frappe.db.get_tables(): - frappe.rename_doc('DocType', 'Scheduler Log', 'Error Log') - frappe.db.sql("""delete from `tabError Log` where datediff(curdate(), creation) > 30""") - frappe.db.commit() - frappe.db.sql('alter table `tabError Log` change column name name varchar(140)') - frappe.db.sql('alter table `tabError Log` change column parent parent varchar(140)') - frappe.db.sql('alter table `tabError Log` engine=MyISAM') diff --git a/frappe/patches/v7_1/set_backup_limit.py b/frappe/patches/v7_1/set_backup_limit.py deleted file mode 100644 index ce502393b2..0000000000 --- a/frappe/patches/v7_1/set_backup_limit.py +++ /dev/null @@ -1,10 +0,0 @@ - -from frappe.utils import cint -import frappe - -def execute(): - frappe.reload_doctype('System Settings') - backup_limit = frappe.db.get_single_value('System Settings', 'backup_limit') - - if cint(backup_limit) == 0: - frappe.db.set_value('System Settings', 'System Settings', 'backup_limit', 3) diff --git a/frappe/patches/v7_1/setup_integration_services.py b/frappe/patches/v7_1/setup_integration_services.py deleted file mode 100644 index 9f4c8a3915..0000000000 --- a/frappe/patches/v7_1/setup_integration_services.py +++ /dev/null @@ -1,118 +0,0 @@ - -import frappe -from frappe.exceptions import DataError -from frappe.utils.password import get_decrypted_password -from frappe.utils import cstr -import os - -app_list = [ - {"app_name": "razorpay_integration", "service_name": "Razorpay", "doctype": "Razorpay Settings", "remove": True}, - {"app_name": "paypal_integration", "service_name": "PayPal", "doctype": "PayPal Settings", "remove": True}, - {"app_name": "frappe", "service_name": "Dropbox", "doctype": "Dropbox Backup", "remove": False} -] - -def execute(): - installed_apps = frappe.get_installed_apps() - - for app_details in app_list: - if app_details["app_name"] in installed_apps: - settings = get_app_settings(app_details) - if app_details["remove"]: - uninstall_app(app_details["app_name"]) - - try: - setup_integration_service(app_details, settings) - except DataError: - pass - - frappe.delete_doc("DocType", "Dropbox Backup") - -def setup_integration_service(app_details, settings=None): - if not settings: - return - - setup_service_settings(app_details["service_name"], settings) - - doc_path = frappe.get_app_path("frappe", "integration_broker", "doctype", - "integration_service", "integration_service.json") - - if not os.path.exists(doc_path): - return - - frappe.reload_doc("integration_broker", "doctype", "integration_service") - - if frappe.db.exists("Integration Service", app_details["service_name"]): - integration_service = frappe.get_doc("Integration Service", app_details["service_name"]) - else: - integration_service = frappe.new_doc("Integration Service") - integration_service.service = app_details["service_name"] - - integration_service.enabled = 1 - integration_service.flags.ignore_mandatory = True - integration_service.save(ignore_permissions=True) - -def get_app_settings(app_details): - parameters = {} - doctype = docname = app_details["doctype"] - - app_settings = get_parameters(app_details) - if app_settings: - settings = app_settings["settings"] - frappe.reload_doc("integrations", "doctype", "{0}_settings".format(app_details["service_name"].lower())) - controller = frappe.get_meta("{0} Settings".format(app_details["service_name"])) - - for d in controller.fields: - if settings.get(d.fieldname): - if ''.join(set(cstr(settings.get(d.fieldname)))) == '*': - setattr(settings, d.fieldname, get_decrypted_password(doctype, docname, d.fieldname, raise_exception=True)) - - parameters.update({d.fieldname : settings.get(d.fieldname)}) - - return parameters - -def uninstall_app(app_name): - from frappe.installer import remove_from_installed_apps - remove_from_installed_apps(app_name) - -def get_parameters(app_details): - if app_details["service_name"] == "Razorpay": - return {"settings": frappe.get_doc(app_details["doctype"])} - - elif app_details["service_name"] == "PayPal": - if frappe.conf.paypal_username and frappe.conf.paypal_password and frappe.conf.paypal_signature: - return { - "settings": { - "api_username": frappe.conf.paypal_username, - "api_password": frappe.conf.paypal_password, - "signature": frappe.conf.paypal_signature - } - } - else: - return {"settings": frappe.get_doc(app_details["doctype"])} - - elif app_details["service_name"] == "Dropbox": - doc = frappe.db.get_value(app_details["doctype"], None, - ["dropbox_access_key", "dropbox_access_secret", "upload_backups_to_dropbox"], as_dict=1) - - if not doc: - return - - if not (frappe.conf.dropbox_access_key and frappe.conf.dropbox_secret_key): - return - - return { - "settings": { - "app_access_key": frappe.conf.dropbox_access_key, - "app_secret_key": frappe.conf.dropbox_secret_key, - "dropbox_access_key": doc.dropbox_access_key, - "dropbox_access_secret": doc.dropbox_access_secret, - "backup_frequency": doc.upload_backups_to_dropbox, - "enabled": doc.send_backups_to_dropbox - } - } - -def setup_service_settings(service_name, settings): - service_doc = frappe.get_doc("{0} Settings".format(service_name)) - service_doc.update(settings) - service_doc.flags.ignore_mandatory = True - service_doc.save(ignore_permissions=True) \ No newline at end of file diff --git a/frappe/patches/v7_1/sync_language_doctype.py b/frappe/patches/v7_1/sync_language_doctype.py deleted file mode 100644 index a5e9ad1cb1..0000000000 --- a/frappe/patches/v7_1/sync_language_doctype.py +++ /dev/null @@ -1,22 +0,0 @@ - -import frappe -from frappe.translate import get_lang_dict - -def execute(): - frappe.reload_doc('core', 'doctype', 'language') - - from frappe.core.doctype.language.language import sync_languages - sync_languages() - - # move language from old style to new style for old accounts - # i.e. from "english" to "en" - - lang_dict = get_lang_dict() - language = frappe.db.get_value('System Settings', None, 'language') - if language: - frappe.db.set_value('System Settings', None, 'language', lang_dict.get('language') or 'en') - - for user in frappe.get_all('User', fields=['name', 'language']): - if user.language: - frappe.db.set_value('User', user.name, 'language', - lang_dict.get('language') or 'en', update_modified=False) diff --git a/frappe/patches/v7_2/__init__.py b/frappe/patches/v7_2/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/frappe/patches/v7_2/fix_email_queue_recipient.py b/frappe/patches/v7_2/fix_email_queue_recipient.py deleted file mode 100644 index 021397031b..0000000000 --- a/frappe/patches/v7_2/fix_email_queue_recipient.py +++ /dev/null @@ -1,6 +0,0 @@ - -import frappe - -def execute(): - frappe.reload_doc('email', 'doctype', 'email_queue_recipient') - frappe.db.sql('update `tabEmail Queue Recipient` set parenttype="recipients"') \ No newline at end of file diff --git a/frappe/patches/v7_2/merge_knowledge_base.py b/frappe/patches/v7_2/merge_knowledge_base.py deleted file mode 100644 index 04e6c16213..0000000000 --- a/frappe/patches/v7_2/merge_knowledge_base.py +++ /dev/null @@ -1,24 +0,0 @@ - -import frappe - -from frappe.patches.v7_0.re_route import update_routes -from frappe.installer import remove_from_installed_apps - -def execute(): - if 'knowledge_base' in frappe.get_installed_apps(): - frappe.reload_doc('website', 'doctype', 'help_category') - frappe.reload_doc('website', 'doctype', 'help_article') - update_routes(['Help Category', 'Help Article']) - remove_from_installed_apps('knowledge_base') - - # remove module def - if frappe.db.exists('Module Def', 'Knowledge Base'): - frappe.delete_doc('Module Def', 'Knowledge Base') - - # set missing routes - for doctype in ('Help Category', 'Help Article'): - for d in frappe.get_all(doctype, fields=['name', 'route']): - if not d.route: - doc = frappe.get_doc(doctype, d.name) - doc.set_route() - doc.db_update() \ No newline at end of file diff --git a/frappe/patches/v7_2/remove_in_filter.py b/frappe/patches/v7_2/remove_in_filter.py deleted file mode 100644 index 306879f996..0000000000 --- a/frappe/patches/v7_2/remove_in_filter.py +++ /dev/null @@ -1,7 +0,0 @@ - -import frappe - -def execute(): - if frappe.db.has_column('DocField', 'in_filter'): - frappe.db.sql('alter table tabDocField drop column in_filter') - frappe.clear_cache(doctype="DocField") \ No newline at end of file diff --git a/frappe/patches/v7_2/set_doctype_engine.py b/frappe/patches/v7_2/set_doctype_engine.py deleted file mode 100644 index e0df9cff87..0000000000 --- a/frappe/patches/v7_2/set_doctype_engine.py +++ /dev/null @@ -1,7 +0,0 @@ - -import frappe - -def execute(): - for t in frappe.db.sql('show table status'): - if t[0].startswith('tab'): - frappe.db.sql('update tabDocType set engine=%s where name=%s', (t[1], t[0][3:])) \ No newline at end of file diff --git a/frappe/patches/v7_2/set_in_standard_filter_property.py b/frappe/patches/v7_2/set_in_standard_filter_property.py deleted file mode 100644 index 568f43d2aa..0000000000 --- a/frappe/patches/v7_2/set_in_standard_filter_property.py +++ /dev/null @@ -1,20 +0,0 @@ - -import frappe - -def execute(): - frappe.reload_doc('custom', 'doctype', 'custom_field', force=True) - - try: - frappe.db.sql('update `tabCustom Field` set in_standard_filter = in_filter_dash') - except Exception as e: - if not frappe.db.is_missing_column(e): raise e - - for doctype in frappe.get_all("DocType", {"istable": 0, "issingle": 0, "custom": 0}): - try: - frappe.reload_doctype(doctype.name, force=True) - except KeyError: - pass - except frappe.db.DataError: - pass - except Exception: - pass diff --git a/frappe/patches/v7_2/setup_custom_perms.py b/frappe/patches/v7_2/setup_custom_perms.py deleted file mode 100644 index 1f46072782..0000000000 --- a/frappe/patches/v7_2/setup_custom_perms.py +++ /dev/null @@ -1,13 +0,0 @@ - -import frappe -from frappe.permissions import setup_custom_perms -from frappe.core.page.permission_manager.permission_manager import get_standard_permissions -from frappe.utils.reset_doc import setup_perms_for - -''' -Copy DocPerm to Custom DocPerm where permissions are set differently -''' - -def execute(): - for d in frappe.db.get_all('DocType', dict(istable=0, issingle=0, custom=0)): - setup_perms_for(d.name) diff --git a/frappe/patches/v7_2/setup_ldap_config.py b/frappe/patches/v7_2/setup_ldap_config.py deleted file mode 100644 index c9ad3e6714..0000000000 --- a/frappe/patches/v7_2/setup_ldap_config.py +++ /dev/null @@ -1,22 +0,0 @@ - -import frappe -from frappe.utils import cint - -def execute(): - frappe.reload_doc("integrations", "doctype", "ldap_settings") - - if not frappe.db.exists("DocType", "Integration Service"): - return - - if not frappe.db.exists("Integration Service", "LDAP"): - return - - if not cint(frappe.db.get_value("Integration Service", "LDAP", 'enabled')): - return - - import ldap - try: - ldap_settings = frappe.get_doc("LDAP Settings") - ldap_settings.save(ignore_permissions=True) - except ldap.LDAPError: - pass diff --git a/frappe/patches/v7_2/update_communications.py b/frappe/patches/v7_2/update_communications.py deleted file mode 100644 index 114e531324..0000000000 --- a/frappe/patches/v7_2/update_communications.py +++ /dev/null @@ -1,10 +0,0 @@ - -import frappe - -def execute(): - """ - in communication move feedback details to content - remove Guest None from sender full name - setup feedback request trigger's is_manual field - """ - return diff --git a/frappe/patches/v7_2/update_feedback_request.py b/frappe/patches/v7_2/update_feedback_request.py deleted file mode 100644 index 9bc656bf67..0000000000 --- a/frappe/patches/v7_2/update_feedback_request.py +++ /dev/null @@ -1,10 +0,0 @@ - -import frappe - -def execute(): - """ - rename feedback request documents, - update the feedback request and save the rating and communication - reference in Feedback Request document - """ - return diff --git a/frappe/patches/v8_0/__init__.py b/frappe/patches/v8_0/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/frappe/patches/v8_0/deprecate_integration_broker.py b/frappe/patches/v8_0/deprecate_integration_broker.py deleted file mode 100644 index 9aeee17837..0000000000 --- a/frappe/patches/v8_0/deprecate_integration_broker.py +++ /dev/null @@ -1,51 +0,0 @@ - -import frappe -from frappe.integrations.utils import create_payment_gateway - -def execute(): - setup_enabled_integrations() - - for doctype in ["integration_request", "oauth_authorization_code", "oauth_bearer_token", "oauth_client"]: - frappe.reload_doc('integrations', 'doctype', doctype) - - frappe.reload_doc("core", "doctype", "payment_gateway") - update_doctype_module() - create_payment_gateway_master_records() - - for doctype in ["Integration Service", "Integration Service Parameter"]: - frappe.delete_doc("DocType", doctype) - - if not frappe.db.get_value("DocType", {"module": "Integration Broker"}, "name"): - frappe.delete_doc("Module Def", "Integration Broker") - -def setup_enabled_integrations(): - if not frappe.db.exists("DocType", "Integration Service"): - return - - for service in frappe.get_all("Integration Service", - filters={"enabled": 1, "service": ('in', ("Dropbox", "LDAP"))}, fields=["name"]): - - doctype = "{0} Settings".format(service.name) - frappe.db.set_value(doctype, doctype, 'enabled', 1) - -def update_doctype_module(): - frappe.db.sql("""update tabDocType set module='Integrations' - where name in ('Integration Request', 'Oauth Authorization Code', - 'Oauth Bearer Token', 'Oauth Client') """) - - frappe.db.sql(""" update tabDocType set module='Core' where name = 'Payment Gateway'""") - -def create_payment_gateway_master_records(): - for payment_gateway in ["Razorpay", "PayPal"]: - doctype = "{0} Settings".format(payment_gateway) - doc = frappe.get_doc(doctype) - doc_meta = frappe.get_meta(doctype) - all_mandatory_fields_has_value = True - - for d in doc_meta.fields: - if d.reqd and not doc.get(d.fieldname): - all_mandatory_fields_has_value = False - break - - if all_mandatory_fields_has_value: - create_payment_gateway(payment_gateway) diff --git a/frappe/patches/v8_0/drop_in_dialog.py b/frappe/patches/v8_0/drop_in_dialog.py deleted file mode 100644 index 5022333d22..0000000000 --- a/frappe/patches/v8_0/drop_in_dialog.py +++ /dev/null @@ -1,7 +0,0 @@ - -import frappe - -def execute(): - if frappe.db.has_column('DocType', 'in_dialog'): - frappe.db.sql('alter table tabDocType drop column in_dialog') - frappe.clear_cache(doctype="DocType") \ No newline at end of file diff --git a/frappe/patches/v8_0/drop_is_custom_from_docperm.py b/frappe/patches/v8_0/drop_is_custom_from_docperm.py deleted file mode 100644 index 0f17bbef5c..0000000000 --- a/frappe/patches/v8_0/drop_is_custom_from_docperm.py +++ /dev/null @@ -1,8 +0,0 @@ - -import frappe - -def execute(): - frappe.reload_doctype('DocPerm') - if frappe.db.has_column('DocPerm', 'is_custom'): - frappe.db.commit() - frappe.db.sql('alter table `tabDocPerm` drop column is_custom') \ No newline at end of file diff --git a/frappe/patches/v8_0/drop_unwanted_indexes.py b/frappe/patches/v8_0/drop_unwanted_indexes.py deleted file mode 100644 index 655bce1a4b..0000000000 --- a/frappe/patches/v8_0/drop_unwanted_indexes.py +++ /dev/null @@ -1,16 +0,0 @@ -# Copyright (c) 2017, Frappe and Contributors -# License: GNU General Public License v3. See license.txt -# -*- coding: utf-8 -*- - -import frappe - -def execute(): - # communication - unwanted_indexes = ["communication_date_index", "message_id_index", "modified_index", - "creation_index", "reference_owner", "communication_date"] - - for k in unwanted_indexes: - try: - frappe.db.sql("drop index {0} on `tabCommunication`".format(k)) - except: - pass \ No newline at end of file diff --git a/frappe/patches/v8_0/install_new_build_system_requirements.py b/frappe/patches/v8_0/install_new_build_system_requirements.py deleted file mode 100644 index 75ccfa87cd..0000000000 --- a/frappe/patches/v8_0/install_new_build_system_requirements.py +++ /dev/null @@ -1,21 +0,0 @@ -from subprocess import Popen, call, PIPE - -def execute(): - # update nodejs version if brew exists - p = Popen(['which', 'brew'], stdout=PIPE, stderr=PIPE) - output, err = p.communicate() - if output: - call(['brew', 'upgrade', 'node']) - else: - print('Please update your NodeJS version') - - call([ - 'npm', 'install', - 'babel-core', - 'less', - 'chokidar', - 'babel-preset-es2015', - 'babel-preset-es2016', - 'babel-preset-es2017', - 'babel-preset-babili' - ]) \ No newline at end of file diff --git a/frappe/patches/v8_0/newsletter_childtable_migrate.py b/frappe/patches/v8_0/newsletter_childtable_migrate.py deleted file mode 100644 index 67ff5e586f..0000000000 --- a/frappe/patches/v8_0/newsletter_childtable_migrate.py +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright (c) 2017, Frappe and Contributors -# License: GNU General Public License v3. See license.txt - -import frappe - -def execute(): - frappe.reload_doc('email', 'doctype', 'newsletter_email_group') - frappe.reload_doctype('Newsletter') - - if "email_group" not in frappe.db.get_table_columns("Newsletter"): - return - - newsletters = frappe.get_all("Newsletter", fields=["name", "email_group"]) - for newsletter in newsletters: - if newsletter.email_group: - newsletter_doc = frappe.get_doc("Newsletter", newsletter.name) - if not newsletter_doc.get("email_group"): - newsletter_doc.append("email_group", { - "email_group": newsletter.email_group, - }) - newsletter_doc.flags.ignore_validate = True - newsletter_doc.flags.ignore_mandatory = True - newsletter_doc.save() diff --git a/frappe/patches/v8_0/rename_listsettings_to_usersettings.py b/frappe/patches/v8_0/rename_listsettings_to_usersettings.py deleted file mode 100644 index 9545953e34..0000000000 --- a/frappe/patches/v8_0/rename_listsettings_to_usersettings.py +++ /dev/null @@ -1,45 +0,0 @@ - -from frappe.model.utils.user_settings import update_user_settings -import frappe, json - - -def execute(): - if frappe.db.table_exists("__ListSettings"): - for us in frappe.db.sql('''select user, doctype, data from __ListSettings''', as_dict=True): - try: - data = json.loads(us.data) - except: - continue - - if 'List' in data: - continue - - if 'limit' in data: - data['page_length'] = data['limit'] - del data['limit'] - - new_data = dict(List=data) - new_data = json.dumps(new_data) - - frappe.db.sql('''update __ListSettings - set data=%(new_data)s - where user=%(user)s - and doctype=%(doctype)s''', - {'new_data': new_data, 'user': us.user, 'doctype': us.doctype}) - - frappe.db.sql("RENAME TABLE __ListSettings to __UserSettings") - else: - if not frappe.db.table_exists("__UserSettings"): - frappe.db.create_user_settings_table() - - for user in frappe.db.get_all('User', {'user_type': 'System User'}): - defaults = frappe.defaults.get_defaults_for(user.name) - for key, value in defaults.items(): - if key.startswith('_list_settings:'): - doctype = key.replace('_list_settings:', '') - columns = ['`tab{1}`.`{0}`'.format(*c) for c in json.loads(value)] - for col in columns: - if "name as" in col: - columns.remove(col) - - update_user_settings(doctype, {'fields': columns}) \ No newline at end of file diff --git a/frappe/patches/v8_0/rename_page_role_to_has_role.py b/frappe/patches/v8_0/rename_page_role_to_has_role.py deleted file mode 100644 index 49006ea419..0000000000 --- a/frappe/patches/v8_0/rename_page_role_to_has_role.py +++ /dev/null @@ -1,47 +0,0 @@ -# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors -# License: GNU General Public License v3. See license.txt - -import frappe - -def execute(): - if not frappe.db.exists('DocType', 'Has Role'): - frappe.rename_doc('DocType', 'Page Role', 'Has Role') - reload_doc() - set_ref_doctype_roles_to_report() - copy_user_roles_to_has_roles() - remove_doctypes() - -def reload_doc(): - frappe.reload_doc("core", 'doctype', "page") - frappe.reload_doc("core", 'doctype', "report") - frappe.reload_doc("core", 'doctype', "user") - frappe.reload_doc("core", 'doctype', "has_role") - -def set_ref_doctype_roles_to_report(): - for data in frappe.get_all('Report', fields=["name"]): - doc = frappe.get_doc('Report', data.name) - if frappe.db.exists("DocType", doc.ref_doctype): - try: - doc.set_doctype_roles() - for row in doc.roles: - row.db_update() - except: - pass - -def copy_user_roles_to_has_roles(): - if frappe.db.exists('DocType', 'UserRole'): - for data in frappe.get_all('User', fields = ["name"]): - doc = frappe.get_doc('User', data.name) - doc.set('roles',[]) - for args in frappe.get_all('UserRole', fields = ["role"], - filters = {'parent': data.name, 'parenttype': 'User'}): - doc.append('roles', { - 'role': args.role - }) - for role in doc.roles: - role.db_update() - -def remove_doctypes(): - for doctype in ['UserRole', 'Event Role']: - if frappe.db.exists('DocType', doctype): - frappe.delete_doc('DocType', doctype) \ No newline at end of file diff --git a/frappe/patches/v8_0/rename_print_to_printing.py b/frappe/patches/v8_0/rename_print_to_printing.py deleted file mode 100644 index 56889d630e..0000000000 --- a/frappe/patches/v8_0/rename_print_to_printing.py +++ /dev/null @@ -1,13 +0,0 @@ - -import frappe - -def execute(): - if frappe.db.exists('Module Def', 'Print'): - frappe.reload_doc('printing', 'doctype', 'print_format') - frappe.reload_doc('printing', 'doctype', 'print_settings') - frappe.reload_doc('printing', 'doctype', 'print_heading') - frappe.reload_doc('printing', 'doctype', 'letter_head') - frappe.reload_doc('printing', 'page', 'print_format_builder') - frappe.db.sql("""update `tabPrint Format` set module='Printing' where module='Print'""") - - frappe.delete_doc('Module Def', 'Print') \ No newline at end of file diff --git a/frappe/patches/v8_0/set_allow_traceback.py b/frappe/patches/v8_0/set_allow_traceback.py deleted file mode 100644 index bb72e7dde6..0000000000 --- a/frappe/patches/v8_0/set_allow_traceback.py +++ /dev/null @@ -1,6 +0,0 @@ - -import frappe - -def execute(): - frappe.reload_doc('core', 'doctype', 'system_settings') - frappe.db.sql("update `tabSystem Settings` set allow_error_traceback=1") diff --git a/frappe/patches/v8_0/set_currency_field_precision.py b/frappe/patches/v8_0/set_currency_field_precision.py deleted file mode 100644 index 57b12ffdee..0000000000 --- a/frappe/patches/v8_0/set_currency_field_precision.py +++ /dev/null @@ -1,21 +0,0 @@ -# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors -# License: GNU General Public License v3. See license.txt - -import frappe -from frappe.utils import get_number_format_info - -def execute(): - frappe.reload_doc('core', 'doctype', 'system_settings', force=True) - if not frappe.db.get_value("System Settings", None, "currency_precision"): - default_currency = frappe.db.get_default("currency") - number_format = frappe.db.get_value("Currency", default_currency, "number_format", cache=True) \ - or frappe.db.get_default("number_format") - if number_format: - precision = get_number_format_info(number_format)[2] - else: - precision = 2 - - ss = frappe.get_doc("System Settings") - ss.currency_precision = precision - ss.flags.ignore_mandatory = True - ss.save() diff --git a/frappe/patches/v8_0/set_doctype_values_in_custom_role.py b/frappe/patches/v8_0/set_doctype_values_in_custom_role.py deleted file mode 100644 index 50e7eb83e1..0000000000 --- a/frappe/patches/v8_0/set_doctype_values_in_custom_role.py +++ /dev/null @@ -1,12 +0,0 @@ -# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors -# License: GNU General Public License v3. See license.txt - -import frappe - -def execute(): - frappe.reload_doctype('Custom Role') - - # set ref doctype in custom role for reports - frappe.db.sql(""" update `tabCustom Role` set - `tabCustom Role`.ref_doctype = (select ref_doctype from `tabReport` where name = `tabCustom Role`.report) - where `tabCustom Role`.report is not null""") diff --git a/frappe/patches/v8_0/set_user_permission_for_page_and_report.py b/frappe/patches/v8_0/set_user_permission_for_page_and_report.py deleted file mode 100644 index 55789a8301..0000000000 --- a/frappe/patches/v8_0/set_user_permission_for_page_and_report.py +++ /dev/null @@ -1,54 +0,0 @@ -# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors -# License: GNU General Public License v3. See license.txt - -import frappe - -def execute(): - if not frappe.db.exists('DocType', 'Custom Role'): - frappe.reload_doc("core", 'doctype', "custom_role") - set_user_permission_for_page_and_report() - - update_ref_doctype_in_custom_role() - -def update_ref_doctype_in_custom_role(): - frappe.reload_doc("core", 'doctype', "custom_role") - frappe.db.sql("""update `tabCustom Role` - set - ref_doctype = (select ref_doctype from tabReport where name = `tabCustom Role`.report) - where report is not null""") - -def set_user_permission_for_page_and_report(): - make_custom_roles_for_page_and_report() - -def make_custom_roles_for_page_and_report(): - for doctype in ['Page', 'Report']: - for data in get_data(doctype): - doc = frappe.get_doc(doctype, data.name) - roles = get_roles(doctype, data, doc) - make_custom_roles(doctype, doc.name, roles) - -def get_data(doctype): - fields = ["name"] if doctype == 'Page' else ["name", "ref_doctype"] - return frappe.get_all(doctype, fields = fields) - -def get_roles(doctype, data, doc): - roles = [] - if doctype == 'Page': - for d in doc.roles: - if frappe.db.exists('Role', d.role): - roles.append({'role': d.role}) - else: - out = frappe.get_all('Custom DocPerm', fields='distinct role', filters=dict(parent = data.ref_doctype)) - for d in out: - roles.append({'role': d.role}) - return roles - -def make_custom_roles(doctype, name, roles): - field = doctype.lower() - - if roles: - custom_permission = frappe.get_doc({ - 'doctype': 'Custom Role', - field : name, - 'roles' : roles - }).insert() diff --git a/frappe/patches/v8_0/setup_email_inbox.py b/frappe/patches/v8_0/setup_email_inbox.py deleted file mode 100644 index ad99068eb9..0000000000 --- a/frappe/patches/v8_0/setup_email_inbox.py +++ /dev/null @@ -1,26 +0,0 @@ - -import frappe, json -from frappe.core.doctype.user.user import ask_pass_update, setup_user_email_inbox - -def execute(): - """ - depricate email inbox page if exists - remove desktop icon for email inbox page if exists - patch to remove Custom DocPerm for communication - add user inbox child table entry for existing email account in not exists - """ - - if frappe.db.exists("Page", "email_inbox"): - frappe.delete_doc("Page", "email_inbox") - - frappe.db.sql("""update `tabCustom DocPerm` set `write`=0, email=1 where parent='Communication'""") - - frappe.reload_doc("core", "doctype", "user_email") - frappe.reload_doc("email", "doctype", "email_account") - - email_accounts = frappe.get_all("Email Account", filters={"enable_incoming": 1}, - fields=["name", "email_id", "awaiting_password", "enable_outgoing"]) - - for email_account in email_accounts: - setup_user_email_inbox(email_account.get("name"), email_account.get("awaiting_password"), - email_account.get("email_id"), email_account.get("enabled_outgoing")) diff --git a/frappe/patches/v8_0/update_gender_and_salutation.py b/frappe/patches/v8_0/update_gender_and_salutation.py deleted file mode 100644 index 913e0f714b..0000000000 --- a/frappe/patches/v8_0/update_gender_and_salutation.py +++ /dev/null @@ -1,14 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors - -import frappe -from frappe.desk.page.setup_wizard.install_fixtures import update_genders, update_salutations - -def execute(): - frappe.db.set_value("DocType", "Contact", "module", "Contacts") - frappe.db.set_value("DocType", "Address", "module", "Contacts") - frappe.db.set_value("DocType", "Address Template", "module", "Contacts") - frappe.reload_doc('contacts', 'doctype', 'gender') - frappe.reload_doc('contacts', 'doctype', 'salutation') - - update_genders() - update_salutations() \ No newline at end of file diff --git a/frappe/patches/v8_0/update_global_search_table.py b/frappe/patches/v8_0/update_global_search_table.py deleted file mode 100644 index 4d5c8be9cf..0000000000 --- a/frappe/patches/v8_0/update_global_search_table.py +++ /dev/null @@ -1,13 +0,0 @@ - -import frappe - -def execute(): - if not 'published' in frappe.db.get_db_table_columns('__global_search'): - frappe.db.sql('''alter table __global_search - add column `title` varchar(140)''') - - frappe.db.sql('''alter table __global_search - add column `route` varchar(140)''') - - frappe.db.sql('''alter table __global_search - add column `published` int(1) not null default 0''') diff --git a/frappe/patches/v8_0/update_published_in_global_search.py b/frappe/patches/v8_0/update_published_in_global_search.py deleted file mode 100644 index ae86cb8b24..0000000000 --- a/frappe/patches/v8_0/update_published_in_global_search.py +++ /dev/null @@ -1,12 +0,0 @@ - -import frappe - -def execute(): - from frappe.website.router import get_doctypes_with_web_view - from frappe.utils.global_search import rebuild_for_doctype - - for doctype in get_doctypes_with_web_view(): - try: - rebuild_for_doctype(doctype) - except frappe.DoesNotExistError: - pass diff --git a/frappe/patches/v8_0/update_records_in_global_search.py b/frappe/patches/v8_0/update_records_in_global_search.py deleted file mode 100644 index 316f84b2f0..0000000000 --- a/frappe/patches/v8_0/update_records_in_global_search.py +++ /dev/null @@ -1,12 +0,0 @@ - -import frappe -from frappe.utils.global_search import get_doctypes_with_global_search, rebuild_for_doctype -from frappe.utils import update_progress_bar - -def execute(): - frappe.cache().delete_value('doctypes_with_global_search') - doctypes_with_global_search = get_doctypes_with_global_search(with_child_tables=False) - - for i, doctype in enumerate(doctypes_with_global_search): - update_progress_bar("Updating Global Search", i, len(doctypes_with_global_search)) - rebuild_for_doctype(doctype) diff --git a/frappe/patches/v8_1/__init__.py b/frappe/patches/v8_1/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/frappe/patches/v8_1/delete_custom_docperm_if_doctype_not_exists.py b/frappe/patches/v8_1/delete_custom_docperm_if_doctype_not_exists.py deleted file mode 100644 index 510018eb47..0000000000 --- a/frappe/patches/v8_1/delete_custom_docperm_if_doctype_not_exists.py +++ /dev/null @@ -1,7 +0,0 @@ - -import frappe - -def execute(): - frappe.db.sql("""delete from `tabCustom DocPerm` - where parent not in ( select name from `tabDocType` ) - """) diff --git a/frappe/patches/v8_1/enable_allow_error_traceback_in_system_settings.py b/frappe/patches/v8_1/enable_allow_error_traceback_in_system_settings.py deleted file mode 100644 index 513bb274bc..0000000000 --- a/frappe/patches/v8_1/enable_allow_error_traceback_in_system_settings.py +++ /dev/null @@ -1,14 +0,0 @@ -# Copyright (c) 2017, Frappe and Contributors -# License: GNU General Public License v3. See license.txt - -import frappe - -def execute(): - """ enable the allow_enable_traceback property in system settings """ - - frappe.reload_doc("core", "doctype", "system_settings") - doc = frappe.get_doc("System Settings", "System Settings") - doc.allow_error_traceback = 1 - doc.flags.ignore_permissions=True - doc.flags.ignore_mandatory=True - doc.save() \ No newline at end of file diff --git a/frappe/patches/v8_1/update_format_options_in_auto_email_report.py b/frappe/patches/v8_1/update_format_options_in_auto_email_report.py deleted file mode 100644 index 8bea2b7bf5..0000000000 --- a/frappe/patches/v8_1/update_format_options_in_auto_email_report.py +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright (c) 2017, Frappe and Contributors -# License: GNU General Public License v3. See license.txt - -import frappe - -def execute(): - """ change the XLS option as XLSX in the auto email report """ - - frappe.reload_doc("email", "doctype", "auto_email_report") - - auto_email_list = frappe.get_all("Auto Email Report", filters={"format": "XLS"}) - for auto_email in auto_email_list: - frappe.db.set_value("Auto Email Report", auto_email.name, "format", "XLSX") diff --git a/frappe/patches/v8_10/__init__.py b/frappe/patches/v8_10/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/frappe/patches/v8_10/delete_static_web_page_from_global_search.py b/frappe/patches/v8_10/delete_static_web_page_from_global_search.py deleted file mode 100644 index aa6a053412..0000000000 --- a/frappe/patches/v8_10/delete_static_web_page_from_global_search.py +++ /dev/null @@ -1,5 +0,0 @@ - -import frappe - -def execute(): - frappe.db.sql("""delete from `__global_search` where doctype='Static Web Page'"""); \ No newline at end of file diff --git a/frappe/patches/v8_5/__init__.py b/frappe/patches/v8_5/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/frappe/patches/v8_5/delete_email_group_member_with_invalid_emails.py b/frappe/patches/v8_5/delete_email_group_member_with_invalid_emails.py deleted file mode 100644 index 5851e2855b..0000000000 --- a/frappe/patches/v8_5/delete_email_group_member_with_invalid_emails.py +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright (c) 2017, Frappe and Contributors -# License: GNU General Public License v3. See license.txt - -import frappe -from frappe.utils import validate_email_address - -def execute(): - ''' update/delete the email group member with the wrong email ''' - - email_group_members = frappe.get_all("Email Group Member", fields=["name", "email"]) - for member in email_group_members: - validated_email = validate_email_address(member.email) - if (validated_email==member.email): - pass - else: - try: - frappe.db.set_value("Email Group Member", member.name, "email", validated_email) - except Exception: - frappe.delete_doc(doctype="Email Group Member", name=member.name, force=1, ignore_permissions=True) \ No newline at end of file diff --git a/frappe/patches/v8_5/patch_event_colors.py b/frappe/patches/v8_5/patch_event_colors.py deleted file mode 100644 index 3c34f7946b..0000000000 --- a/frappe/patches/v8_5/patch_event_colors.py +++ /dev/null @@ -1,25 +0,0 @@ - -import frappe - -def execute(): - - if not frappe.db.sql("SHOW COLUMNS FROM `tabEvent` LIKE 'color';"): - return - - colors = ['red', 'green', 'blue', 'yellow', 'skyblue', 'orange'] - hex_colors = ['#ffc4c4', '#cef6d1', '#d2d2ff', '#fffacd', '#d2f1ff', '#ffd2c2'] - - def get_hex_for_color(color): - index = colors.index(color) - return hex_colors[index] - - query = ''' - update tabEvent - set color='{hex}' - where color='{color}' - ''' - - for color in colors: - frappe.db.sql(query.format(color=color, hex=get_hex_for_color(color))) - - frappe.db.commit() diff --git a/frappe/patches/v8_7/__init__.py b/frappe/patches/v8_7/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/frappe/patches/v8_x/__init__.py b/frappe/patches/v8_x/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/frappe/patches/v8_x/update_user_permission.py b/frappe/patches/v8_x/update_user_permission.py deleted file mode 100644 index 387751500f..0000000000 --- a/frappe/patches/v8_x/update_user_permission.py +++ /dev/null @@ -1,28 +0,0 @@ - -import frappe - -def execute(): - frappe.reload_doc('core', 'doctype', 'user_permission') - frappe.delete_doc('core', 'page', 'user-permissions') - for perm in frappe.db.sql(""" - select - name, parent, defkey, defvalue - from - tabDefaultValue - where - parent not in ('__default', '__global') - and - substr(defkey,1,1)!='_' - and - parenttype='User Permission' - """, as_dict=True): - if frappe.db.exists(perm.defkey, perm.defvalue) and frappe.db.exists('User', perm.parent): - frappe.get_doc(dict( - doctype='User Permission', - user=perm.parent, - allow=perm.defkey, - for_value=perm.defvalue, - apply_for_all_roles=0, - )).insert(ignore_permissions = True) - - frappe.db.sql('delete from tabDefaultValue where parenttype="User Permission"') diff --git a/frappe/patches/v9_1/__init__.py b/frappe/patches/v9_1/__init__.py deleted file mode 100644 index 8b13789179..0000000000 --- a/frappe/patches/v9_1/__init__.py +++ /dev/null @@ -1 +0,0 @@ - diff --git a/frappe/patches/v9_1/add_sms_sender_name_as_parameters.py b/frappe/patches/v9_1/add_sms_sender_name_as_parameters.py deleted file mode 100644 index f63e86a340..0000000000 --- a/frappe/patches/v9_1/add_sms_sender_name_as_parameters.py +++ /dev/null @@ -1,18 +0,0 @@ -# Copyright (c) 2017, Frappe and Contributors -# License: GNU General Public License v3. See license.txt - -import frappe - -def execute(): - frappe.reload_doc("core", "doctype", "sms_parameter") - sms_sender_name = frappe.db.get_single_value("SMS Settings", "sms_sender_name") - if sms_sender_name: - frappe.reload_doc("core", "doctype", "sms_settings") - sms_settings = frappe.get_doc("SMS Settings") - sms_settings.append("parameters", { - "parameter": "sender_name", - "value": sms_sender_name - }) - sms_settings.flags.ignore_mandatory = True - sms_settings.flags.ignore_permissions = True - sms_settings.save() diff --git a/frappe/patches/v9_1/move_feed_to_activity_log.py b/frappe/patches/v9_1/move_feed_to_activity_log.py deleted file mode 100644 index a549296357..0000000000 --- a/frappe/patches/v9_1/move_feed_to_activity_log.py +++ /dev/null @@ -1,24 +0,0 @@ - -import frappe -from frappe.utils.background_jobs import enqueue - -def execute(): - comm_records_count = frappe.db.count("Communication", {"comment_type": "Updated"}) - if comm_records_count > 100000: - enqueue(method=move_data_from_communication_to_activity_log, queue='short', now=True) - else: - move_data_from_communication_to_activity_log() - -def move_data_from_communication_to_activity_log(): - frappe.reload_doc("core", "doctype", "communication") - frappe.reload_doc("core", "doctype", "activity_log") - - frappe.db.sql("""insert into `tabActivity Log` (name, owner, modified, creation, status, communication_date, - reference_doctype, reference_name, timeline_doctype, timeline_name, link_doctype, link_name, subject, content, user) - select name, owner, modified, creation, status, communication_date, - reference_doctype, reference_name, timeline_doctype, timeline_name, link_doctype, link_name, subject, content, user - from `tabCommunication` - where comment_type = 'Updated'""") - - frappe.db.sql("""delete from `tabCommunication` where comment_type = 'Updated'""") - frappe.delete_doc("DocType", "Authentication Log") \ No newline at end of file diff --git a/frappe/patches/v9_1/resave_domain_settings.py b/frappe/patches/v9_1/resave_domain_settings.py deleted file mode 100644 index 5814871c2e..0000000000 --- a/frappe/patches/v9_1/resave_domain_settings.py +++ /dev/null @@ -1,13 +0,0 @@ - -import frappe - -def execute(): - domain_settings = frappe.get_doc('Domain Settings') - active_domains = [d.domain for d in domain_settings.active_domains] - try: - for d in ('Education', 'Healthcare', 'Hospitality'): - if d in active_domains and frappe.db.exists('Domain', d): - domain = frappe.get_doc('Domain', d) - domain.setup_domain() - except frappe.LinkValidationError: - pass diff --git a/frappe/patches/v9_1/revert_domain_settings.py b/frappe/patches/v9_1/revert_domain_settings.py deleted file mode 100644 index 99c5561d78..0000000000 --- a/frappe/patches/v9_1/revert_domain_settings.py +++ /dev/null @@ -1,11 +0,0 @@ - -import frappe - -def execute(): - domain_settings = frappe.get_doc('Domain Settings') - active_domains = [d.domain for d in domain_settings.active_domains] - - for domain_name in ('Education', 'Healthcare', 'Hospitality'): - if frappe.db.exists('Domain', domain_name) and domain_name not in active_domains: - domain = frappe.get_doc('Domain', domain_name) - domain.remove_domain() \ No newline at end of file diff --git a/frappe/permissions.py b/frappe/permissions.py index c25a7c3947..07b4a2e68f 100644 --- a/frappe/permissions.py +++ b/frappe/permissions.py @@ -308,7 +308,7 @@ def has_controller_permissions(doc, ptype, user=None): return None def get_doctypes_with_read(): - return list(set([p.parent if type(p.parent) == str else p.parent.encode('UTF8') for p in get_valid_perms()])) + return list({p.parent if type(p.parent) == str else p.parent.encode('UTF8') for p in get_valid_perms()}) def get_valid_perms(doctype=None, user=None): '''Get valid permissions for the current user from DocPerm and Custom DocPerm''' diff --git a/frappe/printing/doctype/print_settings/print_settings.json b/frappe/printing/doctype/print_settings/print_settings.json index d64cb4c6d3..31962be050 100644 --- a/frappe/printing/doctype/print_settings/print_settings.json +++ b/frappe/printing/doctype/print_settings/print_settings.json @@ -148,7 +148,7 @@ "label": "Print Style" }, { - "default": "Modern", + "default": "Redesign", "fieldname": "print_style", "fieldtype": "Link", "in_list_view": 1, @@ -183,7 +183,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2020-10-22 23:42:09.471022", + "modified": "2021-02-15 14:16:18.474254", "modified_by": "Administrator", "module": "Printing", "name": "Print Settings", diff --git a/frappe/public/js/frappe/form/controls/text_editor.js b/frappe/public/js/frappe/form/controls/text_editor.js index 059b0f76f8..99e87c5f21 100644 --- a/frappe/public/js/frappe/form/controls/text_editor.js +++ b/frappe/public/js/frappe/form/controls/text_editor.js @@ -1,7 +1,10 @@ import Quill from 'quill'; import ImageResize from 'quill-image-resize'; +import MagicUrl from 'quill-magic-url'; + Quill.register('modules/imageResize', ImageResize); +Quill.register('modules/magicUrl', MagicUrl); const CodeBlockContainer = Quill.import('formats/code-block-container'); CodeBlockContainer.tagName = 'PRE'; Quill.register(CodeBlockContainer, true); @@ -148,7 +151,8 @@ frappe.ui.form.ControlTextEditor = class ControlTextEditor extends frappe.ui.for modules: { toolbar: this.get_toolbar_options(), table: true, - imageResize: {} + imageResize: {}, + magicUrl: true }, theme: 'snow' }; diff --git a/frappe/public/js/frappe/form/dashboard.js b/frappe/public/js/frappe/form/dashboard.js index c1c95d94cf..eb7a6edc5d 100644 --- a/frappe/public/js/frappe/form/dashboard.js +++ b/frappe/public/js/frappe/form/dashboard.js @@ -5,6 +5,7 @@ frappe.ui.form.Dashboard = class FormDashboard { constructor(opts) { $.extend(this, opts); this.setup_dashboard_sections(); + this.set_open_count = frappe.utils.throttle(this.set_open_count, 500); } setup_dashboard_sections() { diff --git a/frappe/public/js/frappe/form/footer/form_timeline.js b/frappe/public/js/frappe/form/footer/form_timeline.js index ab83ed2f71..115a62e098 100644 --- a/frappe/public/js/frappe/form/footer/form_timeline.js +++ b/frappe/public/js/frappe/form/footer/form_timeline.js @@ -190,6 +190,7 @@ class FormTimeline extends BaseTimeline { } doc.owner = doc.sender; doc.user_full_name = doc.sender_full_name; + doc.content = frappe.dom.remove_script_and_style(doc.content); let communication_content = $(frappe.render_template('timeline_message_box', { doc })); if (allow_reply) { this.setup_reply(communication_content, doc); @@ -248,6 +249,7 @@ class FormTimeline extends BaseTimeline { } get_comment_timeline_content(doc) { + doc.content = frappe.dom.remove_script_and_style(doc.content); const comment_content = $(frappe.render_template('timeline_message_box', { doc })); this.setup_comment_actions(comment_content, doc); return comment_content; diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js index 35ebf9274d..a24c6ab0d6 100644 --- a/frappe/public/js/frappe/form/form.js +++ b/frappe/public/js/frappe/form/form.js @@ -1607,7 +1607,9 @@ frappe.ui.form.Form = class FrappeForm { } show_tour(on_finish) { - if (!Array.isArray(frappe.tour[this.doctype])) { + const tour_info = frappe.tour[this.doctype]; + + if (!Array.isArray(tour_info)) { return; } @@ -1619,23 +1621,29 @@ frappe.ui.form.Form = class FrappeForm { keyboardControl: true, nextBtnText: 'Next', prevBtnText: 'Previous', - opacity: 0.25, - onNext: () => { - if (!driver.hasNextStep()) { - on_finish && on_finish(); - } - } + opacity: 0.25 }); this.layout.sections.forEach(section => section.collapse(false)); - let steps = frappe.tour[this.doctype].map(step => { + let steps = tour_info.map(step => { let field = this.get_docfield(step.fieldname); return { element: `.frappe-control[data-fieldname='${step.fieldname}']`, popover: { title: step.title || field.label, - description: step.description + description: step.description, + position: step.position || 'bottom' + }, + onNext: () => { + const next_condition_satisfied = this.layout.evaluate_depends_on_value(step.next_step_condition || true); + if (!next_condition_satisfied) { + driver.preventMove(); + } + + if (!driver.hasNextStep()) { + on_finish && on_finish(); + } } }; }); diff --git a/frappe/public/js/frappe/form/formatters.js b/frappe/public/js/frappe/form/formatters.js index 89c34ed80c..b9a838688d 100644 --- a/frappe/public/js/frappe/form/formatters.js +++ b/frappe/public/js/frappe/form/formatters.js @@ -221,9 +221,13 @@ frappe.form.formatters = { Tag: function(value) { var html = ""; $.each((value || "").split(","), function(i, v) { - if(v) html+= ''+v +''; + if (v) html += ` + + ${v} + `; }); return html; }, @@ -310,6 +314,7 @@ frappe.form.get_formatter = function(fieldtype) { frappe.format = function(value, df, options, doc) { if(!df) df = {"fieldtype":"Data"}; + if (df.fieldname == '_user_tags') df.fieldtype = 'Tag'; var fieldtype = df.fieldtype || "Data"; // format Dynamic Link as a Link diff --git a/frappe/public/js/frappe/form/grid.js b/frappe/public/js/frappe/form/grid.js index a77791d0a2..ebc3fa19f5 100644 --- a/frappe/public/js/frappe/form/grid.js +++ b/frappe/public/js/frappe/form/grid.js @@ -210,9 +210,9 @@ export default class Grid { delete_all_rows() { frappe.confirm(__("Are you sure you want to delete all rows?"), () => { - this.frm.doc[this.df.fieldname] = []; - $(this.parent).find('.rows').empty(); - this.grid_rows = []; + this.grid_rows.forEach(row => { + row.remove(); + }); this.frm.script_manager.trigger(this.df.fieldname + "_delete", this.doctype); this.wrapper.find('.grid-heading-row .grid-row-check:checked:first').prop('checked', 0); @@ -236,6 +236,10 @@ export default class Grid { } refresh_remove_rows_button() { + if (this.df.cannot_delete_rows) { + return; + } + this.remove_rows_button.toggleClass('hidden', this.wrapper.find('.grid-body .grid-row-check:checked:first').length ? false : true); this.remove_all_rows_button.toggleClass('hidden', diff --git a/frappe/public/js/frappe/form/grid_row.js b/frappe/public/js/frappe/form/grid_row.js index 33db00dede..8c51314066 100644 --- a/frappe/public/js/frappe/form/grid_row.js +++ b/frappe/public/js/frappe/form/grid_row.js @@ -569,6 +569,9 @@ export default class GridRow { .find('.grid-insert-row-below, .grid-insert-row, .grid-duplicate-row, .grid-append-row') .toggle(!cannot_add_rows); + this.wrapper.find('.grid-delete-row') + .toggle(!(this.grid.df && this.grid.df.cannot_delete_rows)); + frappe.dom.freeze("", "dark"); if (cur_frm) cur_frm.cur_grid = this; this.wrapper.addClass("grid-row-open"); diff --git a/frappe/public/js/frappe/ui/filters/filter_list.js b/frappe/public/js/frappe/ui/filters/filter_list.js index 611ab024bf..72312d7f13 100644 --- a/frappe/public/js/frappe/ui/filters/filter_list.js +++ b/frappe/public/js/frappe/ui/filters/filter_list.js @@ -323,9 +323,12 @@ frappe.ui.FilterGroup = class { } add_filters_to_filter_group(filters) { - filters.forEach((filter) => { - this.add_filter(filter[0], filter[1], filter[2], filter[3]); - }); + if (filters.length) { + this.toggle_empty_filters(false); + filters.forEach((filter) => { + this.add_filter(filter[0], filter[1], filter[2], filter[3]); + }); + } } add(filters, refresh = true) { diff --git a/frappe/public/js/frappe/ui/group_by/group_by.js b/frappe/public/js/frappe/ui/group_by/group_by.js index 3ebf9c9d3d..692d675c62 100644 --- a/frappe/public/js/frappe/ui/group_by/group_by.js +++ b/frappe/public/js/frappe/ui/group_by/group_by.js @@ -381,10 +381,11 @@ frappe.ui.GroupBy = class { this.group_by_fields = {}; this.all_fields = {}; - let fields = this.report_view.meta.fields.filter((f) => + const fields = this.report_view.meta.fields.filter((f) => ['Select', 'Link', 'Data', 'Int', 'Check'].includes(f.fieldtype) ); - this.group_by_fields[this.doctype] = fields; + const tag_field = {fieldname: '_user_tags', fieldtype: 'Data', label: __('Tags')}; + this.group_by_fields[this.doctype] = fields.concat(tag_field); this.all_fields[this.doctype] = this.report_view.meta.fields; const standard_fields_filter = (df) => diff --git a/frappe/public/js/frappe/ui/messages.js b/frappe/public/js/frappe/ui/messages.js index 2e8ba7d206..067fed233c 100644 --- a/frappe/public/js/frappe/ui/messages.js +++ b/frappe/public/js/frappe/ui/messages.js @@ -26,13 +26,13 @@ frappe.throw = function(msg) { frappe.confirm = function(message, confirm_action, reject_action) { var d = new frappe.ui.Dialog({ - title: __("Confirm"), - primary_action_label: __("Yes"), + title: __("Confirm", null, "Title of confirmation dialog"), + primary_action_label: __("Yes", null, "Approve confirmation dialog"), primary_action: () => { confirm_action && confirm_action(); d.hide(); }, - secondary_action_label: __("No"), + secondary_action_label: __("No", null, "Dismiss confirmation dialog"), secondary_action: () => d.hide(), }); @@ -88,9 +88,9 @@ frappe.prompt = function(fields, callback, title, primary_label) { if(!$.isArray(fields)) fields = [fields]; var d = new frappe.ui.Dialog({ fields: fields, - title: title || __("Enter Value"), + title: title || __("Enter Value", null, "Title of prompt dialog"), }); - d.set_primary_action(primary_label || __("Submit"), function() { + d.set_primary_action(primary_label || __("Submit", null, "Primary action of prompt dialog"), function() { var values = d.get_values(); if(!values) { return; diff --git a/frappe/public/js/frappe/ui/page.js b/frappe/public/js/frappe/ui/page.js index e740718ef9..22fdf476b8 100644 --- a/frappe/public/js/frappe/ui/page.js +++ b/frappe/public/js/frappe/ui/page.js @@ -377,11 +377,12 @@ frappe.ui.Page = class Page { }); } - add_actions_menu_item(label, click, standard) { + add_actions_menu_item(label, click, standard, shortcut) { return this.add_dropdown_item({ label, click, standard, + shortcut, parent: this.actions, show_parent: false }); @@ -409,6 +410,9 @@ frappe.ui.Page = class Page { parent.parent().removeClass("hide"); } + let $link = this.is_in_group_button_dropdown(parent, 'li > a.grey-link', label); + if ($link) return $link; + let $li; let $icon = ``; @@ -440,9 +444,8 @@ frappe.ui.Page = class Page { `); } - var $link = $li.find("a").on("click", click); - if (this.is_in_group_button_dropdown(parent, 'li > a.grey-link', label)) return; + $link = $li.find("a").on("click", click); if (standard) { $li.appendTo(parent); @@ -508,7 +511,7 @@ frappe.ui.Page = class Page { let item = $(this).html(); return $(item).attr('data-label') === label; }); - return result.length > 0; + return result.length > 0 && result; } clear_btn_group(parent) { diff --git a/frappe/public/js/frappe/views/reports/report_view.js b/frappe/public/js/frappe/views/reports/report_view.js index b29b6b87e6..6a324f6034 100644 --- a/frappe/public/js/frappe/views/reports/report_view.js +++ b/frappe/public/js/frappe/views/reports/report_view.js @@ -410,7 +410,7 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView { x_fields.push({ label: col.content, fieldname: col.id, - value: col.id, + value: col.id, }); // numeric values in y @@ -1024,8 +1024,12 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView { return docfield.fieldtype === 'Date' ? 'right' : 'left'; })(); + let id = fieldname; + // child table column - const id = doctype !== this.doctype ? `${doctype}:${fieldname}` : fieldname; + if (doctype !== this.doctype && fieldname !== '_aggregate_column') { + id = `${doctype}:${fieldname}`; + } let width = (docfield ? cint(docfield.width) : null) || null; if (this.report_doc) { diff --git a/frappe/public/js/frappe/widgets/widget_dialog.js b/frappe/public/js/frappe/widgets/widget_dialog.js index eefb78c29a..3f5a4acd73 100644 --- a/frappe/public/js/frappe/widgets/widget_dialog.js +++ b/frappe/public/js/frappe/widgets/widget_dialog.js @@ -271,18 +271,19 @@ class ShortcutDialog extends WidgetDialog { } process_data(data) { - let stats_filter = {}; if (this.dialog.get_value("type") == "DocType" && this.filter_group) { let filters = this.filter_group.get_filters(); + let stats_filter = null; if (filters.length) { + stats_filter = {}; filters.forEach((arr) => { stats_filter[arr[1]] = [arr[2], arr[3]]; }); - - data.stats_filter = JSON.stringify(stats_filter); + stats_filter = JSON.stringify(stats_filter); } + data.stats_filter = stats_filter; } data.label = data.label diff --git a/frappe/public/scss/common/controls.scss b/frappe/public/scss/common/controls.scss index 83fc4461d6..c939c6de39 100644 --- a/frappe/public/scss/common/controls.scss +++ b/frappe/public/scss/common/controls.scss @@ -241,6 +241,7 @@ textarea.form-control { // rating .rating { + cursor: pointer; --star-fill: var(--gray-300); .star-hover { --star-fill: var(--yellow-100); @@ -248,6 +249,24 @@ textarea.form-control { .star-click { --star-fill: var(--yellow-300); } + + .rating-box { + background-color: var(--gray-300); + border-radius: 5px; + font-size: 14px; + text-align: center; + padding: 2px; + cursor: pointer; + width: 25px; + height: 25px; + margin: 4px 2px; + } + .rating-hover { + background-color: var(--yellow-100); + } + .rating-click { + background-color: var(--yellow-300); + } } .frappe-control .control-value { diff --git a/frappe/public/scss/common/grid.scss b/frappe/public/scss/common/grid.scss index aac949b1bf..57d0583b35 100644 --- a/frappe/public/scss/common/grid.scss +++ b/frappe/public/scss/common/grid.scss @@ -53,7 +53,12 @@ display: none; } +.form-grid .grid-heading-row .template-row { + margin-left: 20px; +} + .form-grid .template-row { + width: calc(100% - 30px); padding: 8px 15px; } diff --git a/frappe/public/scss/common/quill.scss b/frappe/public/scss/common/quill.scss index 6f6e09dc70..12706d6b7f 100644 --- a/frappe/public/scss/common/quill.scss +++ b/frappe/public/scss/common/quill.scss @@ -176,6 +176,7 @@ } .ql-editor.read-mode { + height: unset; padding: 0; .mention { --user-mention-bg-color: var(--control-bg); diff --git a/frappe/public/scss/website/blog.scss b/frappe/public/scss/website/blog.scss index 9918b490c5..ea82efed21 100644 --- a/frappe/public/scss/website/blog.scss +++ b/frappe/public/scss/website/blog.scss @@ -14,6 +14,10 @@ position: relative; width: 100%; + .card { + border: 1px solid var(--border-color) + } + .card-body { display: flex; flex-direction: column; diff --git a/frappe/public/scss/website/markdown.scss b/frappe/public/scss/website/markdown.scss index c5f44d20d8..6f009df393 100644 --- a/frappe/public/scss/website/markdown.scss +++ b/frappe/public/scss/website/markdown.scss @@ -48,7 +48,6 @@ $font-sizes-mobile: ( } li { - text-indent: 0.25rem; padding-top: 1px; padding-bottom: 1px; } diff --git a/frappe/search/website_search.py b/frappe/search/website_search.py index 452ea2a427..49bdade936 100644 --- a/frappe/search/website_search.py +++ b/frappe/search/website_search.py @@ -9,7 +9,7 @@ from whoosh.fields import ID, TEXT, Schema import frappe from frappe.search.full_text_search import FullTextSearch from frappe.utils import set_request, update_progress_bar -from frappe.website.render import render_page +from frappe.website.serve import get_response_content INDEX_NAME = "web_routes" @@ -61,7 +61,7 @@ class WebsiteSearch(FullTextSearch): try: set_request(method="GET", path=route) - content = render_page(route) + content = get_response_content(route) soup = BeautifulSoup(content, "html.parser") page_content = soup.find(class_="page_content") text_content = page_content.text if page_content else "" diff --git a/frappe/sessions.py b/frappe/sessions.py index 1bc78448e7..4d922d6769 100644 --- a/frappe/sessions.py +++ b/frappe/sessions.py @@ -167,7 +167,8 @@ def get_csrf_token(): def generate_csrf_token(): frappe.local.session.data.csrf_token = frappe.generate_hash() - frappe.local.session_obj.update(force=True) + if not frappe.flags.in_test: + frappe.local.session_obj.update(force=True) class Session: def __init__(self, user, resume=False, full_name=None, user_type=None): diff --git a/frappe/templates/includes/comments/comments.html b/frappe/templates/includes/comments/comments.html index c490bedd72..935fa5367e 100644 --- a/frappe/templates/includes/comments/comments.html +++ b/frappe/templates/includes/comments/comments.html @@ -49,8 +49,10 @@ {% endif %} \ No newline at end of file diff --git a/frappe/templates/includes/feedback/feedback.py b/frappe/templates/includes/feedback/feedback.py new file mode 100644 index 0000000000..1830a3e09e --- /dev/null +++ b/frappe/templates/includes/feedback/feedback.py @@ -0,0 +1,63 @@ +# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# MIT License. See license.txt +from __future__ import unicode_literals + +import frappe + +from frappe import _ + +@frappe.whitelist(allow_guest=True) +def add_feedback(reference_doctype, reference_name, rating, feedback, feedback_email): + doc = frappe.get_doc(reference_doctype, reference_name) + if doc.disable_feedback == 1: + return + + doc = frappe.new_doc('Feedback') + doc.reference_doctype = reference_doctype + doc.reference_name = reference_name + doc.rating = rating + doc.feedback = feedback + doc.email = feedback_email + doc.save(ignore_permissions=True) + + subject = _('New Feedback on {0}: {1}').format(reference_doctype, reference_name) + send_mail(doc, subject) + return doc + +@frappe.whitelist() +def update_feedback(reference_doctype, reference_name, rating, feedback, feedback_email): + doc = frappe.get_doc(reference_doctype, reference_name) + if doc.disable_feedback == 1: + return + + filters = { + "email": feedback_email, + "reference_doctype": reference_doctype, + "reference_name": reference_name + } + d = frappe.get_all('Feedback', filters=filters, limit=1) + doc = frappe.get_doc('Feedback', d[0].name) + doc.rating = rating + doc.feedback = feedback + doc.save(ignore_permissions=True) + + subject = _('Feedback updated on {0}: {1}').format(reference_doctype, reference_name) + send_mail(doc, subject) + return doc + +def send_mail(feedback, subject): + doc = frappe.get_doc(feedback.reference_doctype, feedback.reference_name) + + message = ("

{0} ({1})

".format(feedback.feedback, feedback.rating) + + "

{2}

".format(frappe.utils.get_request_site_address(), + feedback.name, + _("View Feedback"))) + + # notify creator + frappe.sendmail( + recipients=frappe.db.get_value('User', doc.owner, 'email') or doc.owner, + subject=subject, + message=message, + reference_doctype=doc.doctype, + reference_name=doc.name + ) diff --git a/frappe/templates/includes/full_index.html b/frappe/templates/includes/full_index.html index a7443c482a..eb8fb322f6 100644 --- a/frappe/templates/includes/full_index.html +++ b/frappe/templates/includes/full_index.html @@ -3,11 +3,6 @@ {% for item in children_map[route] %}
  • {{ item.title }} - {# - {% if children_map[item.route] %} - {{ make_item_list(item.route, children_map) }} - {% endif %} - #}
  • {% endfor %} diff --git a/frappe/templates/test/_test_base.html b/frappe/templates/test/_test_base.html index a0b1a83c97..17caf8df1b 100644 --- a/frappe/templates/test/_test_base.html +++ b/frappe/templates/test/_test_base.html @@ -1,8 +1,20 @@ - + + {%- block style %} + {% if colocated_css -%} + + {%- endif %} + {%- endblock -%} + + {% include "templates/includes/breadcrumbs.html" %}

    This is for testing

    {% block content %}{% endblock %} + {%- block script %} + {% if colocated_js -%} + + {%- endif %} + {%- endblock %} diff --git a/frappe/test_runner.py b/frappe/test_runner.py index 1f99e55fb8..0c30fbbd00 100644 --- a/frappe/test_runner.py +++ b/frappe/test_runner.py @@ -175,6 +175,7 @@ def run_tests_for_module(module, verbose=False, tests=(), profile=False, junit_x for doctype in module.test_dependencies: make_test_records(doctype, verbose=verbose) + frappe.db.commit() return _run_unittest(module, verbose=verbose, tests=tests, profile=profile, junit_xml_output=junit_xml_output) def _run_unittest(modules, verbose=False, tests=(), profile=False, junit_xml_output=False): diff --git a/frappe/tests/test_api.py b/frappe/tests/test_api.py index 7e77aab779..29939fea1c 100644 --- a/frappe/tests/test_api.py +++ b/frappe/tests/test_api.py @@ -39,6 +39,11 @@ class TestResourceAPI(unittest.TestCase): for name in self.GENERATED_DOCUMENTS: frappe.delete_doc_if_exists(self.DOCTYPE, name) + def setUp(self): + # commit to ensure consistency in session (postgres CI randomly fails) + if frappe.conf.db_type == "postgres": + frappe.db.commit() + @property def sid(self): if not getattr(self, "_sid", None): diff --git a/frappe/tests/test_commands.py b/frappe/tests/test_commands.py index b6cd0b575c..07bdf8791e 100644 --- a/frappe/tests/test_commands.py +++ b/frappe/tests/test_commands.py @@ -80,7 +80,7 @@ def exists_in_backup(doctypes, file): ) with gzip.open(file, "rb") as f: content = f.read().decode("utf8") - return all([predicate.format(doctype).lower() in content.lower() for doctype in doctypes]) + return all(predicate.format(doctype).lower() in content.lower() for doctype in doctypes) class BaseTestCommands(unittest.TestCase): @@ -355,12 +355,12 @@ class TestCommands(BaseTestCommands): # test 2: bare functionality for single site self.execute("bench --site {site} list-apps") self.assertEqual(self.returncode, 0) - list_apps = set([ + list_apps = set( _x.split()[0] for _x in self.stdout.split("\n") - ]) + ) doctype = frappe.get_single("Installed Applications").installed_applications if doctype: - installed_apps = set([x.app_name for x in doctype]) + installed_apps = set(x.app_name for x in doctype) else: installed_apps = set(frappe.get_installed_apps()) self.assertSetEqual(list_apps, installed_apps) diff --git a/frappe/tests/test_db_query.py b/frappe/tests/test_db_query.py index 42ebd05b67..89975b46d6 100644 --- a/frappe/tests/test_db_query.py +++ b/frappe/tests/test_db_query.py @@ -21,6 +21,18 @@ class TestReportview(unittest.TestCase): def test_basic(self): self.assertTrue({"name":"DocType"} in DatabaseQuery("DocType").execute(limit_page_length=None)) + def test_extract_tables(self): + db_query = DatabaseQuery("DocType") + add_custom_field("DocType", 'test_tab_field', 'Data') + + db_query.fields = ["tabNote.creation", "test_tab_field", "tabDocType.test_tab_field"] + db_query.extract_tables() + self.assertIn("`tabNote`", db_query.tables) + self.assertIn("`tabDocType`", db_query.tables) + self.assertNotIn("test_tab_field", db_query.tables) + + clear_custom_fields("DocType") + def test_build_match_conditions(self): clear_user_permissions_for_doctype('Blog Post', 'test2@example.com') diff --git a/frappe/tests/test_recorder.py b/frappe/tests/test_recorder.py index 08dbde0144..d9386ca25b 100644 --- a/frappe/tests/test_recorder.py +++ b/frappe/tests/test_recorder.py @@ -7,7 +7,7 @@ import unittest import frappe import frappe.recorder from frappe.utils import set_request -from frappe.website.render import render_page +from frappe.website.serve import get_response_content import sqlparse @@ -121,5 +121,5 @@ class TestRecorder(unittest.TestCase): self.assertEqual(call['exact_copies'], query[1]) def test_error_page_rendering(self): - content = render_page("error") + content = get_response_content("error") self.assertIn("Error", content) diff --git a/frappe/tests/test_website.py b/frappe/tests/test_website.py index 52ddc5ef71..6f265d9b94 100644 --- a/frappe/tests/test_website.py +++ b/frappe/tests/test_website.py @@ -1,38 +1,41 @@ import unittest import frappe -from frappe.website import render -from frappe.website.utils import get_home_page from frappe.utils import set_request +from frappe.website.serve import get_response, get_response_content +from frappe.website.utils import (build_response, clear_website_cache, get_home_page) class TestWebsite(unittest.TestCase): + def setUp(self): + frappe.set_user('Guest') - def test_home_page_for_role(self): - frappe.delete_doc_if_exists('User', 'test-user-for-home-page@example.com') - frappe.delete_doc_if_exists('Role', 'home-page-test') - frappe.delete_doc_if_exists('Web Page', 'home-page-test') + def tearDown(self): + frappe.set_user('Administrator') + + def test_home_page(self): + frappe.set_user('Administrator') + # test home page via role user = frappe.get_doc(dict( doctype='User', email='test-user-for-home-page@example.com', - first_name='test')).insert() + first_name='test')).insert(ignore_if_duplicate=True) role = frappe.get_doc(dict( doctype = 'Role', role_name = 'home-page-test', desk_access = 0, - home_page = '/home-page-test' - )).insert() + )).insert(ignore_if_duplicate=True) user.add_roles(role.name) user.save() + frappe.db.set_value('Role', 'home-page-test', 'home_page', 'home-page-test') frappe.set_user('test-user-for-home-page@example.com') self.assertEqual(get_home_page(), 'home-page-test') frappe.set_user('Administrator') - role.home_page = '' - role.save() + frappe.db.set_value('Role', 'home-page-test', 'home_page', '') # home page via portal settings frappe.db.set_value('Portal Settings', None, 'default_portal_home', 'test-portal-home') @@ -41,10 +44,45 @@ class TestWebsite(unittest.TestCase): frappe.cache().hdel('home_page', frappe.session.user) self.assertEqual(get_home_page(), 'test-portal-home') - def test_page_load(self): + frappe.db.set_value("Portal Settings", None, "default_portal_home", '') + clear_website_cache() + + # home page via website settings + frappe.db.set_value("Website Settings", None, "home_page", 'contact') + self.assertEqual(get_home_page(), 'contact') + + frappe.db.set_value("Website Settings", None, "home_page", None) + clear_website_cache() + + # fallback homepage + self.assertEqual(get_home_page(), 'me') + + # fallback homepage for guest frappe.set_user('Guest') + self.assertEqual(get_home_page(), 'login') + frappe.set_user('Administrator') + + # test homepage via hooks + clear_website_cache() + set_home_page_hook('get_website_user_home_page', 'frappe.www._test._test_home_page.get_website_user_home_page') + self.assertEqual(get_home_page(), '_test/_test_folder') + + clear_website_cache() + set_home_page_hook('website_user_home_page', 'login') + self.assertEqual(get_home_page(), 'login') + + clear_website_cache() + set_home_page_hook('home_page', 'about') + self.assertEqual(get_home_page(), 'about') + + clear_website_cache() + set_home_page_hook('role_home_page', {'home-page-test': 'home-page-test'}) + self.assertEqual(get_home_page(), 'home-page-test') + + + def test_page_load(self): set_request(method='POST', path='login') - response = render.render() + response = get_response() self.assertEqual(response.status_code, 200) @@ -52,14 +90,52 @@ class TestWebsite(unittest.TestCase): self.assertTrue('// login.js' in html) self.assertTrue('' in html) + + def test_static_page(self): + set_request(method='GET', path='/_test/static-file-test.png') + response = get_response() + self.assertEqual(response.status_code, 200) + + def test_error_page(self): + set_request(method='GET', path='/_test/problematic_page') + response = get_response() + self.assertEqual(response.status_code, 500) + + def test_login(self): + set_request(method='GET', path='/login') + response = get_response() + self.assertEqual(response.status_code, 200) + + html = frappe.safe_decode(response.get_data()) + + self.assertTrue('// login.js' in html) + self.assertTrue('' in html) + + def test_app(self): frappe.set_user('Administrator') + set_request(method='GET', path='/app') + response = get_response() + self.assertEqual(response.status_code, 200) + + html = frappe.safe_decode(response.get_data()) + self.assertTrue('window.app = true;' in html) + frappe.local.session_obj = None + + def test_not_found(self): + set_request(method='GET', path='/_test/missing') + response = get_response() + self.assertEqual(response.status_code, 404) + def test_redirect(self): import frappe.hooks + frappe.set_user('Administrator') + frappe.hooks.website_redirects = [ dict(source=r'/testfrom', target=r'://testto1'), dict(source=r'/testfromregex.*', target=r'://testto2'), - dict(source=r'/testsub/(.*)', target=r'://testto3/\1') + dict(source=r'/testsub/(.*)', target=r'://testto3/\1'), + dict(source=r'/courses/course\?course=(.*)', target=r'/courses/\1', match_with_query_string=True), ] website_settings = frappe.get_doc('Website Settings') @@ -69,32 +145,82 @@ class TestWebsite(unittest.TestCase): }) website_settings.save() - frappe.cache().delete_key('app_hooks') - frappe.cache().delete_key('website_redirects') - set_request(method='GET', path='/testfrom') - response = render.render() + response = get_response() self.assertEqual(response.status_code, 301) self.assertEqual(response.headers.get('Location'), r'://testto1') set_request(method='GET', path='/testfromregex/test') - response = render.render() + response = get_response() self.assertEqual(response.status_code, 301) self.assertEqual(response.headers.get('Location'), r'://testto2') set_request(method='GET', path='/testsub/me') - response = render.render() + response = get_response() self.assertEqual(response.status_code, 301) self.assertEqual(response.headers.get('Location'), r'://testto3/me') set_request(method='GET', path='/test404') - response = render.render() + response = get_response() self.assertEqual(response.status_code, 404) set_request(method='GET', path='/testsource') - response = render.render() + response = get_response() self.assertEqual(response.status_code, 301) self.assertEqual(response.headers.get('Location'), '/testtarget') + set_request(method='GET', path='/courses/course?course=data') + response = get_response() + self.assertEqual(response.status_code, 301) + self.assertEqual(response.headers.get('Location'), '/courses/data') + delattr(frappe.hooks, 'website_redirects') frappe.cache().delete_key('app_hooks') + + def test_custom_page_renderer(self): + import frappe.hooks + frappe.hooks.page_renderer = ['frappe.tests.test_website.CustomPageRenderer'] + frappe.cache().delete_key('app_hooks') + set_request(method='GET', path='/custom') + response = get_response() + self.assertEqual(response.status_code, 3984) + + set_request(method='GET', path='/new') + content = get_response_content() + self.assertIn("
    Custom Page Response
    ", content) + + set_request(method='GET', path='/random') + response = get_response() + self.assertEqual(response.status_code, 404) + + delattr(frappe.hooks, 'page_renderer') + frappe.cache().delete_key('app_hooks') + + def test_printview_page(self): + content = get_response_content('/Language/en') + self.assertIn(' {% endif %} + {% if not disable_feedback %} +
    + {% include 'templates/includes/feedback/feedback.html' %} +
    + {% endif %} ", content) + self.assertIn("background-color: var(--bg-color);", content) + + def test_breadcrumbs(self): + content = get_response_content('/_test/_test_folder/_test_page') + self.assertIn('Test TOC', content) + self.assertIn(' Test Page', content) + + content = get_response_content('/_test/_test_folder/index') + self.assertIn(' Test', content) + self.assertIn('Test TOC', content) + + def test_downloadable_file(self): + pass diff --git a/frappe/website/doctype/web_page/web_page.py b/frappe/website/doctype/web_page/web_page.py index cea14d3bbe..05f5cac546 100644 --- a/frappe/website/doctype/web_page/web_page.py +++ b/frappe/website/doctype/web_page/web_page.py @@ -3,20 +3,17 @@ import re -import requests -import requests.exceptions from jinja2.exceptions import TemplateSyntaxError import frappe from frappe import _ -from frappe.utils import get_datetime, now, strip_html, quoted +from frappe.utils import get_datetime, now, quoted, strip_html from frappe.utils.jinja import render_template -from frappe.website.doctype.website_slideshow.website_slideshow import get_slideshow -from frappe.website.router import resolve_route -from frappe.website.utils import (extract_title, find_first_image, get_comment_list, - get_html_content_based_on_type) -from frappe.website.website_generator import WebsiteGenerator from frappe.utils.safe_exec import safe_exec +from frappe.website.doctype.website_slideshow.website_slideshow import get_slideshow +from frappe.website.utils import (extract_title, find_first_image, + get_comment_list, get_html_content_based_on_type) +from frappe.website.website_generator import WebsiteGenerator class WebPage(WebsiteGenerator): @@ -53,6 +50,7 @@ class WebPage(WebsiteGenerator): if self.enable_comments: context.comment_list = get_comment_list(self.doctype, self.name) + context.guest_allowed = True context.update({ "style": self.css or "", @@ -183,32 +181,6 @@ def check_publish_status(): frappe.db.set_value("Web Page", page.name, "published", 1) - -def check_broken_links(): - cnt = 0 - for p in frappe.db.sql("select name, main_section from `tabWeb Page`", as_dict=True): - for link in re.findall('href=["\']([^"\']*)["\']', p.main_section): - if link.startswith("http"): - try: - res = requests.get(link) - except requests.exceptions.SSLError: - res = frappe._dict({"status_code": "SSL Error"}) - except requests.exceptions.ConnectionError: - res = frappe._dict({"status_code": "Connection Error"}) - - if res.status_code!=200: - print("[{0}] {1}: {2}".format(res.status_code, p.name, link)) - cnt += 1 - else: - link = link[1:] # remove leading / - link = link.split("#")[0] - - if not resolve_route(link): - print(p.name + ":" + link) - cnt += 1 - - print("{0} links broken".format(cnt)) - def get_web_blocks_html(blocks): '''Converts a list of blocks into Raw HTML and extracts out their scripts for deduplication''' diff --git a/frappe/website/doctype/web_template/test_web_template.py b/frappe/website/doctype/web_template/test_web_template.py index 45e35c4626..2f2dbdc40a 100644 --- a/frappe/website/doctype/web_template/test_web_template.py +++ b/frappe/website/doctype/web_template/test_web_template.py @@ -5,8 +5,7 @@ import frappe import unittest from bs4 import BeautifulSoup from frappe.utils import set_request -from frappe.website.render import render - +from frappe.website.serve import get_response class TestWebTemplate(unittest.TestCase): def test_render_web_template_with_values(self): @@ -34,7 +33,7 @@ class TestWebTemplate(unittest.TestCase): self.create_web_page() set_request(method="GET", path="test-web-template") - response = render() + response = get_response() self.assertEqual(response.status_code, 200) @@ -56,7 +55,7 @@ class TestWebTemplate(unittest.TestCase): frappe.conf.developer_mode = 1 set_request(method="GET", path="test-web-template") - response = render() + response = get_response() self.assertEqual(response.status_code, 200) html = frappe.safe_decode(response.get_data()) diff --git a/frappe/website/doctype/web_template/web_template.py b/frappe/website/doctype/web_template/web_template.py index 891a0c3679..6905680523 100644 --- a/frappe/website/doctype/web_template/web_template.py +++ b/frappe/website/doctype/web_template/web_template.py @@ -7,7 +7,7 @@ from shutil import rmtree import frappe from frappe.model.document import Document -from frappe.website.render import clear_cache +from frappe.website.utils import clear_cache from frappe import _ from frappe.modules.export_file import ( write_document_file, diff --git a/frappe/website/doctype/website_route_meta/test_website_route_meta.py b/frappe/website/doctype/website_route_meta/test_website_route_meta.py index 1f927abafc..c55dcce1ca 100644 --- a/frappe/website/doctype/website_route_meta/test_website_route_meta.py +++ b/frappe/website/doctype/website_route_meta/test_website_route_meta.py @@ -4,7 +4,7 @@ import frappe import unittest from frappe.utils import set_request -from frappe.website.render import render +from frappe.website.serve import get_response test_dependencies = ['Blog Post'] class TestWebsiteRouteMeta(unittest.TestCase): @@ -29,7 +29,7 @@ class TestWebsiteRouteMeta(unittest.TestCase): # set request on this route set_request(path=blog.route) - response = render() + response = get_response() self.assertTrue(response.status_code, 200) diff --git a/frappe/website/doctype/website_script/website_script.py b/frappe/website/doctype/website_script/website_script.py index 111beeaf2a..6ec10291cd 100644 --- a/frappe/website/doctype/website_script/website_script.py +++ b/frappe/website/doctype/website_script/website_script.py @@ -13,5 +13,5 @@ class WebsiteScript(Document): """clear cache""" frappe.clear_cache(user = 'Guest') - from frappe.website.render import clear_cache + from frappe.website.utils import clear_cache clear_cache() \ No newline at end of file diff --git a/frappe/website/doctype/website_settings/website_settings.py b/frappe/website/doctype/website_settings/website_settings.py index 03a1aecc5f..1ccd106c38 100644 --- a/frappe/website/doctype/website_settings/website_settings.py +++ b/frappe/website/doctype/website_settings/website_settings.py @@ -1,14 +1,13 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # MIT License. See license.txt +from urllib.parse import quote import frappe from frappe import _ -from frappe.utils import get_request_site_address, encode -from frappe.model.document import Document -from urllib.parse import quote -from frappe.website.router import resolve_route -from frappe.website.doctype.website_theme.website_theme import add_website_theme from frappe.integrations.doctype.google_settings.google_settings import get_auth_url +from frappe.model.document import Document +from frappe.utils import encode, get_request_site_address +from frappe.website.doctype.website_theme.website_theme import add_website_theme INDEXING_SCOPES = "https://www.googleapis.com/auth/indexing" @@ -22,7 +21,8 @@ class WebsiteSettings(Document): def validate_home_page(self): if frappe.flags.in_install: return - if self.home_page and not resolve_route(self.home_page): + from frappe.website.path_resolver import PathResolver + if self.home_page and not PathResolver(self.home_page).is_valid_path(): frappe.msgprint(_("Invalid Home Page") + " (Standard pages - index, login, products, blog, about, contact)") self.home_page = '' @@ -68,7 +68,7 @@ class WebsiteSettings(Document): # clear web cache (for menus!) frappe.clear_cache(user = 'Guest') - from frappe.website.render import clear_cache + from frappe.website.utils import clear_cache clear_cache() # clears role based home pages diff --git a/frappe/website/doctype/website_slideshow/website_slideshow.py b/frappe/website/doctype/website_slideshow/website_slideshow.py index d31adbf986..8566475b33 100644 --- a/frappe/website/doctype/website_slideshow/website_slideshow.py +++ b/frappe/website/doctype/website_slideshow/website_slideshow.py @@ -14,7 +14,7 @@ class WebsiteSlideshow(Document): def on_update(self): # a slide show can be in use and any change in it should get reflected - from frappe.website.render import clear_cache + from frappe.website.utils import clear_cache clear_cache() def validate_images(self): @@ -22,7 +22,7 @@ class WebsiteSlideshow(Document): files = map(lambda row: row.image, self.slideshow_items) if files: result = frappe.get_all("File", filters={ "file_url":("in", list(files)) }, fields="is_private") - if any([file.is_private for file in result]): + if any(file.is_private for file in result): frappe.throw(_("All Images attached to Website Slideshow should be public")) def get_slideshow(doc): diff --git a/frappe/website/page/__init__.py b/frappe/website/page/__init__.py deleted file mode 100644 index 8b13789179..0000000000 --- a/frappe/website/page/__init__.py +++ /dev/null @@ -1 +0,0 @@ - diff --git a/frappe/website/page_renderers/base_renderer.py b/frappe/website/page_renderers/base_renderer.py new file mode 100644 index 0000000000..14d4448e6c --- /dev/null +++ b/frappe/website/page_renderers/base_renderer.py @@ -0,0 +1,25 @@ +import frappe +from frappe.website.utils import build_response + +class BaseRenderer(object): + def __init__(self, path=None, http_status_code=None): + self.headers = None + self.http_status_code = http_status_code or 200 + if not path: + path = frappe.local.request.path + self.path = path.strip('/ ') + self.basepath = '' + self.basename = '' + self.name = '' + self.route = '' + self.file_dir = None + + def can_render(self): + raise NotImplementedError + + def render(self): + raise NotImplementedError + + def build_response(self, data, http_status_code=None, headers=None): + return build_response(self.path, data, http_status_code or self.http_status_code, headers or self.headers) + diff --git a/frappe/website/page_renderers/base_template_page.py b/frappe/website/page_renderers/base_template_page.py new file mode 100644 index 0000000000..7802e6e7f6 --- /dev/null +++ b/frappe/website/page_renderers/base_template_page.py @@ -0,0 +1,70 @@ +import frappe +from frappe.website.doctype.website_settings.website_settings import get_website_settings +from frappe.website.page_renderers.base_renderer import BaseRenderer +from frappe.website.website_components.metatags import MetaTags + + +class BaseTemplatePage(BaseRenderer): + def __init__(self, path, http_status_code=None): + super().__init__(path=path, http_status_code=http_status_code) + self.template_path = '' + + def init_context(self): + self.context = frappe._dict() + self.context.update(get_website_settings()) + self.context.update(frappe.local.conf.get("website_context") or {}) + + def add_csrf_token(self, html): + if frappe.local.session: + csrf_token = frappe.local.session.data.csrf_token + return html.replace("", + f'') + + return html + + def post_process_context(self): + self.tags = MetaTags(self.path, self.context).tags + self.context.metatags = self.tags + self.set_base_template_if_missing() + self.set_title_with_prefix() + self.update_website_context() + # context sends us a new template path + self.template_path = self.context.template or self.template_path + self.context._context_dict = self.context + self.set_missing_values() + + def set_base_template_if_missing(self): + if not self.context.base_template_path: + app_base = frappe.get_hooks("base_template") + self.context.base_template_path = app_base[-1] if app_base else "templates/base.html" + + def set_title_with_prefix(self): + if (self.context.title_prefix and self.context.title + and not self.context.title.startswith(self.context.title_prefix)): + self.context.title = '{0} - {1}'.format(self.context.title_prefix, self.context.title) + + def set_missing_values(self): + # set using frappe.respond_as_web_page + if hasattr(frappe.local, 'response') and frappe.local.response.get('context'): + self.context.update(frappe.local.response.context) + + # to be able to inspect the context dict + # Use the macro "inspect" from macros.html + self.context.canonical = frappe.utils.get_url(frappe.utils.escape_html(self.path)) + + if "url_prefix" not in self.context: + self.context.url_prefix = "" + + if self.context.url_prefix and self.context.url_prefix[-1]!='/': + self.context.url_prefix += '/' + + self.context.path = self.path + self.context.pathname = frappe.local.path if hasattr(frappe, 'local') else self.path + + def update_website_context(self): + # apply context from hooks + update_website_context = frappe.get_hooks('update_website_context') + for method in update_website_context: + values = frappe.get_attr(method)(self.context) + if values: + self.context.update(values) diff --git a/frappe/website/page_renderers/document_page.py b/frappe/website/page_renderers/document_page.py new file mode 100644 index 0000000000..f1741c681f --- /dev/null +++ b/frappe/website/page_renderers/document_page.py @@ -0,0 +1,87 @@ +import frappe +from frappe.model.document import get_controller +from frappe.website.page_renderers.base_template_page import BaseTemplatePage +from frappe.website.utils import build_response +from frappe.website.router import (get_doctypes_with_web_view, + get_page_info_from_web_page_with_dynamic_routes) + + +class DocumentPage(BaseTemplatePage): + def can_render(self): + ''' + Find a document with matching `route` from all doctypes with `has_web_view`=1 + ''' + if self.search_in_doctypes_with_web_view(): + return True + + if self.search_web_page_dynamic_routes(): + return True + + return False + + def search_in_doctypes_with_web_view(self): + for doctype in get_doctypes_with_web_view(): + filters = dict(route=self.path) + meta = frappe.get_meta(doctype) + condition_field = self.get_condition_field(meta) + + if condition_field: + filters[condition_field] = 1 + + try: + self.docname = frappe.db.get_value(doctype, filters, 'name') + if self.docname: + self.doctype = doctype + return True + except Exception as e: + if not frappe.db.is_missing_column(e): + raise e + + def search_web_page_dynamic_routes(self): + d = get_page_info_from_web_page_with_dynamic_routes(self.path) + if d: + self.doctype = 'Web Page' + self.docname = d.name + return True + else: + return False + + def render(self): + self.doc = frappe.get_doc(self.doctype, self.docname) + self.init_context() + self.update_context() + self.post_process_context() + html = frappe.get_template(self.template_path).render(self.context) + html = self.add_csrf_token(html) + + return build_response(self.path, html, self.http_status_code or 200, self.headers) + + def update_context(self): + self.context.doc = self.doc + self.context.update(self.context.doc.as_dict()) + self.context.update(self.context.doc.get_page_info()) + + self.template_path = self.context.template or self.template_path + + if not self.template_path: + self.template_path = self.context.doc.meta.get_web_template() + + if hasattr(self.doc, "get_context"): + ret = self.doc.get_context(self.context) + + if ret: + self.context.update(ret) + + for prop in ("no_cache", "sitemap"): + if prop not in self.context: + self.context[prop] = getattr(self.doc, prop, False) + + def get_condition_field(self, meta): + condition_field = None + if meta.is_published_field: + condition_field = meta.is_published_field + elif not meta.custom: + controller = get_controller(meta.name) + condition_field = controller.website.condition_field + + return condition_field diff --git a/frappe/website/page_renderers/error_page.py b/frappe/website/page_renderers/error_page.py new file mode 100644 index 0000000000..3501c77765 --- /dev/null +++ b/frappe/website/page_renderers/error_page.py @@ -0,0 +1,10 @@ +from frappe.website.page_renderers.template_page import TemplatePage + +class ErrorPage(TemplatePage): + def __init__(self, path=None, http_status_code=None, exception=None): + path = 'error' + super().__init__(path=path, http_status_code=http_status_code) + self.http_status_code = getattr(exception, 'http_status_code', None) or http_status_code or 500 + + def can_render(self): + return True diff --git a/frappe/website/page_renderers/list_page.py b/frappe/website/page_renderers/list_page.py new file mode 100644 index 0000000000..61c781ea14 --- /dev/null +++ b/frappe/website/page_renderers/list_page.py @@ -0,0 +1,11 @@ +import frappe +from frappe.website.page_renderers.template_page import TemplatePage + +class ListPage(TemplatePage): + def can_render(self): + return frappe.db.exists('DocType', self.path, True) + + def render(self): + frappe.local.form_dict.doctype = self.path + self.set_standard_path('list') + return super().render() diff --git a/frappe/website/page_renderers/not_found_page.py b/frappe/website/page_renderers/not_found_page.py new file mode 100644 index 0000000000..af510fecfc --- /dev/null +++ b/frappe/website/page_renderers/not_found_page.py @@ -0,0 +1,34 @@ +import os +from urllib.parse import urlparse + +import frappe +from frappe.website.page_renderers.template_page import TemplatePage +from frappe.website.utils import can_cache + +HOMEPAGE_PATHS = ('/', '/index', 'index') + +class NotFoundPage(TemplatePage): + def __init__(self, path, http_status_code): + self.request_path = path + self.request_url = frappe.local.request.url if hasattr(frappe.local, 'request') else '' + path = '404' + http_status_code = 404 + super().__init__(path=path, http_status_code=http_status_code) + + def can_render(self): + return True + + def render(self): + if self.can_cache_404(): + frappe.cache().hset('website_404', self.request_url, True) + return super().render() + + def can_cache_404(self): + # do not cache 404 for custom homepages + return can_cache() and self.request_url and not self.is_custom_home_page() + + def is_custom_home_page(self): + url_parts = urlparse(self.request_url) + request_url = os.path.splitext(url_parts.path)[0] + request_path = os.path.splitext(self.request_path)[0] + return request_url in HOMEPAGE_PATHS and request_path not in HOMEPAGE_PATHS diff --git a/frappe/website/page_renderers/not_permitted_page.py b/frappe/website/page_renderers/not_permitted_page.py new file mode 100644 index 0000000000..e69299f5c5 --- /dev/null +++ b/frappe/website/page_renderers/not_permitted_page.py @@ -0,0 +1,24 @@ +import frappe +from frappe import _ +from frappe.website.page_renderers.template_page import TemplatePage +from frappe.utils import cstr + +class NotPermittedPage(TemplatePage): + def __init__(self, path=None, http_status_code=None, exception=''): + frappe.local.message = cstr(exception) + super().__init__(path=path, http_status_code=http_status_code) + self.http_status_code = 403 + + def can_render(self): + return True + + def render(self): + frappe.local.message_title = _("Not Permitted") + frappe.local.response['context'] = dict( + indicator_color = 'red', + primary_action = '/login', + primary_label = _('Login'), + fullpage=True + ) + self.set_standard_path('message') + return super().render() diff --git a/frappe/website/page_renderers/print_page.py b/frappe/website/page_renderers/print_page.py new file mode 100644 index 0000000000..05d4026e2b --- /dev/null +++ b/frappe/website/page_renderers/print_page.py @@ -0,0 +1,23 @@ +import frappe +from frappe.website.page_renderers.template_page import TemplatePage + +class PrintPage(TemplatePage): + ''' + default path returns a printable object (based on permission) + /Quotation/Q-0001 + ''' + def can_render(self): + parts = self.path.split('/', 1) + if len(parts)==2: + if (frappe.db.exists('DocType', parts[0], True) + and frappe.db.exists(parts[0], parts[1], True)): + return True + + return False + + def render(self): + parts = self.path.split('/', 1) + frappe.form_dict.doctype = parts[0] + frappe.form_dict.name = parts[1] + self.set_standard_path('printview') + return super().render() diff --git a/frappe/website/page_renderers/redirect_page.py b/frappe/website/page_renderers/redirect_page.py new file mode 100644 index 0000000000..2049c375e8 --- /dev/null +++ b/frappe/website/page_renderers/redirect_page.py @@ -0,0 +1,16 @@ +import frappe +from frappe.website.utils import build_response + +class RedirectPage(object): + def __init__(self, path, http_status_code=301): + self.path = path + self.http_status_code = http_status_code + + def can_render(self): + return True + + def render(self): + return build_response(self.path, "", 301, { + "Location": frappe.flags.redirect_location or (frappe.local.response or {}).get('location'), + "Cache-Control": "no-store, no-cache, must-revalidate" + }) diff --git a/frappe/website/page_renderers/static_page.py b/frappe/website/page_renderers/static_page.py new file mode 100644 index 0000000000..632e9b4302 --- /dev/null +++ b/frappe/website/page_renderers/static_page.py @@ -0,0 +1,41 @@ +import mimetypes +import os + +from werkzeug.wrappers import Response +from werkzeug.wsgi import wrap_file + +import frappe +from frappe.website.page_renderers.base_renderer import BaseRenderer + +UNSUPPORTED_STATIC_PAGE_TYPES = ('html', 'md', 'js', 'xml', 'css', 'txt', 'py', 'json') + +class StaticPage(BaseRenderer): + def __init__(self, path, http_status_code=None): + super().__init__(path=path, http_status_code=http_status_code) + self.set_file_path() + + def set_file_path(self): + self.file_path = '' + if not self.is_valid_file_path(): + return + for app in frappe.get_installed_apps(): + file_path = frappe.get_app_path(app, 'www') + '/' + self.path + if os.path.isfile(file_path): + self.file_path = file_path + + def can_render(self): + return self.is_valid_file_path() and self.file_path + + def is_valid_file_path(self): + if ('.' not in self.path): + return False + extension = self.path.rsplit('.', 1)[-1] + if extension in UNSUPPORTED_STATIC_PAGE_TYPES: + return False + return True + + def render(self): + f = open(self.file_path, 'rb') + response = Response(wrap_file(frappe.local.request.environ, f), direct_passthrough=True) + response.mimetype = mimetypes.guess_type(self.file_path)[0] or 'application/octet-stream' + return response diff --git a/frappe/website/page_renderers/template_page.py b/frappe/website/page_renderers/template_page.py new file mode 100644 index 0000000000..5e6e57e33a --- /dev/null +++ b/frappe/website/page_renderers/template_page.py @@ -0,0 +1,281 @@ +import io +import os +import click + +import frappe +from frappe.website.router import get_page_info +from frappe.website.page_renderers.base_template_page import BaseTemplatePage +from frappe.website.router import get_base_template +from frappe.website.utils import (extract_comment_tag, extract_title, get_next_link, + get_toc, get_frontmatter, cache_html, get_sidebar_items, build_response) + +WEBPAGE_PY_MODULE_PROPERTIES = ("base_template_path", "template", "no_cache", "sitemap", "condition_field") + +COMMENT_PROPERTY_KEY_VALUE_MAP = { + "no-breadcrumbs": ("no_breadcrumbs", 1), + "show-sidebar": ("show_sidebar", 1), + "add-breadcrumbs": ("add_breadcrumbs", 1), + "no-header": ("no_header", 1), + "add-next-prev-links": ("add_next_prev_links", 1), + "no-cache": ("no_cache", 1), + "no-sitemap": ("sitemap", 0), + "sitemap": ("sitemap", 1) +} + +class TemplatePage(BaseTemplatePage): + def __init__(self, path, http_status_code=None): + super().__init__(path=path, http_status_code=http_status_code) + self.set_template_path() + + def set_template_path(self): + ''' + Searches for file matching the path in the /www + and /templates/pages folders and sets path if match is found + ''' + folders = get_start_folders() + for app in frappe.get_installed_apps(frappe_last=True): + app_path = frappe.get_app_path(app) + + for dirname in folders: + search_path = os.path.join(app_path, dirname, self.path) + for file_path in self.get_index_path_options(search_path): + if os.path.isfile(file_path): + self.app = app + self.app_path = app_path + self.file_dir = dirname + self.basename = os.path.splitext(file_path)[0] + self.template_path = os.path.relpath(file_path, self.app_path) + self.basepath = os.path.dirname(file_path) + self.filename = os.path.basename(file_path) + self.name = os.path.splitext(self.filename)[0] + return + + def can_render(self): + return hasattr(self, 'template_path') and bool(self.template_path) + + @staticmethod + def get_index_path_options(search_path): + return (frappe.as_unicode(f'{search_path}{d}') for d in ('', '.html', '.md', '/index.html', '/index.md')) + + def render(self): + return build_response(self.path, self.get_html(), self.http_status_code, self.headers) + + @cache_html + def get_html(self): + # context object should be separate from self for security + # because it will be accessed via the user defined template + self.init_context() + + self.set_pymodule() + self.setup_template() + self.update_context() + self.post_process_context() + + html = self.render_template() + html = self.update_toc(html) + html = self.add_csrf_token(html) + + return html + + def post_process_context(self): + self.set_user_info() + self.add_sidebar_and_breadcrumbs() + super(TemplatePage, self).post_process_context() + + def add_sidebar_and_breadcrumbs(self): + if self.basepath: + self.context.sidebar_items = get_sidebar_items(self.context.website_sidebar, self.basepath) + + if self.context.add_breadcrumbs and not self.context.parents: + parent_path = os.path.dirname(self.path) + if self.path.endswith('index'): + # in case of index page move one directory up for parent path + parent_path = os.path.dirname(parent_path) + + for parent_file_path in self.get_index_path_options(parent_path): + parent_file_path = os.path.join(self.app_path, self.file_dir, parent_file_path) + if os.path.isfile(parent_file_path): + parent_page_context = get_page_info(parent_file_path, self.app, self.file_dir) + if parent_page_context: + self.context.parents = [dict(route=os.path.dirname(self.path), title=parent_page_context.title)] + break + + def set_pymodule(self): + ''' + A template may have a python module with a `get_context` method along with it in the + same folder. Also the hyphens will be coverted to underscore for python module names. + This method sets the pymodule_name if it exists. + ''' + template_basepath = os.path.splitext(self.template_path)[0] + self.pymodule_name = None + + # replace - with _ in the internal modules names + self.pymodule_path = os.path.join(os.path.dirname(template_basepath), os.path.basename(template_basepath.replace("-", "_")) + ".py") + + if os.path.exists(os.path.join(self.app_path, self.pymodule_path)): + self.pymodule_name = self.app + "." + self.pymodule_path.replace(os.path.sep, ".")[:-3] + + def setup_template(self): + '''Setup template source, frontmatter and markdown conversion''' + self.source = self.get_raw_template() + self.extract_frontmatter() + self.convert_from_markdown() + + def update_context(self): + self.set_page_properties() + self.set_properties_from_source() + self.load_colocated_files() + self.context.build_version = frappe.utils.get_build_version() + + if self.pymodule_name: + self.pymodule = frappe.get_module(self.pymodule_name) + self.set_pymodule_properties() + + data = self.run_pymodule_method('get_context') + # some methods may return a "context" object + if data: + self.context.update(data) + # TODO: self.context.children = self.run_pymodule_method('get_children') + + self.context.developer_mode = frappe.conf.developer_mode + if self.context.http_status_code: + self.http_status_code = self.context.http_status_code + + def set_pymodule_properties(self): + for prop in WEBPAGE_PY_MODULE_PROPERTIES: + if hasattr(self.pymodule, prop): + self.context[prop] = getattr(self.pymodule, prop) + + def set_page_properties(self): + self.context.base_template = self.context.base_template \ + or get_base_template(self.path) \ + or 'templates/web.html' + self.context.basepath = self.basepath + self.context.basename = self.basename + self.context.name = self.name + self.context.path = self.path + self.context.route = self.path + self.context.template = self.template_path + + def set_properties_from_source(self): + if not self.source: + return + context = self.context + if not context.title: + context.title = extract_title(self.source, self.path) + + base_template = extract_comment_tag(self.source, 'base_template') + if base_template: + context.base_template = base_template + + if (context.base_template + and "{%- extends" not in self.source + and "{% extends" not in self.source + and "" not in self.source): + self.source = '''{{% extends "{0}" %}} + {{% block page_content %}}{1}{{% endblock %}}'''.format(context.base_template, self.source) + + self.set_properties_via_comments() + + def set_properties_via_comments(self): + for comment, (context_key, value) in COMMENT_PROPERTY_KEY_VALUE_MAP.items(): + comment_tag = f"" + if comment_tag in self.source: + self.context[context_key] = value + click.echo(f'\n⚠️ DEPRECATION WARNING: {comment_tag} will be deprecated on 2021-12-31.') + click.echo(f'Please remove it from {self.template_path} in {self.app}') + + def run_pymodule_method(self, method): + if hasattr(self.pymodule, method): + try: + return getattr(self.pymodule, method)(self.context) + except (frappe.PermissionError, frappe.DoesNotExistError, frappe.Redirect): + raise + except Exception: + if not frappe.flags.in_migrate: + frappe.errprint(frappe.utils.get_traceback()) + + def render_template(self): + if self.source: + html = frappe.render_template(self.source, self.context) + elif self.template_path: + if self.path.endswith('min.js'): + html = self.get_raw_template() # static + else: + html = frappe.get_template(self.template_path).render(self.context) + + return html + + def extends_template(self): + return (self.template_path.endswith(('.html', '.md')) + and ('{%- extends' in self.source + or '{% extends' in self.source)) + + def get_raw_template(self): + return frappe.get_jloader().get_source(frappe.get_jenv(), self.template_path)[0] + + def load_colocated_files(self): + '''load co-located css/js files with the same name''' + js_path = self.basename + '.js' + if os.path.exists(js_path) and '{% block script %}' not in self.source: + self.context.colocated_js = self.get_colocated_file(js_path) + + css_path = self.basename + '.css' + if os.path.exists(css_path) and '{% block style %}' not in self.source: + self.context.colocated_css = self.get_colocated_file(css_path) + + def get_colocated_file(self, path): + with io.open(path, 'r', encoding = 'utf-8') as f: + return f.read() + + def extract_frontmatter(self): + if not self.template_path.endswith(('.md', '.html')): + return + + try: + # values will be used to update self + res = get_frontmatter(self.source) + if res['attributes']: + self.context.update(res['attributes']) + self.source = res['body'] + except Exception: + pass + + def convert_from_markdown(self): + if self.template_path.endswith('.md'): + self.source = frappe.utils.md_to_html(self.source) + self.context.page_toc_html = self.source.toc_html + + if not self.context.show_sidebar: + self.source = '
    ' + self.source + '
    ' + + def update_toc(self, html): + if '{index}' in html: + html = html.replace('{index}', get_toc(self.path)) + + if '{next}' in html: + html = html.replace('{next}', get_next_link(self.path)) + + return html + + def set_standard_path(self, path): + self.app = 'frappe' + self.app_path = frappe.get_app_path('frappe') + self.path = path + self.template_path = 'www/{path}.html'.format(path=path) + + def set_missing_values(self): + super().set_missing_values() + # for backward compatibility + self.context.docs_base_url = '/docs' + + def set_user_info(self): + from frappe.utils.user import get_fullname_and_avatar + info = get_fullname_and_avatar(frappe.session.user) + self.context["fullname"] = info.fullname + self.context["user_image"] = info.avatar + self.context["user"] = info.name + + +def get_start_folders(): + return frappe.local.flags.web_pages_folders or ('www', 'templates/pages') diff --git a/frappe/website/page_renderers/web_form.py b/frappe/website/page_renderers/web_form.py new file mode 100644 index 0000000000..786aeef3d1 --- /dev/null +++ b/frappe/website/page_renderers/web_form.py @@ -0,0 +1,10 @@ +from frappe.website.page_renderers.document_page import DocumentPage +import frappe + +class WebFormPage(DocumentPage): + def can_render(self): + webform_name = frappe.db.exists("Web Form", {'route': self.path}, cache=True) + if webform_name: + self.doctype = 'Web Form' + self.docname = webform_name + return bool(webform_name) diff --git a/frappe/website/path_resolver.py b/frappe/website/path_resolver.py new file mode 100644 index 0000000000..bedd9f19ae --- /dev/null +++ b/frappe/website/path_resolver.py @@ -0,0 +1,155 @@ +import re +import click + +from werkzeug.routing import Rule + +import frappe +from frappe.website.page_renderers.document_page import DocumentPage +from frappe.website.page_renderers.list_page import ListPage +from frappe.website.page_renderers.not_found_page import NotFoundPage +from frappe.website.page_renderers.print_page import PrintPage +from frappe.website.page_renderers.redirect_page import RedirectPage +from frappe.website.page_renderers.static_page import StaticPage +from frappe.website.page_renderers.template_page import TemplatePage +from frappe.website.page_renderers.web_form import WebFormPage +from frappe.website.router import evaluate_dynamic_routes +from frappe.website.utils import can_cache, get_home_page + + +class PathResolver(): + def __init__(self, path): + self.path = path.strip('/ ') + + def resolve(self): + '''Returns endpoint and a renderer instance that can render the endpoint''' + request = frappe._dict() + if hasattr(frappe.local, 'request'): + request = frappe.local.request or request + + # check if the request url is in 404 list + if request.url and can_cache() and frappe.cache().hget('website_404', request.url): + return self.path, NotFoundPage(self.path) + + try: + resolve_redirect(self.path, request.query_string) + except frappe.Redirect: + return frappe.flags.redirect_location, RedirectPage(self.path) + + endpoint = resolve_path(self.path) + custom_renderers = self.get_custom_page_renderers() + renderers = custom_renderers + [StaticPage, WebFormPage, TemplatePage, ListPage, DocumentPage, PrintPage, NotFoundPage] + + for renderer in renderers: + renderer_instance = renderer(endpoint, 200) + if renderer_instance.can_render(): + return endpoint, renderer_instance + + return endpoint, NotFoundPage(endpoint) + + def is_valid_path(self): + _endpoint, renderer_instance = self.resolve() + return not isinstance(renderer_instance, NotFoundPage) + + @staticmethod + def get_custom_page_renderers(): + custom_renderers = [] + for renderer_path in frappe.get_hooks('page_renderer') or []: + try: + renderer = frappe.get_attr(renderer_path) + if not hasattr(renderer, 'can_render'): + click.echo(f'{renderer.__name__} does not have can_render method') + continue + if not hasattr(renderer, 'render'): + click.echo(f'{renderer.__name__} does not have render method') + continue + + custom_renderers.append(renderer) + + except Exception: + click.echo(f'Failed to load page renderer. Import path: {renderer_path}') + + return custom_renderers + + + +def resolve_redirect(path, query_string=None): + ''' + Resolve redirects from hooks + + Example: + + website_redirect = [ + # absolute location + {"source": "/from", "target": "https://mysite/from"}, + + # relative location + {"source": "/from", "target": "/main"}, + + # use regex + {"source": r"/from/(.*)", "target": r"/main/\1"} + # use r as a string prefix if you use regex groups or want to escape any string literal + ] + ''' + redirects = frappe.get_hooks('website_redirects') + redirects += frappe.db.get_all('Website Route Redirect', ['source', 'target']) + + if not redirects: return + + redirect_to = frappe.cache().hget('website_redirects', path) + + if redirect_to: + frappe.flags.redirect_location = redirect_to + raise frappe.Redirect + + for rule in redirects: + pattern = rule['source'].strip('/ ') + '$' + path_to_match = path + if rule.get('match_with_query_string'): + path_to_match = path + '?' + frappe.safe_decode(query_string) + + if re.match(pattern, path_to_match): + redirect_to = re.sub(pattern, rule['target'], path_to_match) + frappe.flags.redirect_location = redirect_to + frappe.cache().hset('website_redirects', path_to_match, redirect_to) + raise frappe.Redirect + + +def resolve_path(path): + if not path: + path = "index" + + if path.endswith('.html'): + path = path[:-5] + + if path == "index": + path = get_home_page() + + frappe.local.path = path + + if path != "index": + path = resolve_from_map(path) + + return path + +def resolve_from_map(path): + '''transform dynamic route to a static one from hooks and route defined in doctype''' + rules = [Rule(r["from_route"], endpoint=r["to_route"], defaults=r.get("defaults")) + for r in get_website_rules()] + + return evaluate_dynamic_routes(rules, path) or path + +def get_website_rules(): + '''Get website route rules from hooks and DocType route''' + def _get(): + rules = frappe.get_hooks("website_route_rules") + for d in frappe.get_all('DocType', 'name, route', dict(has_web_view=1)): + if d.route: + rules.append(dict(from_route = '/' + d.route.strip('/'), to_route=d.name)) + + return rules + + if frappe.local.dev_server: + # dont cache in development + return _get() + + return frappe.cache().get_value('website_route_rules', _get) diff --git a/frappe/website/purifycss.py b/frappe/website/purifycss.py deleted file mode 100644 index bac68b881b..0000000000 --- a/frappe/website/purifycss.py +++ /dev/null @@ -1,42 +0,0 @@ -''' -Check for unused CSS Classes - -sUpdate source and target apps below and run from CLI - - bench --site [sitename] execute frappe.website.purifycss.purify.css - -''' - -import frappe, re, os - -source = frappe.get_app_path('frappe_theme', 'public', 'less', 'frappe_theme.less') -target_apps = ['erpnext_com', 'frappe_io', 'translator', 'chart_of_accounts_builder', 'frappe_theme'] - -def purifycss(): - with open(source, 'r') as f: - src = f.read() - - classes = [] - for line in src.splitlines(): - line = line.strip() - if not line: - continue - if line[0]=='@': - continue - classes.extend(re.findall('\.([^0-9][^ :&.{,(]*)', line)) - - classes = list(set(classes)) - - for app in target_apps: - for basepath, folders, files in os.walk(frappe.get_app_path(app)): - for fname in files: - if fname.endswith('.html') or fname.endswith('.md'): - #print 'checking {0}...'.format(fname) - with open(os.path.join(basepath, fname), 'r') as f: - src = f.read() - for c in classes: - if c in src: - classes.remove(c) - - for c in sorted(classes): - print(c) diff --git a/frappe/website/redirect.py b/frappe/website/redirect.py deleted file mode 100644 index 3194895d95..0000000000 --- a/frappe/website/redirect.py +++ /dev/null @@ -1,39 +0,0 @@ -import re, frappe - -def resolve_redirect(path): - ''' - Resolve redirects from hooks - - Example: - - website_redirect = [ - # absolute location - {"source": "/from", "target": "https://mysite/from"}, - - # relative location - {"source": "/from", "target": "/main"}, - - # use regex - {"source": r"/from/(.*)", "target": r"/main/\1"} - # use r as a string prefix if you use regex groups or want to escape any string literal - ] - ''' - redirects = frappe.get_hooks('website_redirects') - redirects += frappe.db.get_all('Website Route Redirect', ['source', 'target']) - - if not redirects: return - - redirect_to = frappe.cache().hget('website_redirects', path) - - if redirect_to: - frappe.flags.redirect_location = redirect_to - raise frappe.Redirect - - for rule in redirects: - pattern = rule['source'].strip('/ ') + '$' - if re.match(pattern, path): - redirect_to = re.sub(pattern, rule['target'], path) - frappe.flags.redirect_location = redirect_to - frappe.cache().hset('website_redirects', path, redirect_to) - raise frappe.Redirect - diff --git a/frappe/website/render.py b/frappe/website/render.py deleted file mode 100644 index 2b4a5e2dab..0000000000 --- a/frappe/website/render.py +++ /dev/null @@ -1,369 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# MIT License. See license.txt - -import frappe -from frappe import _ -import frappe.sessions -from frappe.utils import cstr -import os, mimetypes, json -import re - -from werkzeug.wrappers import Response -from werkzeug.routing import Rule -from werkzeug.wsgi import wrap_file - -from frappe.website.context import get_context -from frappe.website.redirect import resolve_redirect -from frappe.website.utils import (get_home_page, can_cache, delete_page_cache, - get_toc, get_next_link) -from frappe.website.router import clear_sitemap, evaluate_dynamic_routes -from frappe.translate import guess_language - -class PageNotFoundError(Exception): pass - -def render(path=None, http_status_code=None): - """render html page""" - if not path: - path = frappe.local.request.path - - try: - path = path.strip('/ ') - raise_if_disabled(path) - resolve_redirect(path) - path = resolve_path(path) - data = None - - # if in list of already known 404s, send it - if can_cache() and frappe.cache().hget('website_404', frappe.request.url): - data = render_page('404') - http_status_code = 404 - elif is_static_file(path): - return get_static_file_response() - elif is_web_form(path): - data = render_web_form(path) - else: - try: - data = render_page_by_language(path) - except frappe.PageDoesNotExistError: - doctype, name = get_doctype_from_path(path) - if doctype and name: - path = "printview" - frappe.local.form_dict.doctype = doctype - frappe.local.form_dict.name = name - elif doctype: - path = "list" - frappe.local.form_dict.doctype = doctype - else: - # 404s are expensive, cache them! - frappe.cache().hset('website_404', frappe.request.url, True) - data = render_page('404') - http_status_code = 404 - - if not data: - try: - data = render_page(path) - except frappe.PermissionError as e: - data, http_status_code = render_403(e, path) - - except frappe.PermissionError as e: - data, http_status_code = render_403(e, path) - - except frappe.Redirect as e: - raise e - - except Exception: - path = "error" - data = render_page(path) - http_status_code = 500 - - data = add_csrf_token(data) - - except frappe.Redirect: - return build_response(path, "", 301, { - "Location": frappe.flags.redirect_location or (frappe.local.response or {}).get('location'), - "Cache-Control": "no-store, no-cache, must-revalidate" - }) - - return build_response(path, data, http_status_code or 200) - -def is_static_file(path): - if ('.' not in path): - return False - extn = path.rsplit('.', 1)[-1] - if extn in ('html', 'md', 'js', 'xml', 'css', 'txt', 'py', 'json'): - return False - - for app in frappe.get_installed_apps(): - file_path = frappe.get_app_path(app, 'www') + '/' + path - if os.path.exists(file_path): - frappe.flags.file_path = file_path - return True - - return False - -def is_web_form(path): - return bool(frappe.get_all("Web Form", filters={'route': path})) - -def render_web_form(path): - data = render_page(path) - return data - -def get_static_file_response(): - try: - f = open(frappe.flags.file_path, 'rb') - except IOError: - raise NotFound - - response = Response(wrap_file(frappe.local.request.environ, f), direct_passthrough=True) - response.mimetype = mimetypes.guess_type(frappe.flags.file_path)[0] or 'application/octet-stream' - return response - -def build_response(path, data, http_status_code, headers=None): - # build response - response = Response() - response.data = set_content_type(response, data, path) - response.status_code = http_status_code - response.headers["X-Page-Name"] = path.encode("ascii", errors="xmlcharrefreplace") - response.headers["X-From-Cache"] = frappe.local.response.from_cache or False - - add_preload_headers(response) - if headers: - for key, val in headers.items(): - response.headers[key] = val.encode("ascii", errors="xmlcharrefreplace") - - return response - - -def add_preload_headers(response): - from bs4 import BeautifulSoup - - try: - preload = [] - soup = BeautifulSoup(response.data, "lxml") - for elem in soup.find_all('script', src=re.compile(".*")): - preload.append(("script", elem.get("src"))) - - for elem in soup.find_all('link', rel="stylesheet"): - preload.append(("style", elem.get("href"))) - - links = [] - for _type, link in preload: - links.append("<{}>; rel=preload; as={}".format(link, _type)) - - if links: - response.headers["Link"] = ",".join(links) - except Exception: - import traceback - traceback.print_exc() - - -def render_page_by_language(path): - translated_languages = frappe.get_hooks("translated_languages_for_website") - user_lang = guess_language(translated_languages) - if translated_languages and user_lang in translated_languages: - try: - if path and path != "index": - lang_path = '{0}/{1}'.format(user_lang, path) - else: - lang_path = user_lang # index - - return render_page(lang_path) - except frappe.DoesNotExistError: - return render_page(path) - - else: - return render_page(path) - -def render_page(path): - """get page html""" - out = None - - if can_cache(): - # return rendered page - page_cache = frappe.cache().hget("website_page", path) - if page_cache and frappe.local.lang in page_cache: - out = page_cache[frappe.local.lang] - - if out: - frappe.local.response.from_cache = True - return out - - return build(path) - -def build(path): - if not frappe.db: - frappe.connect() - - try: - return build_page(path) - except frappe.DoesNotExistError: - hooks = frappe.get_hooks() - if hooks.website_catch_all: - path = hooks.website_catch_all[0] - return build_page(path) - else: - raise - except Exception: - raise - -def build_page(path): - if not getattr(frappe.local, "path", None): - frappe.local.path = path - - context = get_context(path) - - if context.source: - html = frappe.render_template(context.source, context) - elif context.template: - if path.endswith('min.js'): - html = frappe.get_jloader().get_source(frappe.get_jenv(), context.template)[0] - else: - html = frappe.get_template(context.template).render(context) - - if '{index}' in html: - html = html.replace('{index}', get_toc(context.route)) - - if '{next}' in html: - html = html.replace('{next}', get_next_link(context.route)) - - # html = frappe.get_template(context.base_template_path).render(context) - - if can_cache(context.no_cache): - page_cache = frappe.cache().hget("website_page", path) or {} - page_cache[frappe.local.lang] = html - frappe.cache().hset("website_page", path, page_cache) - - return html - -def resolve_path(path): - if not path: - path = "index" - - if path.endswith('.html'): - path = path[:-5] - - if path == "index": - path = get_home_page() - - frappe.local.path = path - - if path != "index": - path = resolve_from_map(path) - - return path - -def resolve_from_map(path): - '''transform dynamic route to a static one from hooks and route defined in doctype''' - rules = [Rule(r["from_route"], endpoint=r["to_route"], defaults=r.get("defaults")) - for r in get_website_rules()] - - return evaluate_dynamic_routes(rules, path) or path - -def get_website_rules(): - '''Get website route rules from hooks and DocType route''' - def _get(): - rules = frappe.get_hooks("website_route_rules") - for d in frappe.get_all('DocType', 'name, route', dict(has_web_view=1)): - if d.route: - rules.append(dict(from_route = '/' + d.route.strip('/'), to_route=d.name)) - - return rules - - if frappe.local.dev_server: - # dont cache in development - return _get() - - return frappe.cache().get_value('website_route_rules', _get) - -def set_content_type(response, data, path): - if isinstance(data, dict): - response.mimetype = 'application/json' - response.charset = 'utf-8' - data = json.dumps(data) - return data - - response.mimetype = 'text/html' - response.charset = 'utf-8' - - if "." in path: - content_type, encoding = mimetypes.guess_type(path) - if content_type: - response.mimetype = content_type - if encoding: - response.charset = encoding - - return data - -def clear_cache(path=None): - '''Clear website caches - - :param path: (optional) for the given path''' - for key in ('website_generator_routes', 'website_pages', - 'website_full_index', 'sitemap_routes'): - frappe.cache().delete_value(key) - - frappe.cache().delete_value("website_404") - if path: - frappe.cache().hdel('website_redirects', path) - delete_page_cache(path) - else: - clear_sitemap() - frappe.clear_cache("Guest") - for key in ('portal_menu_items', 'home_page', 'website_route_rules', - 'doctypes_with_web_view', 'website_redirects', 'page_context', - 'website_page'): - frappe.cache().delete_value(key) - - for method in frappe.get_hooks("website_clear_cache"): - frappe.get_attr(method)(path) - -def render_403(e, pathname): - frappe.local.message = cstr(e) - frappe.local.message_title = _("Not Permitted") - frappe.local.response['context'] = dict( - indicator_color = 'red', - primary_action = '/login', - primary_label = _('Login'), - fullpage=True - ) - return render_page("message"), e.http_status_code - -def get_doctype_from_path(path): - doctypes = frappe.db.sql_list("select name from tabDocType") - - parts = path.split("/") - - doctype = parts[0] - name = parts[1] if len(parts) > 1 else None - - if doctype in doctypes: - return doctype, name - - # try scrubbed - doctype = doctype.replace("_", " ").title() - if doctype in doctypes: - return doctype, name - - return None, None - -def add_csrf_token(data): - if frappe.local.session: - return data.replace("", ''.format( - frappe.local.session.data.csrf_token)) - else: - return data - -def raise_if_disabled(path): - routes = frappe.db.get_all('Portal Menu Item', - fields=['route', 'enabled'], - filters={ - 'enabled': 0, - 'route': ['like', '%{0}'.format(path)] - } - ) - - for r in routes: - _path = r.route.lstrip('/') - if path == _path and not r.enabled: - raise frappe.PermissionError - diff --git a/frappe/website/router.py b/frappe/website/router.py index aa74d140c1..a9e2f68fe5 100644 --- a/frappe/website/router.py +++ b/frappe/website/router.py @@ -6,131 +6,9 @@ import os import re import frappe -from frappe.model.document import get_controller -from frappe.website.utils import can_cache, delete_page_cache, extract_comment_tag, extract_title +from frappe.website.utils import extract_title from werkzeug.routing import Map, Rule, NotFound -def resolve_route(path): - """Returns the page route object based on searching in pages and generators. - The `www` folder is also a part of generator **Web Page**. - - The only exceptions are `/about` and `/contact` these will be searched in Web Pages - first before checking the standard pages.""" - - if path not in ("about", "contact"): - context = get_page_info_from_template(path) - if context: - return context - return get_page_context_from_doctype(path) - else: - context = get_page_context_from_doctype(path) - if context: - return context - return get_page_info_from_template(path) - -def get_page_context(path): - page_context = None - if can_cache(): - page_context_cache = frappe.cache().hget("page_context", path) or {} - page_context = page_context_cache.get(frappe.local.lang, None) - - if not page_context: - page_context = make_page_context(path) - if can_cache(page_context.no_cache): - page_context_cache[frappe.local.lang] = page_context - frappe.cache().hset("page_context", path, page_context_cache) - - return page_context - -def make_page_context(path): - context = resolve_route(path) - if not context: - raise frappe.PageDoesNotExistError - - context.doctype = context.ref_doctype - - if context.page_title: - context.title = context.page_title - - context.pathname = frappe.local.path - - return context - -def get_page_info_from_template(path): - '''Return page_info from path''' - for app in frappe.get_installed_apps(frappe_last=True): - app_path = frappe.get_app_path(app) - - folders = get_start_folders() - - for start in folders: - search_path = os.path.join(app_path, start, path) - options = (search_path, search_path + '.html', search_path + '.md', - search_path + '/index.html', search_path + '/index.md') - for o in options: - option = frappe.as_unicode(o) - if os.path.exists(option) and not os.path.isdir(option): - return get_page_info(option, app, start, app_path=app_path) - - return None - -def get_page_context_from_doctype(path): - page_info = get_page_info_from_doctypes(path) - if not page_info: - page_info = get_page_info_from_web_page_with_dynamic_routes(path) - - if page_info: - return frappe.get_doc(page_info.get("doctype"), - page_info.get("name")).get_page_info() - -def clear_sitemap(): - delete_page_cache("*") - -def get_all_page_context_from_doctypes(): - ''' - Get all doctype generated routes (for sitemap.xml) - ''' - routes = frappe.cache().get_value("website_generator_routes") - if not routes: - routes = get_page_info_from_doctypes() - frappe.cache().set_value("website_generator_routes", routes) - - return routes - -def get_page_info_from_doctypes(path=None): - ''' - Find a document with matching `route` from all doctypes with `has_web_view`=1 - ''' - routes = {} - for doctype in get_doctypes_with_web_view(): - filters = {} - controller = get_controller(doctype) - meta = frappe.get_meta(doctype) - - condition_field = (meta.is_published_field or - # custom doctypes dont have controllers and no website attribute - (controller.website.condition_field if not meta.custom else None)) - - if condition_field: - filters[condition_field] = 1 - - if path: - filters['route'] = path - - try: - for r in frappe.get_all(doctype, fields = ['name', 'route', 'modified'], - filters = filters, limit = 1): - - routes[r.route] = {"doctype": doctype, "name": r.name, "modified": r.modified} - - # just want one path, return it! - if path: - return routes[r.route] - except Exception as e: - if not frappe.db.is_missing_column(e): raise e - - return routes - def get_page_info_from_web_page_with_dynamic_routes(path): ''' Query Web Page with dynamic_route = 1 and evaluate if any of the routes match @@ -229,7 +107,7 @@ def get_page_info(path, app, start, basepath=None, app_path=None, fname=None): if basepath is None: basepath = os.path.dirname(path) - page_name, extn = fname.rsplit(".", 1) + page_name, extn = os.path.splitext(fname) # add website route page_info = frappe._dict() @@ -265,14 +143,12 @@ def get_page_info(path, app, start, basepath=None, app_path=None, fname=None): # get the source setup_source(page_info) - # extract properties from HTML comments - load_properties_from_source(page_info) + if not page_info.title: + page_info.title = extract_title(page_info.source, page_info.route) # extract properties from controller attributes load_properties_from_controller(page_info) - page_info.build_version = frappe.utils.get_build_version() - return page_info def get_frontmatter(string): @@ -375,47 +251,6 @@ def setup_index(page_info): with open(index_txt_path, 'r') as f: page_info.index = f.read().splitlines() -def load_properties_from_source(page_info): - '''Load properties like no_cache, title from source html''' - - if not page_info.title: - page_info.title = extract_title(page_info.source, page_info.route) - - base_template = extract_comment_tag(page_info.source, 'base_template') - if base_template: - page_info.base_template = base_template - - if (page_info.base_template - and "{%- extends" not in page_info.source - and "{% extends" not in page_info.source - and "" not in page_info.source): - page_info.source = '''{{% extends "{0}" %}} - {{% block page_content %}}{1}{{% endblock %}}'''.format(page_info.base_template, page_info.source) - - if "" in page_info.source: - page_info.no_breadcrumbs = 1 - - if "" in page_info.source: - page_info.show_sidebar = 1 - - if "" in page_info.source: - page_info.add_breadcrumbs = 1 - - if "" in page_info.source: - page_info.no_header = 1 - - if "" in page_info.source: - page_info.add_next_prev_links = 1 - - if "" in page_info.source: - page_info.no_cache = 1 - - if "" in page_info.source: - page_info.sitemap = 0 - - if "" in page_info.source: - page_info.sitemap = 1 - def load_properties_from_controller(page_info): if not page_info.controller: return @@ -432,8 +267,10 @@ def get_doctypes_with_web_view(): def _get(): installed_apps = frappe.get_installed_apps() doctypes = frappe.get_hooks("website_generators") - doctypes += [d.name for d in frappe.get_all('DocType', 'name, module', - dict(has_web_view=1)) if frappe.local.module_app[frappe.scrub(d.module)] in installed_apps] + doctypes_with_web_view = frappe.get_all('DocType', fields=['name', 'module'], + filters=dict(has_web_view=1)) + module_app_map = frappe.local.module_app + doctypes += [d.name for d in doctypes_with_web_view if module_app_map[frappe.scrub(d.module)] in installed_apps] return doctypes return frappe.cache().get_value('doctypes_with_web_view', _get) diff --git a/frappe/website/serve.py b/frappe/website/serve.py new file mode 100644 index 0000000000..fe7fc77064 --- /dev/null +++ b/frappe/website/serve.py @@ -0,0 +1,29 @@ +import frappe +from frappe.website.page_renderers.error_page import ErrorPage +from frappe.website.page_renderers.not_permitted_page import NotPermittedPage +from frappe.website.page_renderers.redirect_page import RedirectPage +from frappe.website.path_resolver import PathResolver + + +def get_response(path=None, http_status_code=200): + """Resolves path and renders page""" + response = None + path = path or frappe.local.request.path + endpoint = path + + try: + path_resolver = PathResolver(path) + endpoint, renderer_instance = path_resolver.resolve() + response = renderer_instance.render() + except frappe.Redirect: + return RedirectPage(endpoint or path, http_status_code).render() + except frappe.PermissionError as e: + response = NotPermittedPage(endpoint, http_status_code, exception=e).render() + except Exception as e: + response = ErrorPage(exception=e).render() + + return response + +def get_response_content(path=None, http_status_code=200): + response = get_response(path, http_status_code) + return str(response.data, 'utf-8') diff --git a/frappe/website/utils.py b/frappe/website/utils.py index aa98595e2d..0f5f182ea2 100644 --- a/frappe/website/utils.py +++ b/frappe/website/utils.py @@ -1,9 +1,18 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # MIT License. See license.txt -import re +import json +import mimetypes import os -import frappe +import re +from functools import wraps +import yaml +from six import iteritems +from werkzeug.wrappers import Response + +import frappe +from frappe import _ +from frappe.model.document import Document from frappe.utils import md_to_html @@ -128,14 +137,8 @@ def get_home_page_via_hooks(): return home_page -def is_signup_enabled(): - if getattr(frappe.local, "is_signup_enabled", None) is None: - frappe.local.is_signup_enabled = True - if frappe.utils.cint(frappe.db.get_value("Website Settings", - "Website Settings", "disable_signup")): - frappe.local.is_signup_enabled = False - - return frappe.local.is_signup_enabled +def is_signup_disabled(): + return frappe.db.get_single_value('Website Settings', 'disable_signup', True) def cleanup_page_name(title): """make page name from title""" @@ -145,91 +148,15 @@ def cleanup_page_name(title): name = title.lower() name = re.sub(r'[~!@#$%^&*+()<>,."\'\?]', '', name) name = re.sub('[:/]', '-', name) - name = '-'.join(name.split()) - # replace repeating hyphens name = re.sub(r"(-)\1+", r"\1", name) - return name[:140] -def get_shade(color, percent): - color, color_format = detect_color_format(color) - r, g, b, a = color - - avg = (float(int(r) + int(g) + int(b)) / 3) - # switch dark and light shades - if avg > 128: - percent = -percent - - # stronger diff for darker shades - if percent < 25 and avg < 64: - percent = percent * 2 - - new_color = [] - for channel_value in (r, g, b): - new_color.append(get_shade_for_channel(channel_value, percent)) - - r, g, b = new_color - - return format_color(r, g, b, a, color_format) - - -def detect_color_format(color): - if color.startswith("rgba"): - color_format = "rgba" - color = [c.strip() for c in color[5:-1].split(",")] - - elif color.startswith("rgb"): - color_format = "rgb" - color = [c.strip() for c in color[4:-1].split(",")] + [1] - - else: - # assume hex - color_format = "hex" - - if color.startswith("#"): - color = color[1:] - - if len(color) == 3: - # hex in short form like #fff - color = "{0}{0}{1}{1}{2}{2}".format(*tuple(color)) - - color = [int(color[0:2], 16), int(color[2:4], 16), int(color[4:6], 16), 1] - - return color, color_format - - -def get_shade_for_channel(channel_value, percent): - v = int(channel_value) + int(int('ff', 16) * (float(percent)/100)) - if v < 0: - v=0 - if v > 255: - v=255 - - return v - - -def format_color(r, g, b, a, color_format): - if color_format == "rgba": - return "rgba({0}, {1}, {2}, {3})".format(r, g, b, a) - - elif color_format == "rgb": - return "rgb({0}, {1}, {2})".format(r, g, b) - - else: - # assume hex - return "#{0}{1}{2}".format(convert_to_hex(r), convert_to_hex(g), convert_to_hex(b)) - - -def convert_to_hex(channel_value): - h = hex(channel_value)[2:] - - if len(h) < 2: - h = "0" + h - - return h +def get_shade(color, percent=None): + frappe.msgprint(_('get_shade method has been deprecated.')) + return color def abs_url(path): """Deconstructs and Reconstructs a URL into an absolute URL or a URL relative from root '/'""" @@ -359,25 +286,6 @@ def extract_comment_tag(source, tag): return None -def add_missing_headers(): - '''Walk and add missing headers in docs (to be called from bench execute)''' - path = frappe.get_app_path('erpnext', 'docs') - for basepath, folders, files in os.walk(path): - for fname in files: - if fname.endswith('.md'): - with open(os.path.join(basepath, fname), 'r') as f: - content = frappe.as_unicode(f.read()) - - if not content.startswith('# ') and not '

    ' in content: - with open(os.path.join(basepath, fname), 'w') as f: - if fname=='index.md': - fname = os.path.basename(basepath) - else: - fname = fname[:-3] - h = fname.replace('_', ' ').replace('-', ' ').title() - content = '# {0}\n\n'.format(h) + content - f.write(content.encode('utf-8')) - def get_html_content_based_on_type(doc, fieldname, content_type): ''' Set content based on content_type @@ -393,3 +301,208 @@ def get_html_content_based_on_type(doc, fieldname, content_type): content = '' return content + + +def clear_cache(path=None): + '''Clear website caches + :param path: (optional) for the given path''' + for key in ('website_generator_routes', 'website_pages', + 'website_full_index', 'sitemap_routes'): + frappe.cache().delete_value(key) + + frappe.cache().delete_value("website_404") + if path: + frappe.cache().hdel('website_redirects', path) + delete_page_cache(path) + else: + clear_sitemap() + frappe.clear_cache("Guest") + for key in ('portal_menu_items', 'home_page', 'website_route_rules', + 'doctypes_with_web_view', 'website_redirects', 'page_context', + 'website_page'): + frappe.cache().delete_value(key) + + for method in frappe.get_hooks("website_clear_cache"): + frappe.get_attr(method)(path) + +def clear_website_cache(path=None): + clear_cache(path) + +def clear_sitemap(): + delete_page_cache("*") + +def get_frontmatter(string): + "Reference: https://github.com/jonbeebe/frontmatter" + frontmatter = "" + body = "" + result = re.compile(r'^\s*(?:---|\+\+\+)(.*?)(?:---|\+\+\+)\s*(.+)$', re.S | re.M).search(string) + if result: + frontmatter = result.group(1) + body = result.group(2) + + return { + "attributes": yaml.safe_load(frontmatter), + "body": body, + } + +def get_sidebar_items(parent_sidebar, basepath): + import frappe.www.list + sidebar_items = [] + + hooks = frappe.get_hooks('look_for_sidebar_json') + look_for_sidebar_json = hooks[0] if hooks else frappe.flags.look_for_sidebar + + if basepath and look_for_sidebar_json: + sidebar_items = get_sidebar_items_from_sidebar_file(basepath, look_for_sidebar_json) + + if not sidebar_items and parent_sidebar: + sidebar_items = frappe.get_all('Website Sidebar Item', + filters=dict(parent=parent_sidebar), fields=['title', 'route', '`group`'], + order_by='idx asc') + + if not sidebar_items: + sidebar_items = get_portal_sidebar_items() + + return sidebar_items + + +def get_portal_sidebar_items(): + sidebar_items = frappe.cache().hget('portal_menu_items', frappe.session.user) + if sidebar_items is None: + sidebar_items = [] + roles = frappe.get_roles() + portal_settings = frappe.get_doc('Portal Settings', 'Portal Settings') + + def add_items(sidebar_items, items): + for d in items: + if d.get('enabled') and ((not d.get('role')) or d.get('role') in roles): + sidebar_items.append(d.as_dict() if isinstance(d, Document) else d) + + if not portal_settings.hide_standard_menu: + add_items(sidebar_items, portal_settings.get('menu')) + + if portal_settings.custom_menu: + add_items(sidebar_items, portal_settings.get('custom_menu')) + + items_via_hooks = frappe.get_hooks('portal_menu_items') + if items_via_hooks: + for i in items_via_hooks: + i['enabled'] = 1 + add_items(sidebar_items, items_via_hooks) + + frappe.cache().hset('portal_menu_items', frappe.session.user, sidebar_items) + + return sidebar_items + +def get_sidebar_items_from_sidebar_file(basepath, look_for_sidebar_json): + sidebar_items = [] + sidebar_json_path = get_sidebar_json_path(basepath, look_for_sidebar_json) + if not sidebar_json_path: + return sidebar_items + + with open(sidebar_json_path, 'r') as sidebarfile: + try: + sidebar_json = sidebarfile.read() + sidebar_items = json.loads(sidebar_json) + except json.decoder.JSONDecodeError: + frappe.throw('Invalid Sidebar JSON at ' + sidebar_json_path) + + return sidebar_items + +def get_sidebar_json_path(path, look_for=False): + '''Get _sidebar.json path from directory path + :param path: path of the current diretory + :param look_for: if True, look for _sidebar.json going upwards from given path + :return: _sidebar.json path + ''' + if os.path.split(path)[1] == 'www' or path == '/' or not path: + return '' + + sidebar_json_path = os.path.join(path, '_sidebar.json') + if os.path.exists(sidebar_json_path): + return sidebar_json_path + else: + if look_for: + return get_sidebar_json_path(os.path.split(path)[0], look_for) + else: + return '' + +def cache_html(func): + @wraps(func) + def cache_html_decorator(*args, **kwargs): + if can_cache(): + html = None + page_cache = frappe.cache().hget("website_page", args[0].path) + if page_cache and frappe.local.lang in page_cache: + html = page_cache[frappe.local.lang] + if html: + frappe.local.response.from_cache = True + return html + html = func(*args, **kwargs) + context = args[0].context + if can_cache(context.no_cache): + page_cache = frappe.cache().hget("website_page", args[0].path) or {} + page_cache[frappe.local.lang] = html + frappe.cache().hset("website_page", args[0].path, page_cache) + + return html + + return cache_html_decorator + +def build_response(path, data, http_status_code, headers=None): + # build response + response = Response() + response.data = set_content_type(response, data, path) + response.status_code = http_status_code + response.headers["X-Page-Name"] = path.encode("ascii", errors="xmlcharrefreplace") + response.headers["X-From-Cache"] = frappe.local.response.from_cache or False + + add_preload_headers(response) + if headers: + for key, val in iteritems(headers): + response.headers[key] = val.encode("ascii", errors="xmlcharrefreplace") + + return response + +def set_content_type(response, data, path): + if isinstance(data, dict): + response.mimetype = 'application/json' + response.charset = 'utf-8' + data = json.dumps(data) + return data + + response.mimetype = 'text/html' + response.charset = 'utf-8' + + # ignore paths ending with .com to avoid unnecessary download + # https://bugs.python.org/issue22347 + if "." in path and not path.endswith('.com'): + content_type, encoding = mimetypes.guess_type(path) + if content_type: + response.mimetype = content_type + if encoding: + response.charset = encoding + + return data + +def add_preload_headers(response): + from bs4 import BeautifulSoup + + try: + preload = [] + soup = BeautifulSoup(response.data, "lxml") + for elem in soup.find_all('script', src=re.compile(".*")): + preload.append(("script", elem.get("src"))) + + for elem in soup.find_all('link', rel="stylesheet"): + preload.append(("style", elem.get("href"))) + + links = [] + for _type, link in preload: + links.append("<{}>; rel=preload; as={}".format(link, _type)) + + if links: + response.headers["Link"] = ",".join(links) + except Exception: + import traceback + traceback.print_exc() diff --git a/frappe/website/website_components/metatags.py b/frappe/website/website_components/metatags.py new file mode 100644 index 0000000000..045bef8fe1 --- /dev/null +++ b/frappe/website/website_components/metatags.py @@ -0,0 +1,68 @@ +import frappe + +class MetaTags(): + def __init__(self, path, context): + self.path = path + self.context = context + self.tags = frappe._dict(self.context.get("metatags") or {}) + self.init_metatags_from_context() + self.set_opengraph_tags() + self.set_twitter_tags() + self.set_meta_published_on() + self.set_metatags_from_website_route_meta() + + def init_metatags_from_context(self): + for key in ('title', 'description', 'image', 'author', 'url', 'published_on'): + if key not in self.tags and self.context.get(key): + self.tags[key] = self.context[key] + + if not self.tags.get('title'): + self.tags['title'] = self.context.get('name') + + if self.tags.get('image'): + self.tags['image'] = frappe.utils.get_url(self.tags['image']) + + self.tags["language"] = frappe.local.lang or "en" + + def set_opengraph_tags(self): + if "og:type" not in self.tags: + self.tags["og:type"] = "article" + + for key in ('title', 'description', 'image', 'author', 'url'): + if self.tags.get(key): + self.tags['og:' + key] = self.tags.get(key) + + def set_twitter_tags(self): + for key in ('title', 'description', 'image', 'author', 'url'): + if self.tags.get(key): + self.tags['twitter:' + key] = self.tags.get(key) + + if self.tags.get('image'): + self.tags['twitter:card'] = "summary_large_image" + else: + self.tags["twitter:card"] = "summary" + + def set_meta_published_on(self): + if "published_on" in self.tags: + self.tags["datePublished"] = self.tags["published_on"] + del self.tags["published_on"] + + def set_metatags_from_website_route_meta(self): + ''' + Get meta tags from Website Route meta + they can override the defaults set above + ''' + route = self.path + if route == '': + # homepage + route = frappe.db.get_single_value('Website Settings', 'home_page') + + route_exists = (route + and not route.endswith(('.js', '.css')) + and frappe.db.exists('Website Route Meta', route)) + + if route_exists: + website_route_meta = frappe.get_doc('Website Route Meta', route) + for meta_tag in website_route_meta.meta_tags: + d = meta_tag.get_meta_dict() + self.tags.update(d) diff --git a/frappe/website/website_generator.py b/frappe/website/website_generator.py index 351f2f1832..3afe486944 100644 --- a/frappe/website/website_generator.py +++ b/frappe/website/website_generator.py @@ -4,7 +4,7 @@ import frappe from frappe.model.document import Document from frappe.website.utils import cleanup_page_name -from frappe.website.render import clear_cache +from frappe.website.utils import clear_cache from frappe.modules import get_module_name from frappe.search.website_search import update_index_for_path, remove_document_from_index @@ -128,6 +128,8 @@ class WebsiteGenerator(Document): if not route.page_title: route.page_title = self.get(self.get_title_field()) + route.title = route.page_title + return route def send_indexing_request(self, operation_type='URL_UPDATED'): diff --git a/frappe/www/404.py b/frappe/www/404.py index f064a66c17..1e6bdc177d 100644 --- a/frappe/www/404.py +++ b/frappe/www/404.py @@ -1,2 +1,5 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # MIT License. See license.txt + +def get_context(context): + context.http_status_code = 404 diff --git a/frappe/www/_test/_sidebar.json b/frappe/www/_test/_sidebar.json new file mode 100644 index 0000000000..a2567e94da --- /dev/null +++ b/frappe/www/_test/_sidebar.json @@ -0,0 +1,6 @@ +[ + { + "route": "/_test/_test_folder", + "title": "Test Sidebar" + } +] diff --git a/frappe/patches/v5_0/__init__.py b/frappe/www/_test/_test_folder/__init__.py similarity index 100% rename from frappe/patches/v5_0/__init__.py rename to frappe/www/_test/_test_folder/__init__.py diff --git a/frappe/www/_test/_test_folder/_test_page.css b/frappe/www/_test/_test_folder/_test_page.css new file mode 100644 index 0000000000..e42b809085 --- /dev/null +++ b/frappe/www/_test/_test_folder/_test_page.css @@ -0,0 +1,3 @@ +body { + background-color: var(--bg-color); +} \ No newline at end of file diff --git a/frappe/www/_test/_test_folder/_test_page.html b/frappe/www/_test/_test_folder/_test_page.html new file mode 100644 index 0000000000..123d619e38 --- /dev/null +++ b/frappe/www/_test/_test_folder/_test_page.html @@ -0,0 +1,5 @@ +{% block content %} +{% include "templates/includes/web_sidebar.html" %} +

    Test content

    +{next} +{% endblock %} diff --git a/frappe/www/_test/_test_folder/_test_page.js b/frappe/www/_test/_test_folder/_test_page.js new file mode 100644 index 0000000000..6e0c1f3a87 --- /dev/null +++ b/frappe/www/_test/_test_folder/_test_page.js @@ -0,0 +1 @@ +console.log('test data'); \ No newline at end of file diff --git a/frappe/www/_test/_test_folder/_test_page.py b/frappe/www/_test/_test_folder/_test_page.py new file mode 100644 index 0000000000..1813a06bac --- /dev/null +++ b/frappe/www/_test/_test_folder/_test_page.py @@ -0,0 +1,3 @@ +def get_context(context): + context.base_template_path = 'frappe/templates/test/_test_base.html' + context.add_breadcrumbs = 1 diff --git a/frappe/www/_test/_test_folder/_test_toc.md b/frappe/www/_test/_test_folder/_test_toc.md new file mode 100644 index 0000000000..02cc3c82be --- /dev/null +++ b/frappe/www/_test/_test_folder/_test_toc.md @@ -0,0 +1,19 @@ +--- +title: Test TOC +add_breadcrumbs: 1 +show_sidebar: 0 + +metatags: + description: Test Description. + keywords: Frappe Framework. +--- + +# Level 1 + +## Level 1.1 + +## Level 1.2 + +## Level 1.3 + +### Level 1.3.1 diff --git a/frappe/www/_test/_test_folder/index.md b/frappe/www/_test/_test_folder/index.md new file mode 100644 index 0000000000..1a5a9e7f81 --- /dev/null +++ b/frappe/www/_test/_test_folder/index.md @@ -0,0 +1,9 @@ +--- +title: Test TOC +add_breadcrumbs: 1 +show_sidebar: 1 +--- + +# Index + +{index} \ No newline at end of file diff --git a/frappe/patches/v5_2/__init__.py b/frappe/www/_test/_test_folder/new.csv/__init__.py similarity index 100% rename from frappe/patches/v5_2/__init__.py rename to frappe/www/_test/_test_folder/new.csv/__init__.py diff --git a/frappe/www/_test/_test_folder/new.csv/index.html b/frappe/www/_test/_test_folder/new.csv/index.html new file mode 100644 index 0000000000..7a1bb69558 --- /dev/null +++ b/frappe/www/_test/_test_folder/new.csv/index.html @@ -0,0 +1,12 @@ + + + + + + + Document + + + Test Page + + diff --git a/frappe/www/_test/_test_home_page.py b/frappe/www/_test/_test_home_page.py new file mode 100644 index 0000000000..936399c700 --- /dev/null +++ b/frappe/www/_test/_test_home_page.py @@ -0,0 +1,2 @@ +def get_website_user_home_page(user): + return '/_test/_test_folder' \ No newline at end of file diff --git a/frappe/www/_test/index.html b/frappe/www/_test/index.html new file mode 100644 index 0000000000..0dff60b400 --- /dev/null +++ b/frappe/www/_test/index.html @@ -0,0 +1 @@ +{index} \ No newline at end of file diff --git a/frappe/www/_test/problematic_page.html b/frappe/www/_test/problematic_page.html new file mode 100644 index 0000000000..5e194421d2 --- /dev/null +++ b/frappe/www/_test/problematic_page.html @@ -0,0 +1 @@ +{% raise %} diff --git a/frappe/www/_test/static-file-test.png b/frappe/www/_test/static-file-test.png new file mode 100644 index 0000000000..b51db82f82 Binary files /dev/null and b/frappe/www/_test/static-file-test.png differ diff --git a/frappe/www/app.py b/frappe/www/app.py index b0fa19df9b..27505c8131 100644 --- a/frappe/www/app.py +++ b/frappe/www/app.py @@ -1,7 +1,6 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # MIT License. See license.txt no_cache = 1 -base_template_path = "templates/www/app.html" import os, re import frappe diff --git a/frappe/www/error.py b/frappe/www/error.py index dcbfb38e1f..11151ef766 100644 --- a/frappe/www/error.py +++ b/frappe/www/error.py @@ -6,5 +6,7 @@ no_cache = 1 def get_context(context): if frappe.flags.in_migrate: return + context.http_status_code = 500 + print(frappe.get_traceback().encode("utf-8")) return {"error": frappe.get_traceback().replace("<", "<").replace(">", ">") } diff --git a/frappe/www/list.py b/frappe/www/list.py index 881aaf085b..5e4e491c80 100644 --- a/frappe/www/list.py +++ b/frappe/www/list.py @@ -3,7 +3,7 @@ import frappe, json from frappe.utils import cint, quoted -from frappe.website.render import resolve_path +from frappe.website.path_resolver import resolve_path from frappe.model.document import get_controller, Document from frappe import _ diff --git a/frappe/www/me.html b/frappe/www/me.html index 402fdecf59..eb97c566d8 100644 --- a/frappe/www/me.html +++ b/frappe/www/me.html @@ -4,7 +4,6 @@ {% block header %}

    {{ _("My Account") }}

    {% endblock %} {% block page_content %} -