diff --git a/frappe/commands/site.py b/frappe/commands/site.py index b53a6299b3..06c8a47274 100644 --- a/frappe/commands/site.py +++ b/frappe/commands/site.py @@ -4,6 +4,9 @@ import hashlib, os import frappe from frappe.commands import pass_context, get_site from frappe.commands.scheduler import _is_scheduler_enabled +from frappe.commands import get_site +from frappe.limits import set_limits, get_limits +from frappe.installer import update_site_config @click.command('new-site') @click.argument('site') @@ -329,6 +332,54 @@ def set_admin_password(context, admin_password): finally: frappe.destroy() + +@click.command('set-limit') +@click.option('--site', help='site name') +@click.argument('limit', type=click.Choice(['email', 'space', 'user', 'expiry'])) +@click.argument('value') +@pass_context +def set_limit(context, site, limit, value): + """Sets user / space / email limit for a site""" + import datetime + if not site: + site = get_site(context) + + with frappe.init_site(site): + if limit == 'expiry': + try: + datetime.datetime.strptime(value, '%Y-%m-%d') + except ValueError: + raise ValueError("Incorrect data format, should be YYYY-MM-DD") + else: + limit += '_limit' + # Space can be float, while other should be integers + val = float(value) if limit == 'space_limit' else int(value) + + set_limits({limit : value}) + + +@click.command('clear-limit') +@click.option('--site', help='site name') +@click.argument('limit', type=click.Choice(['email', 'space', 'user', 'expiry'])) +@pass_context +def clear_limit(context, site, limit): + """Clears given limit from the site config, and removes limit from site config if its empty""" + from frappe.limits import clear_limit as _clear_limit + if not site: + site = get_site(context) + + with frappe.init_site(site): + if not limit == 'expiry': + limit += '_limit' + + _clear_limit(limit) + + # Remove limits from the site_config, if it's empty + cur_limits = get_limits() + if not cur_limits: + update_site_config('limits', 'None', validate=False) + + commands = [ add_system_manager, backup, @@ -344,5 +395,7 @@ commands = [ run_patch, set_admin_password, uninstall, + set_limit, + clear_limit, _use, ] diff --git a/frappe/commands/utils.py b/frappe/commands/utils.py index 894f51a550..b588dba2d2 100644 --- a/frappe/commands/utils.py +++ b/frappe/commands/utils.py @@ -371,13 +371,17 @@ def make_app(destination, app_name): @click.command('set-config') @click.argument('key') @click.argument('value') +@click.option('--as-dict', is_flag=True, default=False) @pass_context -def set_config(context, key, value): +def set_config(context, key, value, as_dict=False): "Insert/Update a value in site_config.json" from frappe.installer import update_site_config + import ast + if as_dict: + value = ast.literal_eval(value) for site in context.sites: frappe.init(site=site) - update_site_config(key, value) + update_site_config(key, value, validate=False) frappe.destroy() @click.command('version') diff --git a/frappe/core/doctype/file/file.py b/frappe/core/doctype/file/file.py old mode 100644 new mode 100755 index f0a14e8e37..29c19a2d59 --- a/frappe/core/doctype/file/file.py +++ b/frappe/core/doctype/file/file.py @@ -8,21 +8,21 @@ record of files naming for same name files: file.gif, file-1.gif, file-2.gif etc """ -import frappe, frappe.utils -from frappe.utils.file_manager import delete_file_data_content, get_content_hash, get_random_filename -from frappe import _ - -from frappe.utils.nestedset import NestedSet -from frappe.utils import strip +import frappe import json import urllib -from PIL import Image, ImageOps -import os +import os, subprocess import requests import requests.exceptions import StringIO import mimetypes, imghdr -from frappe.utils import get_files_path + +from frappe.utils.file_manager import delete_file_data_content, get_content_hash, get_random_filename +from frappe import _ +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 PIL import Image, ImageOps class FolderNotEmpty(frappe.ValidationError): pass @@ -351,3 +351,67 @@ def check_file_permission(file_url): return True 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: + return + + 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( + "{0}MB".format(cint(space_limit)) if (space_limit < 1024) else "{0}GB".format(frappe_limits['space_limit']), + '{0}'.format(_("Click here to check your usage")), + '{0}'.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') diff --git a/frappe/core/doctype/file/test_file.py b/frappe/core/doctype/file/test_file.py index 60ff647894..67d0b1f610 100644 --- a/frappe/core/doctype/file/test_file.py +++ b/frappe/core/doctype/file/test_file.py @@ -95,3 +95,20 @@ class TestFile(unittest.TestCase): folder = frappe.get_doc("File", "Home/Test Folder 1/Test Folder 3") self.assertRaises(frappe.ValidationError, folder.delete) + + def test_file_upload_limit(self): + from frappe.utils.file_manager import MaxFileSizeReachedError + from frappe.limits import set_limits, clear_limit + from frappe import _dict + + set_limits({'space_limit': 1, 'files_size': (1024 * 1024), 'database_size': 0, 'backup_size': 0}) + + # Rebuild the frappe.local.conf to take up the changes from site_config + frappe.local.conf = _dict(frappe.get_site_config()) + + self.assertRaises(MaxFileSizeReachedError, save_file, '_test_max_space.txt', + 'This files test for max space usage', "", "", self.get_folder("Test Folder 2", "Home").name) + + # Scrub the site_config and rebuild frappe.local.conf + clear_limit("space_limit") + frappe.local.conf = _dict(frappe.get_site_config()) \ No newline at end of file diff --git a/frappe/core/doctype/user/test_user.py b/frappe/core/doctype/user/test_user.py index 0e3aa4aa55..32169b9a6e 100644 --- a/frappe/core/doctype/user/test_user.py +++ b/frappe/core/doctype/user/test_user.py @@ -3,8 +3,14 @@ from __future__ import unicode_literals import frappe, unittest +import requests from frappe.model.delete_doc import delete_doc +from frappe.utils.data import today, add_to_date +from frappe import _dict +from frappe.limits import SiteExpiredError, set_limits, clear_limit +from frappe.utils import get_url +from frappe.installer import update_site_config test_records = frappe.get_test_records('User') @@ -72,3 +78,40 @@ class TestUser(unittest.TestCase): me.add_roles("System Manager") self.assertTrue("System Manager" in [d.role for d in me.get("user_roles")]) + + def test_user_limit_for_site(self): + from frappe.core.doctype.user.user import get_total_users + + set_limits({'user_limit': get_total_users()}) + + # reload site config + from frappe import _dict + frappe.local.conf = _dict(frappe.get_site_config()) + + # Create a new user + user = frappe.new_doc('User') + user.email = 'test_max_users@example.com' + user.first_name = 'Test_max_user' + + self.assertRaises(frappe.utils.user.MaxUsersReachedError, user.add_roles, 'System Manager') + + if frappe.db.exists('User', 'test_max_users@example.com'): + frappe.delete_doc('User', 'test_max_users@example.com') + + # Clear the user limit + clear_limit('user_limit') + + def test_site_expiry(self): + set_limits({'expiry': add_to_date(today(), days=-1)}) + frappe.local.conf = _dict(frappe.get_site_config()) + + frappe.db.commit() + + res = requests.post(get_url(), params={'cmd': 'login', 'usr': 'test@example.com', 'pwd': 'testpassword', + 'device': 'desktop'}) + + # While site is expired status code returned is 417 Failed Expectation + self.assertEqual(res.status_code, 417) + + clear_limit("expiry") + frappe.local.conf = _dict(frappe.get_site_config()) diff --git a/frappe/core/page/usage_info/__init__.py b/frappe/core/page/usage_info/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/core/page/usage_info/usage_info.css b/frappe/core/page/usage_info/usage_info.css new file mode 100644 index 0000000000..f1418dae0c --- /dev/null +++ b/frappe/core/page/usage_info/usage_info.css @@ -0,0 +1,3 @@ +#page-usage-info .indicator-right::after { + margin-left: 8px; +} diff --git a/frappe/core/page/usage_info/usage_info.html b/frappe/core/page/usage_info/usage_info.html new file mode 100644 index 0000000000..f43424974b --- /dev/null +++ b/frappe/core/page/usage_info/usage_info.html @@ -0,0 +1,71 @@ +
Current Users | +Max Users | +Remaining | +
---|---|---|
{%= users %} | +{%= user_limit %} | +{%= user_limit - users %} | +
Type | +Size (MB) | +
---|---|
Database Size | +{%= database_size %} MB | +
Files Size | +{%= files_size %} MB | +
Backup Size | +{%= backup_size %} MB | +
Total | +{%= total %} MB | +
Available | ++ {%= flt(max - total, 2) %} MB | +
Scheduler didn't encounter any problems.
" - def scheduler_task(site, event, handler, now=False): '''This is a wrapper function that runs a hooks.scheduler_events method''' frappe.logger(__name__).info('running {handler} for {site} for event: {event}'.format(handler=handler, site=site, event=event)) @@ -239,3 +243,47 @@ def scheduler_task(site, event, handler, now=False): frappe.db.commit() frappe.logger(__name__).info('ran {handler} for {site} for event: {event}'.format(handler=handler, site=site, event=event)) + + +def reset_enabled_scheduler_events(login_manager): + if login_manager.info.user_type == "System User": + try: + frappe.db.set_global('enabled_scheduler_events', None) + except MySQLdb.OperationalError, e: + if e.args[0]==1205: + frappe.get_logger().error("Error in reset_enabled_scheduler_events") + else: + raise + else: + is_dormant = frappe.conf.get('dormant') + if is_dormant: + update_site_config('dormant', 'None') + +def disable_scheduler_on_expiry(): + if has_expired(): + disable_scheduler() + +def restrict_scheduler_events_if_dormant(): + if is_dormant(): + restrict_scheduler_events() + update_site_config('dormant', True) + +def restrict_scheduler_events(*args, **kwargs): + val = json.dumps(["daily", "daily_long", "weekly", "weekly_long", "monthly", "monthly_long"]) + frappe.db.set_global('enabled_scheduler_events', val) + + +def is_dormant(since = 345600): + last_active = get_datetime(get_last_active()) + # Get now without tz info + now = now_datetime().replace(tzinfo=None) + time_since_last_active = now - last_active + if time_since_last_active.total_seconds() > since: # 4 days + return True + return False + +def get_last_active(): + return frappe.db.sql("""select max(ifnull(last_active, "2000-01-01 00:00:00")) from `tabUser` + where user_type = 'System User' and name not in ({standard_users})"""\ + .format(standard_users=", ".join(["%s"]*len(STANDARD_USERS))), + STANDARD_USERS)[0][0] diff --git a/frappe/utils/user.py b/frappe/utils/user.py old mode 100644 new mode 100755 index f9169968eb..6ff0c9b287 --- a/frappe/utils/user.py +++ b/frappe/utils/user.py @@ -6,6 +6,9 @@ from __future__ import unicode_literals import frappe, json from frappe import _dict import frappe.share +from frappe import _ + +class MaxUsersReachedError(frappe.ValidationError): pass class UserPermissions: """ @@ -203,11 +206,11 @@ class UserPermissions: d.all_reports = self.get_all_reports() return d - + def get_all_reports(self): - reports = frappe.db.sql("""select name, report_type, ref_doctype from tabReport + reports = frappe.db.sql("""select name, report_type, ref_doctype from tabReport where ref_doctype in ('{0}')""".format("', '".join(self.can_get_report)), as_dict=1) - + return frappe._dict((d.name, d) for d in reports) def get_user_fullname(user): @@ -291,6 +294,7 @@ def is_website_user(): def is_system_user(username): return frappe.db.get_value("User", {"name": username, "enabled": 1, "user_type": "System User"}) + def get_users(): from frappe.core.doctype.user.user import get_system_users users = [] @@ -303,3 +307,36 @@ def get_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) diff --git a/frappe/website/utils.py b/frappe/website/utils.py index c4d78696c7..044da7c256 100644 --- a/frappe/website/utils.py +++ b/frappe/website/utils.py @@ -207,4 +207,3 @@ def get_full_index(route=None, doctype="Web Page", extn = False): return children return get_children(route or "") -