Browse Source

[fix] limits variables and how usage is stored within limits + additional fixes

version-14
Anand Doshi 9 years ago
parent
commit
3854bcad04
19 changed files with 286 additions and 255 deletions
  1. +1
    -1
      frappe/__init__.py
  2. +5
    -0
      frappe/boot.py
  3. +10
    -13
      frappe/commands/site.py
  4. +2
    -63
      frappe/core/doctype/file/file.py
  5. +11
    -4
      frappe/core/doctype/file/test_file.py
  6. +4
    -4
      frappe/core/doctype/user/test_user.py
  7. +32
    -1
      frappe/core/doctype/user/user.py
  8. +12
    -20
      frappe/core/page/usage_info/usage_info.js
  9. +5
    -2
      frappe/desk/page/setup_wizard/setup_wizard.js
  10. +2
    -9
      frappe/desk/page/setup_wizard/setup_wizard.py
  11. +10
    -2
      frappe/email/doctype/email_queue/email_queue.py
  12. +6
    -8
      frappe/email/queue.py
  13. +2
    -10
      frappe/hooks.py
  14. +149
    -35
      frappe/limits.py
  15. +0
    -18
      frappe/public/js/frappe/setup_wizard.js
  16. +31
    -26
      frappe/public/js/frappe/toolbar.js
  17. +3
    -3
      frappe/tests/test_scheduler.py
  18. +0
    -35
      frappe/utils/user.py
  19. +1
    -1
      frappe/website/render.py

+ 1
- 1
frappe/__init__.py View File

@@ -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))

+ 5
- 0
frappe/boot.py View File

@@ -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):


+ 10
- 13
frappe/commands/site.py View File

@@ -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)






+ 2
- 63
frappe/core/doctype/file/file.py View File

@@ -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')

+ 11
- 4
frappe/core/doctype/file/test_file.py View File

@@ -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())

+ 4
- 4
frappe/core/doctype/user/test_user.py View File

@@ -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()


+ 32
- 1
frappe/core/doctype/user/user.py View File

@@ -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


+ 12
- 20
frappe/core/page/usage_info/usage_info.js View File

@@ -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);
});
}
} }
}); });




+ 5
- 2
frappe/desk/page/setup_wizard/setup_wizard.js View File

@@ -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);
}
}); });

+ 2
- 9
frappe/desk/page/setup_wizard/setup_wizard.py View File

@@ -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()})

+ 10
- 2
frappe/email/doctype/email_queue/email_queue.py View File

@@ -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):


+ 6
- 8
frappe/email/queue.py View File

@@ -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'))

+ 2
- 10
frappe/hooks.py View File

@@ -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"

+ 149
- 35
frappe/limits.py View File

@@ -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')

+ 0
- 18
frappe/public/js/frappe/setup_wizard.js View File

@@ -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);
}

}
});

+ 31
- 26
frappe/public/js/frappe/toolbar.js View File

@@ -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;
} }

+ 3
- 3
frappe/tests/test_scheduler.py View File

@@ -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')


+ 0
- 35
frappe/utils/user.py View File

@@ -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)

+ 1
- 1
frappe/website/render.py View File

@@ -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"
}) })




Loading…
Cancel
Save