@@ -1227,4 +1227,4 @@ def logger(module=None, with_more_info=True): | |||||
return get_logger(module or __name__, with_more_info=with_more_info) | return get_logger(module or __name__, with_more_info=with_more_info) | ||||
def get_desk_link(doctype, name): | def get_desk_link(doctype, name): | ||||
return '<a href="#Form/{0}/{1}" style="font-weight: bold;">{2} {1}</a>'.format(doctype, name, _(doctype)) | |||||
return '<a href="#Form/{0}/{1}" style="font-weight: bold;">{2} {1}</a>'.format(doctype, name, _(doctype)) |
@@ -12,6 +12,7 @@ import frappe.desk.desk_page | |||||
from frappe.desk.form.load import get_meta_bundle | from frappe.desk.form.load import get_meta_bundle | ||||
from frappe.utils.change_log import get_versions | from frappe.utils.change_log import get_versions | ||||
from frappe.translate import get_lang_dict | from frappe.translate import get_lang_dict | ||||
from frappe.limits import get_limits, get_expiry_message | |||||
def get_bootinfo(): | def get_bootinfo(): | ||||
"""build and return boot info""" | """build and return boot info""" | ||||
@@ -65,6 +66,10 @@ def get_bootinfo(): | |||||
bootinfo.calendars = sorted(frappe.get_hooks("calendars")) | bootinfo.calendars = sorted(frappe.get_hooks("calendars")) | ||||
bootinfo.lang_dict = get_lang_dict() | bootinfo.lang_dict = get_lang_dict() | ||||
# limits | |||||
bootinfo.limits = get_limits() | |||||
bootinfo.expiry_message = get_expiry_message() | |||||
return bootinfo | return bootinfo | ||||
def load_conf_settings(bootinfo): | def load_conf_settings(bootinfo): | ||||
@@ -4,8 +4,7 @@ import hashlib, os | |||||
import frappe | import frappe | ||||
from frappe.commands import pass_context, get_site | from frappe.commands import pass_context, get_site | ||||
from frappe.commands.scheduler import _is_scheduler_enabled | from frappe.commands.scheduler import _is_scheduler_enabled | ||||
from frappe.commands import get_site | |||||
from frappe.limits import set_limits, get_limits | |||||
from frappe.limits import update_limits, get_limits | |||||
from frappe.installer import update_site_config | from frappe.installer import update_site_config | ||||
@click.command('new-site') | @click.command('new-site') | ||||
@@ -345,18 +344,19 @@ def set_limit(context, site, limit, value): | |||||
site = get_site(context) | site = get_site(context) | ||||
with frappe.init_site(site): | with frappe.init_site(site): | ||||
if limit == 'expiry': | |||||
if limit=='expiry': | |||||
try: | try: | ||||
datetime.datetime.strptime(value, '%Y-%m-%d') | datetime.datetime.strptime(value, '%Y-%m-%d') | ||||
except ValueError: | except ValueError: | ||||
raise ValueError("Incorrect data format, should be YYYY-MM-DD") | raise ValueError("Incorrect data format, should be YYYY-MM-DD") | ||||
else: | |||||
limit += '_limit' | |||||
# Space can be float, while other should be integers | |||||
value = float(value) if limit == 'space_limit' else int(value) | |||||
set_limits({limit : value}) | |||||
elif limit=='space': | |||||
value = float(value) | |||||
else: | |||||
value = int(value) | |||||
update_limits({ limit : value }) | |||||
@click.command('clear-limit') | @click.command('clear-limit') | ||||
@click.option('--site', help='site name') | @click.option('--site', help='site name') | ||||
@@ -369,14 +369,11 @@ def clear_limit(context, site, limit): | |||||
site = get_site(context) | site = get_site(context) | ||||
with frappe.init_site(site): | with frappe.init_site(site): | ||||
if not limit == 'expiry': | |||||
limit += '_limit' | |||||
_clear_limit(limit) | _clear_limit(limit) | ||||
# Remove limits from the site_config, if it's empty | # Remove limits from the site_config, if it's empty | ||||
cur_limits = get_limits() | |||||
if not cur_limits: | |||||
limits = get_limits() | |||||
if not limits: | |||||
update_site_config('limits', 'None', validate=False) | update_site_config('limits', 'None', validate=False) | ||||
@@ -11,7 +11,7 @@ naming for same name files: file.gif, file-1.gif, file-2.gif etc | |||||
import frappe | import frappe | ||||
import json | import json | ||||
import urllib | import urllib | ||||
import os, subprocess | |||||
import os | |||||
import requests | import requests | ||||
import requests.exceptions | import requests.exceptions | ||||
import StringIO | import StringIO | ||||
@@ -20,8 +20,7 @@ import mimetypes, imghdr | |||||
from frappe.utils.file_manager import delete_file_data_content, get_content_hash, get_random_filename | from frappe.utils.file_manager import delete_file_data_content, get_content_hash, get_random_filename | ||||
from frappe import _ | from frappe import _ | ||||
from frappe.utils.nestedset import NestedSet | from frappe.utils.nestedset import NestedSet | ||||
from frappe.limits import get_limits, set_limits | |||||
from frappe.utils import strip, get_url, get_files_path, flt | |||||
from frappe.utils import strip, get_files_path | |||||
from PIL import Image, ImageOps | from PIL import Image, ImageOps | ||||
class FolderNotEmpty(frappe.ValidationError): pass | class FolderNotEmpty(frappe.ValidationError): pass | ||||
@@ -352,63 +351,3 @@ def check_file_permission(file_url): | |||||
raise frappe.PermissionError | raise frappe.PermissionError | ||||
def validate_space_limit(file_size): | |||||
"""Stop from writing file if max space limit is reached""" | |||||
from frappe.installer import update_site_config | |||||
from frappe.utils import cint | |||||
from frappe.utils.file_manager import MaxFileSizeReachedError | |||||
frappe_limits = get_limits() | |||||
if not frappe_limits.has_key('space_limit'): | |||||
return | |||||
# In Gigabytes | |||||
space_limit = flt(flt(frappe_limits['space_limit']) * 1024, 2) | |||||
# in Kilobytes | |||||
used_space = flt(frappe_limits['files_size']) + flt(frappe_limits['backup_size']) + flt(frappe_limits['database_size']) | |||||
file_size = file_size / (1024.0**2) | |||||
# Stop from attaching file | |||||
if flt(used_space + file_size, 2) > space_limit: | |||||
frappe.throw(_("You have exceeded the max space of {0} for your plan. {1} or {2}.").format( | |||||
"<b>{0}MB</b>".format(cint(space_limit)) if (space_limit < 1024) else "<b>{0}GB</b>".format(frappe_limits['space_limit']), | |||||
'<a href="#usage-info">{0}</a>'.format(_("Click here to check your usage")), | |||||
'<a href="#upgrade">{0}</a>'.format(_("upgrade to a higher plan")), | |||||
), MaxFileSizeReachedError) | |||||
# update files size in frappe subscription | |||||
new_files_size = flt(frappe_limits['files_size']) + file_size | |||||
set_limits({'files_size': file_size}) | |||||
def update_sizes(): | |||||
from frappe.installer import update_site_config | |||||
# public files | |||||
files_path = frappe.get_site_path("public", "files") | |||||
files_size = flt(subprocess.check_output(['du', '-ms', files_path]).split()[0]) | |||||
# private files | |||||
files_path = frappe.get_site_path("private", "files") | |||||
if os.path.exists(files_path): | |||||
files_size += flt(subprocess.check_output(['du', '-ms', files_path]).split()[0]) | |||||
# backups | |||||
backup_path = frappe.get_site_path("private", "backups") | |||||
backup_size = subprocess.check_output(['du', '-ms', backup_path]).split()[0] | |||||
database_size = get_database_size() | |||||
set_limits({'files_size': files_size, | |||||
'backup_size': backup_size, | |||||
'database_size': database_size}) | |||||
def get_database_size(): | |||||
db_name = frappe.conf.db_name | |||||
# This query will get the database size in MB | |||||
db_size = frappe.db.sql(''' | |||||
SELECT table_schema "database_name", sum( data_length + index_length ) / 1024 / 1024 "database_size" | |||||
FROM information_schema.TABLES WHERE table_schema = %s GROUP BY table_schema''', db_name, as_dict=True) | |||||
return db_size[0].get('database_size') |
@@ -98,10 +98,17 @@ class TestFile(unittest.TestCase): | |||||
def test_file_upload_limit(self): | def test_file_upload_limit(self): | ||||
from frappe.utils.file_manager import MaxFileSizeReachedError | from frappe.utils.file_manager import MaxFileSizeReachedError | ||||
from frappe.limits import set_limits, clear_limit | |||||
from frappe.limits import update_limits, clear_limit | |||||
from frappe import _dict | from frappe import _dict | ||||
set_limits({'space_limit': 1, 'files_size': (1024 * 1024), 'database_size': 0, 'backup_size': 0}) | |||||
update_limits({ | |||||
'space': 1, | |||||
'space_usage': { | |||||
'files_size': (1024 * 1024), | |||||
'database_size': 0, | |||||
'backup_size': 0 | |||||
} | |||||
}) | |||||
# Rebuild the frappe.local.conf to take up the changes from site_config | # Rebuild the frappe.local.conf to take up the changes from site_config | ||||
frappe.local.conf = _dict(frappe.get_site_config()) | frappe.local.conf = _dict(frappe.get_site_config()) | ||||
@@ -110,5 +117,5 @@ class TestFile(unittest.TestCase): | |||||
'This files test for max space usage', "", "", self.get_folder("Test Folder 2", "Home").name) | 'This files test for max space usage', "", "", self.get_folder("Test Folder 2", "Home").name) | ||||
# Scrub the site_config and rebuild frappe.local.conf | # Scrub the site_config and rebuild frappe.local.conf | ||||
clear_limit("space_limit") | |||||
frappe.local.conf = _dict(frappe.get_site_config()) | |||||
clear_limit("space") | |||||
frappe.local.conf = _dict(frappe.get_site_config()) |
@@ -8,7 +8,7 @@ import requests | |||||
from frappe.model.delete_doc import delete_doc | from frappe.model.delete_doc import delete_doc | ||||
from frappe.utils.data import today, add_to_date | from frappe.utils.data import today, add_to_date | ||||
from frappe import _dict | from frappe import _dict | ||||
from frappe.limits import SiteExpiredError, set_limits, clear_limit | |||||
from frappe.limits import SiteExpiredError, update_limits, clear_limit | |||||
from frappe.utils import get_url | from frappe.utils import get_url | ||||
from frappe.installer import update_site_config | from frappe.installer import update_site_config | ||||
@@ -82,7 +82,7 @@ class TestUser(unittest.TestCase): | |||||
def test_user_limit_for_site(self): | def test_user_limit_for_site(self): | ||||
from frappe.core.doctype.user.user import get_total_users | from frappe.core.doctype.user.user import get_total_users | ||||
set_limits({'user_limit': get_total_users()}) | |||||
update_limits({'users': get_total_users()}) | |||||
# reload site config | # reload site config | ||||
from frappe import _dict | from frappe import _dict | ||||
@@ -99,10 +99,10 @@ class TestUser(unittest.TestCase): | |||||
frappe.delete_doc('User', 'test_max_users@example.com') | frappe.delete_doc('User', 'test_max_users@example.com') | ||||
# Clear the user limit | # Clear the user limit | ||||
clear_limit('user_limit') | |||||
clear_limit('users') | |||||
def test_site_expiry(self): | def test_site_expiry(self): | ||||
set_limits({'expiry': add_to_date(today(), days=-1)}) | |||||
update_limits({'expiry': add_to_date(today(), days=-1)}) | |||||
frappe.local.conf = _dict(frappe.get_site_config()) | frappe.local.conf = _dict(frappe.get_site_config()) | ||||
frappe.db.commit() | frappe.db.commit() | ||||
@@ -3,6 +3,7 @@ | |||||
from __future__ import unicode_literals | from __future__ import unicode_literals | ||||
import frappe | import frappe | ||||
from frappe.model.document import Document | |||||
from frappe.utils import cint, has_gravatar, format_datetime, now_datetime, get_formatted_email | from frappe.utils import cint, has_gravatar, format_datetime, now_datetime, get_formatted_email | ||||
from frappe import throw, msgprint, _ | from frappe import throw, msgprint, _ | ||||
from frappe.utils.password import update_password as _update_password | from frappe.utils.password import update_password as _update_password | ||||
@@ -11,10 +12,11 @@ from frappe.utils.user import get_system_managers | |||||
import frappe.permissions | import frappe.permissions | ||||
import frappe.share | import frappe.share | ||||
import re | import re | ||||
from frappe.limits import get_limits | |||||
STANDARD_USERS = ("Guest", "Administrator") | STANDARD_USERS = ("Guest", "Administrator") | ||||
from frappe.model.document import Document | |||||
class MaxUsersReachedError(frappe.ValidationError): pass | |||||
class User(Document): | class User(Document): | ||||
__new_password = None | __new_password = None | ||||
@@ -54,6 +56,7 @@ class User(Document): | |||||
self.remove_all_roles_for_guest() | self.remove_all_roles_for_guest() | ||||
self.validate_username() | self.validate_username() | ||||
self.remove_disabled_roles() | self.remove_disabled_roles() | ||||
self.validate_user_limit() | |||||
if self.language == "Loading...": | if self.language == "Loading...": | ||||
self.language = None | self.language = None | ||||
@@ -383,6 +386,34 @@ class User(Document): | |||||
"""Returns list of modules blocked for that user""" | """Returns list of modules blocked for that user""" | ||||
return [d.module for d in self.block_modules] if self.block_modules else [] | return [d.module for d in self.block_modules] if self.block_modules else [] | ||||
def validate_user_limit(self): | |||||
''' | |||||
Validate if user limit has been reached for System Users | |||||
Checked in 'Validate' event as we don't want welcome email sent if max users are exceeded. | |||||
''' | |||||
if self.user_type == "Website User": | |||||
return | |||||
if not self.enabled: | |||||
# don't validate max users when saving a disabled user | |||||
return | |||||
limits = get_limits() | |||||
if not limits.users: | |||||
# no limits defined | |||||
return | |||||
total_users = get_total_users() | |||||
if self.is_new(): | |||||
# get_total_users gets existing users in database | |||||
# a new record isn't inserted yet, so adding 1 | |||||
total_users += 1 | |||||
if total_users > limits.users: | |||||
frappe.throw(_("Sorry. You have reached the maximum user limit for your subscription. You can either disable an existing user or buy a higher subscription plan."), | |||||
MaxUsersReachedError) | |||||
@frappe.whitelist() | @frappe.whitelist() | ||||
def get_timezones(): | def get_timezones(): | ||||
import pytz | import pytz | ||||
@@ -6,32 +6,24 @@ frappe.pages['usage-info'].on_page_load = function(wrapper) { | |||||
}); | }); | ||||
frappe.call({ | frappe.call({ | ||||
method: "frappe.limits.get_usage_data", | |||||
method: "frappe.limits.get_usage_info", | |||||
callback: function(r) { | callback: function(r) { | ||||
var doc = r.message; | |||||
if(!doc.database_size) doc.database_size = 26; | |||||
if(!doc.files_size) doc.files_size = 1; | |||||
if(!doc.backup_size) doc.backup_size = 1; | |||||
if(typeof doc.space_limit !== "undefined") | |||||
{ | |||||
doc.max = flt(doc.space_limit * 1024); | |||||
var usage_info = r.message; | |||||
if (!usage_info) { | |||||
// nothing to show | |||||
// TODO improve this | |||||
return; | |||||
} | } | ||||
doc.total = (doc.database_size + doc.files_size + doc.backup_size); | |||||
doc.users = keys(frappe.boot.user_info).length - 2; | |||||
doc.today = frappe.datetime.get_today() | |||||
doc.total_days = frappe.datetime.get_day_diff(doc.expiry, doc.creation) | |||||
doc.used_days = frappe.datetime.get_day_diff(doc.today, doc.creation) | |||||
$(frappe.render_template("usage_info", doc)).appendTo(page.main); | $(frappe.render_template("usage_info", doc)).appendTo(page.main); | ||||
var btn_text = doc.user_limit == 1 ? __("Upgrade") : __("Renew / Upgrade"); | |||||
var btn_text = usage_info.limits.users == 1 ? __("Upgrade") : __("Renew / Upgrade"); | |||||
if(doc.limits_upgrade_link) { | |||||
page.set_primary_action(btn_text, function() { | |||||
frappe.set_route("upgrade"); | |||||
}); | |||||
} | |||||
if(usage_info.upgrade_link) { | |||||
page.set_primary_action(btn_text, function() { | |||||
window.open(usage_info.upgrade_link); | |||||
}); | |||||
} | |||||
} | } | ||||
}); | }); | ||||
@@ -421,7 +421,7 @@ function load_frappe_slides() { | |||||
}, | }, | ||||
frappe.wiz.user= { | |||||
frappe.wiz.user = { | |||||
app_name: "frappe", | app_name: "frappe", | ||||
title: __("The First User: You"), | title: __("The First User: You"), | ||||
icon: "icon-user", | icon: "icon-user", | ||||
@@ -465,5 +465,8 @@ frappe.wiz.on("before_load", function() { | |||||
// add welcome slide | // add welcome slide | ||||
frappe.wiz.add_slide(frappe.wiz.welcome); | frappe.wiz.add_slide(frappe.wiz.welcome); | ||||
frappe.wiz.add_slide(frappe.wiz.region); | frappe.wiz.add_slide(frappe.wiz.region); | ||||
frappe.wiz.add_slide(frappe.wiz.user); | |||||
if (!(frappe.boot.limits && frappe.boot.limits.users===1)) { | |||||
frappe.wiz.add_slide(frappe.wiz.user); | |||||
} | |||||
}); | }); |
@@ -163,10 +163,7 @@ def prettify_args(args): | |||||
return pretty_args | return pretty_args | ||||
def email_setup_wizard_exception(traceback, args): | def email_setup_wizard_exception(traceback, args): | ||||
from frappe.limits import get_limits | |||||
frappe_limits = get_limits() | |||||
if not frappe_limits.get('setup_wizard_exception_email'): | |||||
if not frappe.local.conf.setup_wizard_exception_email: | |||||
return | return | ||||
pretty_args = prettify_args(args) | pretty_args = prettify_args(args) | ||||
@@ -210,12 +207,8 @@ def email_setup_wizard_exception(traceback, args): | |||||
headers=frappe.local.request.headers, | headers=frappe.local.request.headers, | ||||
accept_languages=", ".join(frappe.local.request.accept_languages.values())) | accept_languages=", ".join(frappe.local.request.accept_languages.values())) | ||||
frappe.sendmail(recipients=frappe_limits.get('setup_wizard_exception_email'), | |||||
frappe.sendmail(recipients=frappe.local.conf.setup_wizard_exception_email, | |||||
sender=frappe.session.user, | sender=frappe.session.user, | ||||
subject="Exception in Setup Wizard - {}".format(frappe.local.site), | subject="Exception in Setup Wizard - {}".format(frappe.local.site), | ||||
message=message) | message=message) | ||||
def set_setup_complete(*args): | |||||
from frappe.limits import set_limits | |||||
set_limits({'setup_complete' : 1 , 'creation': frappe.utils.today()}) |
@@ -4,12 +4,20 @@ | |||||
from __future__ import unicode_literals | from __future__ import unicode_literals | ||||
import frappe | import frappe | ||||
from frappe import _ | |||||
from frappe.model.document import Document | from frappe.model.document import Document | ||||
from frappe.email.queue import send_one | from frappe.email.queue import send_one | ||||
from frappe.limits import get_limits | |||||
class EmailQueue(Document): | class EmailQueue(Document): | ||||
pass | |||||
def on_trash(self): | |||||
self.prevent_email_queue_delete() | |||||
def prevent_email_queue_delete(self): | |||||
'''If email limit is set, don't allow users to delete Email Queue record''' | |||||
if get_limits().emails and frappe.session.user != 'Administrator': | |||||
frappe.throw(_('Only Administrator can delete Email Queue')) | |||||
@frappe.whitelist() | @frappe.whitelist() | ||||
def retry_sending(name): | def retry_sending(name): | ||||
@@ -147,15 +147,18 @@ def check_email_limit(recipients): | |||||
or frappe.flags.in_test): | or frappe.flags.in_test): | ||||
# get count of mails sent this month | # get count of mails sent this month | ||||
this_month = frappe.db.sql("""select count(name) from `tabEmail Queue` where | |||||
status='Sent' and MONTH(creation)=MONTH(CURDATE())""")[0][0] | |||||
this_month = get_emails_sent_this_month() | |||||
monthly_email_limit = frappe.conf.get('monthly_email_limit') or 500 | |||||
monthly_email_limit = frappe.conf.get('limits', {}).get('emails') or 500 | |||||
if (this_month + len(recipients)) > monthly_email_limit: | if (this_month + len(recipients)) > monthly_email_limit: | ||||
throw(_("Cannot send this email. You have crossed the sending limit of {0} emails for this month.").format(monthly_email_limit), | throw(_("Cannot send this email. You have crossed the sending limit of {0} emails for this month.").format(monthly_email_limit), | ||||
EmailLimitCrossedError) | EmailLimitCrossedError) | ||||
def get_emails_sent_this_month(): | |||||
return frappe.db.sql("""select count(name) from `tabEmail Queue` where | |||||
status='Sent' and MONTH(creation)=MONTH(CURDATE())""")[0][0] | |||||
def get_unsubscribe_link(reference_doctype, reference_name, | def get_unsubscribe_link(reference_doctype, reference_name, | ||||
email, recipients, expose_recipients, show_as_cc, | email, recipients, expose_recipients, show_as_cc, | ||||
unsubscribe_method, unsubscribe_params, unsubscribe_message): | unsubscribe_method, unsubscribe_params, unsubscribe_message): | ||||
@@ -334,8 +337,3 @@ def clear_outbox(): | |||||
"""Remove mails older than 31 days in Outbox. Called daily via scheduler.""" | """Remove mails older than 31 days in Outbox. Called daily via scheduler.""" | ||||
frappe.db.sql("""delete from `tabEmail Queue` where | frappe.db.sql("""delete from `tabEmail Queue` where | ||||
datediff(now(), creation) > 31""") | datediff(now(), creation) > 31""") | ||||
def prevent_email_queue_delete(doc, method): | |||||
from frappe.limits import get_limits | |||||
if frappe.session.user != 'Administrator' and get_limits().get('block_bulk_email_delete'): | |||||
frappe.throw(_('Only Administrator can delete Email Queue')) |
@@ -14,7 +14,6 @@ app_email = "info@frappe.io" | |||||
before_install = "frappe.utils.install.before_install" | before_install = "frappe.utils.install.before_install" | ||||
after_install = "frappe.utils.install.after_install" | after_install = "frappe.utils.install.after_install" | ||||
extend_bootinfo = "frappe.limits.load_limits" | |||||
page_js = { | page_js = { | ||||
"setup-wizard": "public/js/frappe/setup_wizard.js" | "setup-wizard": "public/js/frappe/setup_wizard.js" | ||||
@@ -96,12 +95,6 @@ standard_queries = { | |||||
} | } | ||||
doc_events = { | doc_events = { | ||||
"User": { | |||||
"validate": "frappe.utils.user.validate_user_limit" | |||||
}, | |||||
"Email Queue": { | |||||
"on_trash": "frappe.email.queue.prevent_email_queue_delete" | |||||
}, | |||||
"*": { | "*": { | ||||
"after_insert": "frappe.email.doctype.email_alert.email_alert.trigger_email_alerts", | "after_insert": "frappe.email.doctype.email_alert.email_alert.trigger_email_alerts", | ||||
"validate": "frappe.email.doctype.email_alert.email_alert.trigger_email_alerts", | "validate": "frappe.email.doctype.email_alert.email_alert.trigger_email_alerts", | ||||
@@ -141,7 +134,7 @@ scheduler_events = { | |||||
"frappe.async.remove_old_task_logs", | "frappe.async.remove_old_task_logs", | ||||
"frappe.utils.scheduler.disable_scheduler_on_expiry", | "frappe.utils.scheduler.disable_scheduler_on_expiry", | ||||
"frappe.utils.scheduler.restrict_scheduler_events_if_dormant", | "frappe.utils.scheduler.restrict_scheduler_events_if_dormant", | ||||
"frappe.core.doctype.file.file.update_sizes" | |||||
"frappe.limits.update_space_usage" | |||||
], | ], | ||||
"daily_long": [ | "daily_long": [ | ||||
@@ -180,5 +173,4 @@ bot_parsers = [ | |||||
] | ] | ||||
setup_wizard_exception = "frappe.desk.page.setup_wizard.setup_wizard.email_setup_wizard_exception" | setup_wizard_exception = "frappe.desk.page.setup_wizard.setup_wizard.email_setup_wizard_exception" | ||||
setup_wizard_success = "frappe.desk.page.setup_wizard.setup_wizard.set_setup_complete" | |||||
before_write_file = "frappe.core.doctype.file.file.validate_space_limit" | |||||
before_write_file = "frappe.limits.validate_space_limit" |
@@ -1,25 +1,34 @@ | |||||
from __future__ import unicode_literals | from __future__ import unicode_literals | ||||
import frappe | import frappe | ||||
from frappe.utils import now_datetime, getdate | |||||
from frappe import _ | |||||
from frappe.utils import now_datetime, getdate, flt, cint, get_fullname | |||||
from frappe.installer import update_site_config | from frappe.installer import update_site_config | ||||
from frappe.utils.data import formatdate | from frappe.utils.data import formatdate | ||||
from frappe import _ | |||||
from frappe.utils.user import get_enabled_system_users | |||||
import os, subprocess, urlparse, urllib | |||||
class SiteExpiredError(frappe.ValidationError): | class SiteExpiredError(frappe.ValidationError): | ||||
pass | pass | ||||
EXPIRY_WARNING_DAYS = 10 | EXPIRY_WARNING_DAYS = 10 | ||||
def load_limits(bootinfo): | |||||
bootinfo["frappe_limits"] = get_limits() | |||||
bootinfo["expiry_message"] = get_expiry_message() | |||||
def check_if_expired(): | |||||
"""check if account is expired. If expired, do not allow login""" | |||||
if not has_expired(): | |||||
return | |||||
# if expired, stop user from logging in | |||||
expires_on = formatdate(get_limits().get("expiry")) | |||||
support_email = get_limits().get("support_email") or _("your provider") | |||||
frappe.throw(_("""Your subscription expired on {0}. | |||||
To extend please send an email to {1}""").format(expires_on, support_email), | |||||
SiteExpiredError) | |||||
def has_expired(): | def has_expired(): | ||||
if frappe.session.user=="Administrator": | if frappe.session.user=="Administrator": | ||||
return False | return False | ||||
expires_on = get_limits().get("expiry") | |||||
expires_on = get_limits().expiry | |||||
if not expires_on: | if not expires_on: | ||||
return False | return False | ||||
@@ -28,18 +37,6 @@ def has_expired(): | |||||
return True | return True | ||||
def check_if_expired(): | |||||
"""check if account is expired. If expired, do not allow login""" | |||||
if not has_expired(): | |||||
return | |||||
# if expired, stop user from logging in | |||||
expires_on = formatdate(get_limits().get("expiry")) | |||||
support_email = get_limits().get("support_email") or _("your provider") | |||||
frappe.throw(_("""Your subscription expired on {0}. | |||||
To extend please send an email to {1}""").format(expires_on, support_email), | |||||
SiteExpiredError) | |||||
def get_expiry_message(): | def get_expiry_message(): | ||||
if "System Manager" not in frappe.get_roles(): | if "System Manager" not in frappe.get_roles(): | ||||
return "" | return "" | ||||
@@ -67,30 +64,147 @@ def get_expiry_message(): | |||||
return message | return message | ||||
@frappe.whitelist() | |||||
def get_usage_info(): | |||||
'''Get data to show for Usage Info''' | |||||
# imported here to prevent circular import | |||||
from frappe.email.queue import get_emails_sent_this_month | |||||
limits = get_limits() | |||||
if not (limits and any([limits.users, limits.space, limits.emails, limits.expiry])): | |||||
# no limits! | |||||
return | |||||
limits.space = limits.space * 1024.0 # to MB | |||||
if not limits.space_usage: | |||||
# hack! to show some progress | |||||
limits.space_usage = { | |||||
'database_size': 26, | |||||
'files_size': 1, | |||||
'backup_size': 1, | |||||
'total': 28 | |||||
} | |||||
usage_info = frappe._dict({ | |||||
'limits': limits, | |||||
'enabled_users': get_enabled_system_users(), | |||||
'emails_sent': get_emails_sent_this_month(), | |||||
'space_usage': limits.space_usage['total'], | |||||
}) | |||||
if limits.expiry: | |||||
usage_info['days_to_expiry'] = (getdate(limits.expiry) - getdate()).days | |||||
if limits.upgrade_link: | |||||
usage_info['upgrade_link'] = get_upgrade_link(limits.upgrade_link) | |||||
return usage_info | |||||
def get_upgrade_link(upgrade_link): | |||||
parts = urlparse.urlsplit(upgrade_link) | |||||
params = dict(urlparse.parse_qsl(parts.query)) | |||||
params.update({ | |||||
'site': frappe.local.site, | |||||
'email': frappe.session.user, | |||||
'fullname': get_fullname() | |||||
}) | |||||
parts.query = urllib.urlencode(params) | |||||
url = urlparse.urlunparse(parts) | |||||
return url | |||||
def get_limits(): | def get_limits(): | ||||
return frappe.get_conf().get("limits") or {} | |||||
''' | |||||
"limits": { | |||||
"users": 1, | |||||
"space": 0.5, # in GB | |||||
"emails": 1000 # per month | |||||
"expiry": "2099-12-31" | |||||
} | |||||
''' | |||||
return frappe._dict(frappe.local.conf.limits or {}) | |||||
def update_limits(key, value): | |||||
'''Add/Update limit in site_config''' | |||||
limits = get_limits() | |||||
if isinstance(key, dict): | |||||
limits.update(key) | |||||
else: | |||||
limits[key] = value | |||||
@frappe.whitelist() | |||||
def get_usage_data(): | |||||
update_site_config("limits", limits, validate=False) | |||||
frappe.conf.limits = limits | |||||
def clear_limit(key): | |||||
'''Remove a limit option from site_config''' | |||||
limits = get_limits() | limits = get_limits() | ||||
day = frappe.utils.add_months(frappe.utils.today(), -1) | |||||
limits["emails_sent"] = frappe.db.count("Email Queue", filters={'creation': ['>', day]}) | |||||
return limits | |||||
if key in limits: | |||||
del limits[key] | |||||
update_site_config("limits", limits, validate=False) | |||||
frappe.conf.limits = limits | |||||
def validate_space_limit(file_size): | |||||
"""Stop from writing file if max space limit is reached""" | |||||
from frappe.utils.file_manager import MaxFileSizeReachedError | |||||
limits = get_limits() | |||||
if not limits.space: | |||||
return | |||||
# to MB | |||||
space_limit = flt(limits.space * 1024.0, 2) | |||||
# in MB | |||||
usage = frappe._dict(limits.space_usage or {}) | |||||
if not usage: | |||||
# first time | |||||
usage = frappe._dict(update_space_usage()) | |||||
file_size = file_size / (1024.0 ** 2) | |||||
if flt(usage.total + file_size, 2) > space_limit: | |||||
# Stop from attaching file | |||||
frappe.throw(_("You have exceeded the max space of {0} for your plan. {1}.").format( | |||||
"<b>{0}MB</b>".format(cint(space_limit)) if (space_limit < 1024) else "<b>{0}GB</b>".format(limits.space), | |||||
'<a href="#usage-info">{0}</a>'.format(_("Click here to check your usage or upgrade to a higher plan"))), | |||||
MaxFileSizeReachedError) | |||||
# update files size in frappe subscription | |||||
usage.files_size = flt(usage.files_size) + file_size | |||||
update_limits({ 'space_usage': usage }) | |||||
def update_space_usage(): | |||||
# public and private files | |||||
files_size = get_folder_size(frappe.get_site_path("public", "files")) | |||||
files_size += get_folder_size(frappe.get_site_path("private", "files")) | |||||
backup_size = get_folder_size(frappe.get_site_path("private", "backups")) | |||||
database_size = get_database_size() | |||||
usage = { | |||||
'files_size': files_size, | |||||
'backup_size': backup_size, | |||||
'database_size': database_size, | |||||
'total': flt(flt(files_size) + flt(backup_size) + flt(database_size), 2) | |||||
} | |||||
update_limits({ 'space_usage': usage }) | |||||
def set_limits(limits): | |||||
# Add/Update current config options in site_config | |||||
frappe_limits = get_limits() or {} | |||||
for key in limits.keys(): | |||||
frappe_limits[key] = limits[key] | |||||
return usage | |||||
update_site_config("limits", frappe_limits, validate=False) | |||||
def get_folder_size(path): | |||||
'''Returns folder size in MB if it exists''' | |||||
if os.path.exists(path): | |||||
return flt(subprocess.check_output(['du', '-ms', path]).split()[0]) | |||||
def get_database_size(): | |||||
'''Returns approximate database size in MB''' | |||||
db_name = frappe.conf.db_name | |||||
def clear_limit(limit): | |||||
frappe_limits = get_limits() | |||||
if limit in frappe_limits: | |||||
del frappe_limits[limit] | |||||
# This query will get the database size in MB | |||||
db_size = frappe.db.sql(''' | |||||
SELECT table_schema "database_name", sum( data_length + index_length ) / 1024 / 1024 "database_size" | |||||
FROM information_schema.TABLES WHERE table_schema = %s GROUP BY table_schema''', db_name, as_dict=True) | |||||
update_site_config("limits", frappe_limits, validate=False) | |||||
return db_size[0].get('database_size') |
@@ -1,18 +0,0 @@ | |||||
frappe.wiz.on("after_load", function() { | |||||
if (frappe.boot.frappe_limits.user_limit===1) { | |||||
// remove users slide | |||||
var users_slide; | |||||
for (var i in frappe.wiz.wizard.slides) { | |||||
var slide = frappe.wiz.wizard.slides[i]; | |||||
if (slide.title === frappe.wiz.user.title) { | |||||
users_slide = i; | |||||
break; | |||||
} | |||||
} | |||||
if (users_slide >= 0) { | |||||
frappe.wiz.wizard.slides.splice(users_slide, 1); | |||||
} | |||||
} | |||||
}); |
@@ -5,51 +5,56 @@ $(document).on("toolbar_setup", function() { | |||||
var help_links = []; | var help_links = []; | ||||
var support_link = "#upgrade"; | var support_link = "#upgrade"; | ||||
var chat_link = "#upgrade"; | var chat_link = "#upgrade"; | ||||
frappe_limits = frappe.boot.frappe_limits | |||||
var limits = frappe.boot.limits; | |||||
if(frappe.boot.expiry_message) { | if(frappe.boot.expiry_message) { | ||||
frappe.msgprint(frappe.boot.expiry_message) | frappe.msgprint(frappe.boot.expiry_message) | ||||
} | } | ||||
if(frappe_limits.support_email || frappe_limits.support_chat) { | |||||
if(limits.support_email || limits.support_chat) { | |||||
help_links.push('<li class="divider"></li>'); | help_links.push('<li class="divider"></li>'); | ||||
} | } | ||||
if(frappe_limits.support_email) { | |||||
support_link = 'mailto:'+frappe.boot.frappe_limits.support_email; | |||||
if(limits.support_email) { | |||||
support_link = 'mailto:'+frappe.boot.limits.support_email; | |||||
help_links.push('<li><a href="'+support_link+'">' + frappe._('Email Support') + '</a></li>'); | help_links.push('<li><a href="'+support_link+'">' + frappe._('Email Support') + '</a></li>'); | ||||
} | |||||
if(frappe_limits.support_chat) { | |||||
chat_link = frappe_limits.support_chat; | |||||
help_links.push('<li><a href="'+chat_link+'" target="_blank">' + frappe._('Chat Support') + '</a></li>'); | |||||
} | } | ||||
if(limits.support_chat) { | |||||
help_links.push('<li><a href="'+limits.support_chat+'" target="_blank">' + frappe._('Chat Support') + '</a></li>'); | |||||
} | |||||
$(help_links.join("\n")).insertBefore($("#toolbar-user").find(".divider:last")); | $(help_links.join("\n")).insertBefore($("#toolbar-user").find(".divider:last")); | ||||
if(frappe_limits.space_limit || frappe_limits.user_limit || frappe_limits.expiry || frappe_limits.email_limit) | |||||
{ | |||||
help_links = []; | |||||
help_links.push('<li><a href="#usage-info">' + frappe._('Usage Info') + '</a></li>'); | |||||
help_links.push('<li class="divider"></li>'); | |||||
if(limits.space || limits.users || limits.expiry || limits.emails) { | |||||
help_links = []; | |||||
help_links.push('<li><a href="#usage-info">' + frappe._('Usage Info') + '</a></li>'); | |||||
help_links.push('<li class="divider"></li>'); | |||||
$(help_links.join("\n")).insertBefore($("#toolbar-user").find("li:first")); | |||||
} | } | ||||
$(help_links.join("\n")).insertBefore($("#toolbar-user").find("li:first")); | |||||
}); | }); | ||||
frappe.get_form_sidebar_extension = function() { | frappe.get_form_sidebar_extension = function() { | ||||
var fs = frappe.boot.frappe_limits; | |||||
if(!fs.sidebar_usage_html) { | |||||
fs.total_used = flt(fs.database_size + fs.backup_size + fs.files_size); | |||||
fs.total_used_percent = cint(fs.total_used / flt(fs.max_space * 1024) * 100); | |||||
var template = '<ul class="list-unstyled sidebar-menu">\ | |||||
<li class="usage-stats">\ | |||||
<a href="#usage-info" class="text-muted">{{ fs.total_used }}MB ({{ fs.total_used_percent }}%) used</a></li>\ | |||||
</ul>'; | |||||
fs.sidebar_usage_html = frappe.render(template, {fs:fs}, "form_sidebar_usage"); | |||||
var limits = frappe.boot.limits; | |||||
if (!limits.usage) { limits.usage = {}; } | |||||
var usage = limits.usage; | |||||
if(!usage.sidebar_usage_html) { | |||||
if (limits.space) { | |||||
usage.total_used = flt(usage.database_size) + flt(usage.backup_size) + flt(usage.files_size); | |||||
usage.total_used_percent = cint(usage.total_used / flt(limits.space * 1024) * 100); | |||||
var template = '<ul class="list-unstyled sidebar-menu">\ | |||||
<li class="usage-stats">\ | |||||
<a href="#usage-info" class="text-muted">{{ usage.total_used }}MB ({{ usage.total_used_percent }}%) used</a></li>\ | |||||
</ul>'; | |||||
usage.sidebar_usage_html = frappe.render(template, { 'usage': usage }, "form_sidebar_usage"); | |||||
} else { | |||||
usage.sidebar_usage_html = ''; | |||||
} | |||||
} | } | ||||
return fs.sidebar_usage_html; | |||||
return usage.sidebar_usage_html; | |||||
} | } |
@@ -7,7 +7,7 @@ from frappe.utils.scheduler import (enqueue_applicable_events, restrict_schedule | |||||
from frappe import _dict | from frappe import _dict | ||||
from frappe.utils.background_jobs import enqueue | from frappe.utils.background_jobs import enqueue | ||||
from frappe.utils import now_datetime, today, add_days, add_to_date | from frappe.utils import now_datetime, today, add_days, add_to_date | ||||
from frappe.limits import set_limits, clear_limit | |||||
from frappe.limits import update_limits, clear_limit | |||||
import frappe | import frappe | ||||
import json, time | import json, time | ||||
@@ -56,7 +56,7 @@ class TestScheduler(TestCase): | |||||
def test_restrict_scheduler_events(self): | def test_restrict_scheduler_events(self): | ||||
frappe.set_user("Administrator") | frappe.set_user("Administrator") | ||||
user = frappe.get_doc("User", "Administrator") | user = frappe.get_doc("User", "Administrator") | ||||
dormant_date = add_days(today(), -5) | |||||
dormant_date = add_days(today(), -5) | |||||
user.last_active = dormant_date | user.last_active = dormant_date | ||||
user.save() | user.save() | ||||
@@ -69,7 +69,7 @@ class TestScheduler(TestCase): | |||||
def test_disable_scheduler_on_expiry(self): | def test_disable_scheduler_on_expiry(self): | ||||
set_limits({'expiry': add_to_date(today(), days=-1)}) | |||||
update_limits({'expiry': add_to_date(today(), days=-1)}) | |||||
frappe.local.conf = _dict(frappe.get_site_config()) | frappe.local.conf = _dict(frappe.get_site_config()) | ||||
user = frappe.new_doc('User') | user = frappe.new_doc('User') | ||||
@@ -8,8 +8,6 @@ from frappe import _dict | |||||
import frappe.share | import frappe.share | ||||
from frappe import _ | from frappe import _ | ||||
class MaxUsersReachedError(frappe.ValidationError): pass | |||||
class UserPermissions: | class UserPermissions: | ||||
""" | """ | ||||
A user permission object can be accessed as `frappe.get_user()` | A user permission object can be accessed as `frappe.get_user()` | ||||
@@ -307,36 +305,3 @@ def get_users(): | |||||
}) | }) | ||||
return users | return users | ||||
def validate_user_limit(doc, method): | |||||
""" | |||||
This is called using validate hook, because welcome email is sent in on_update. | |||||
We don't want welcome email sent if max users are exceeded. | |||||
""" | |||||
from frappe.limits import get_limits | |||||
from frappe.core.doctype.user.user import get_total_users | |||||
frappe_limits = get_limits() | |||||
if doc.user_type == "Website User": | |||||
return | |||||
if not doc.enabled: | |||||
# don't validate max users when saving a disabled user | |||||
return | |||||
user_limit = frappe_limits.get("user_limit") if frappe_limits else None | |||||
if not user_limit: | |||||
return | |||||
total_users = get_total_users() | |||||
if doc.is_new(): | |||||
# get_total_users gets existing users in database | |||||
# a new record isn't inserted yet, so adding 1 | |||||
total_users += 1 | |||||
if total_users > user_limit: | |||||
frappe.throw(_("Sorry. You have reached the maximum user limit for your subscription. You can either disable an existing user or buy a higher subscription plan."), | |||||
MaxUsersReachedError) |
@@ -56,7 +56,7 @@ def render(path, http_status_code=None): | |||||
except frappe.Redirect, e: | except frappe.Redirect, e: | ||||
return build_response(path, "", 301, { | return build_response(path, "", 301, { | ||||
"Location": frappe.flags.redirect_location, | |||||
"Location": frappe.flags.redirect_location or (frappe.local.response or {}).get('location'), | |||||
"Cache-Control": "no-store, no-cache, must-revalidate" | "Cache-Control": "no-store, no-cache, must-revalidate" | ||||
}) | }) | ||||