Merge branch 'develop'

This commit is contained in:
Rushabh Mehta 2016-08-12 11:52:32 +05:30
commit 525efb3f2c
25 changed files with 1733 additions and 1531 deletions

View file

@ -13,7 +13,7 @@ import os, sys, importlib, inspect, json
from .exceptions import *
from .utils.jinja import get_jenv, get_template, render_template
__version__ = "7.0.19"
__version__ = "7.0.20"
local = Local()

View file

@ -166,7 +166,7 @@ class LoginManager:
def clear_active_sessions(self):
"""Clear other sessions of the current user if `deny_multiple_sessions` is not set"""
if not (frappe.conf.get("deny_multiple_sessions") or cint(frappe.db.get_system_setting('deny_multiple_sessions'))):
if not (cint(frappe.conf.get("deny_multiple_sessions")) or cint(frappe.db.get_system_setting('deny_multiple_sessions'))):
return
if frappe.session.user != "Guest":

View file

@ -0,0 +1,2 @@
- New Feature: Ability to add multiple sessions to users. Edit **User** record and edit "Simultaneous Sessions"
- New Feature: Select columns to export and import in **Data Import Tool**

View file

@ -102,6 +102,59 @@ class TestUser(unittest.TestCase):
# Clear the user limit
clear_limit('users')
def test_user_limit_for_site_with_simultaneous_sessions(self):
from frappe.core.doctype.user.user import get_total_users
clear_limit('users')
# make sure this user counts
user = frappe.get_doc('User', 'test@example.com')
user.add_roles('Website Manager')
user.save()
update_limits({'users': get_total_users()})
user.simultaneous_sessions = user.simultaneous_sessions + 1
self.assertRaises(MaxUsersReachedError, user.save)
# Clear the user limit
clear_limit('users')
# def test_deny_multiple_sessions(self):
# clear_limit('users')
#
# # allow one session
# user = frappe.get_doc('User', 'test@example.com')
# user.simultaneous_sessions = 1
# user.new_password = 'testpassword'
# user.save()
#
# def test_request(conn):
# value = conn.get_value('User', 'first_name', {'name': 'test@example.com'})
# self.assertTrue('first_name' in value)
#
# from frappe.frappeclient import FrappeClient
# update_site_config('deny_multiple_sessions', 0)
#
# print 'conn1'
# conn1 = FrappeClient(get_url(), "test@example.com", "testpassword", verify=False)
# test_request(conn1)
#
# print 'conn2'
# conn2 = FrappeClient(get_url(), "test@example.com", "testpassword", verify=False)
# test_request(conn2)
#
# update_site_config('deny_multiple_sessions', 1)
#
# print 'conn3'
#
# conn3 = FrappeClient(get_url(), "test@example.com", "testpassword", verify=False)
# test_request(conn3)
#
# # first connection should fail
# test_request(conn1)
def test_site_expiry(self):
update_limits({'expiry': add_to_date(today(), days=-1)})
frappe.local.conf = _dict(frappe.get_site_config())

File diff suppressed because it is too large Load diff

View file

@ -58,11 +58,18 @@ class User(Document):
self.remove_all_roles_for_guest()
self.validate_username()
self.remove_disabled_roles()
self.validate_user_limit()
if self.language == "Loading...":
self.language = None
def on_update(self):
# clear new password
self.validate_user_limit()
self.share_with_self()
clear_notifications(user=self.name)
frappe.clear_cache(user=self.name)
self.send_password_notification(self.__new_password)
def check_demo(self):
if frappe.session.user == 'demo@erpnext.com':
frappe.throw('Cannot change user details in demo. Please signup for a new account at https://erpnext.com', title='Not Allowed')
@ -122,13 +129,6 @@ class User(Document):
else:
self.user_type = 'Website User'
def on_update(self):
# clear new password
self.share_with_self()
clear_notifications(user=self.name)
frappe.clear_cache(user=self.name)
self.send_password_notification(self.__new_password)
def share_with_self(self):
if self.user_type=="System User":
frappe.share.add(self.doctype, self.name, self.name, share=1,
@ -576,9 +576,11 @@ def user_query(doctype, txt, searchfield, start, page_len, filters):
key=searchfield, mcond=get_match_cond(doctype)),
tuple(list(STANDARD_USERS) + [txt, txt, txt, txt, start, page_len]))
def get_total_users(exclude_users=None):
def get_total_users():
"""Returns total no. of system users"""
return len(get_system_users(exclude_users=exclude_users))
return frappe.db.sql('''select sum(simultaneous_sessions) from `tabUser`
where enabled=1 and user_type="System User"
and name not in ({})'''.format(", ".join(["%s"]*len(STANDARD_USERS))), STANDARD_USERS)[0][0]
def get_system_users(exclude_users=None):
if not exclude_users:

View file

@ -14,19 +14,32 @@
<br>
</div>
</div>
<div class="export-import-section hide">
<div class="row" style="max-width: 700px;">
<div class="export-import-section hide" style="max-width: 700px;">
<h4>{{ __("1. Select Columns") }}</h4>
<p>
<a class="btn btn-default btn-xs btn-select-all" style="margin-right: 7px;">
{%= __("Select All") %}</a>
<a class="btn btn-default btn-xs btn-select-mandatory" style="margin-right: 7px;">
{%= __("Select Mandatory") %}</a>
<a class="btn btn-default btn-xs btn-unselect-all">
{%= __("Unselect All") %}</a>
</p>
<div class="select-columns">
</div>
<br>
<h4>{{ __("2. Download") }}</h4>
<div class="row">
<div class="col-sm-4">
<p><a class="btn btn-default btn-sm btn-download-template">
<p><a class="btn btn-primary btn-xs btn-download-template">
{%= __("Download Blank Template") %}</a></p>
</div>
<div class="col-sm-8">
<h6 class="text-muted">{%= __("Recommended for inserting new records.") %}</h6>
</div>
</div>
<div class="row" style="max-width: 700px;">
<div class="row">
<div class="col-sm-4">
<p><a class="btn btn-default btn-sm btn-download-data">
<p><a class="btn btn-primary btn-xs btn-download-data">
{%= __("Download with Data") %}</a></p>
</div>
<div class="col-sm-8">
@ -35,12 +48,17 @@
</div>
</div>
<div>
<hr>
<hr style="margin-top: 50px;">
<h3>{%= __("Import") %}</h3>
<p class="text-muted">{%= __("Update the template and save in CSV (Comma Separate Values) format before attaching.") %}</p>
<div class="row">
<div class="col-md-6">
<br>
<h4>{{ __("1. Select File") }}</h4>
<div class="upload-area"></div>
<br>
<h4>{{ __("2. Upload") }}</h4>
<div class="checkbox">
<label>
<input type="checkbox" name="always_insert">

View file

@ -34,28 +34,65 @@ frappe.DataImportTool = Class.extend({
$(frappe.render_template("data_import_main", this)).appendTo(this.page.main);
this.select = this.page.main.find("select.doctype");
this.select_columns = this.page.main.find('.select-columns');
this.select.on("change", function() {
me.doctype = $(this).val();
me.page.main.find(".export-import-section").toggleClass(!!me.doctype);
if(me.doctype) {
me.set_btn_links();
// set button links
}
});
},
set_btn_links: function() {
var doctype = encodeURIComponent(this.doctype);
this.page.main.find(".btn-download-template").attr("href",
"/api/method/frappe.core.page.data_import_tool.exporter.get_template?"
+ "doctype=" + doctype
+ "&parent_doctype=" + doctype
+ "&with_data=No&all_doctypes=Yes");
frappe.model.with_doctype(me.doctype, function() {
me.page.main.find(".export-import-section").toggleClass(!!me.doctype);
if(me.doctype) {
this.page.main.find(".btn-download-data").attr("href",
"/api/method/frappe.core.page.data_import_tool.exporter.get_template?"
// render select columns
var doctype_list = [frappe.get_doc('DocType', me.doctype)];
frappe.meta.get_table_fields(me.doctype).forEach(function(df) {
doctype_list.push(frappe.get_doc('DocType', df.options));
});
$(frappe.render_template("data_import_tool_columns", {doctype_list: doctype_list}))
.appendTo(me.select_columns.empty());
}
});
});
this.page.main.find('.btn-select-all').on('click', function() {
me.select_columns.find('.select-column-check').prop('checked', true);
});
this.page.main.find('.btn-unselect-all').on('click', function() {
me.select_columns.find('.select-column-check').prop('checked', false);
});
this.page.main.find('.btn-select-mandatory').on('click', function() {
me.select_columns.find('.select-column-check').prop('checked', false);
me.select_columns.find('.select-column-check[data-reqd="1"]').prop('checked', true);
});
this.page.main.find(".btn-download-template").on('click', function() {
window.open(me.get_export_url(false));
});
this.page.main.find(".btn-download-data").on('click', function() {
window.open(me.get_export_url(true));
});
},
get_export_url: function(with_data) {
var doctype = this.select.val();
var columns = {};
this.select_columns.find('.select-column-check:checked').each(function() {
var _doctype = $(this).attr('data-doctype');
var _fieldname = $(this).attr('data-fieldname');
if(!columns[_doctype]) {
columns[_doctype] = [];
}
columns[_doctype].push(_fieldname);
});
return "/api/method/frappe.core.page.data_import_tool.exporter.get_template?"
+ "doctype=" + doctype
+ "&parent_doctype=" + doctype
+ "&with_data=Yes&all_doctypes=Yes");
+ "&select_columns=" + JSON.stringify(columns)
+ "&with_data="+ (with_data ? 'Yes' : 'No')+"&all_doctypes=Yes";
},
make_upload: function() {
var me = this;

View file

@ -0,0 +1,21 @@
<div style="margin: 15px 0px;">
{% for doctype in doctype_list %}
<h5 style="margin-top: 25px; margin-bottom: 5px;">{{ doctype.name }}</h5>
<div class="row">
{% for f in doctype.fields %}
{% if (frappe.model.no_value_type.indexOf(f.fieldtype)===-1) %}
<div class="col-sm-4">
<div class="checkbox" style="margin: 5px 0px;">
<label>
<input type="checkbox" class="select-column-check"
data-fieldname="{{ f.fieldname }}" data-reqd="{{ f.reqd }}"
data-doctype="{{ doctype.name }}" checked>
<small>{{ __(f.label) }}</small>
</label>
</div>
</div>
{% endif %}
{% endfor %}
</div>
{% endfor %}
</div>

View file

@ -3,12 +3,12 @@
from __future__ import unicode_literals
import frappe, json, os
import frappe, json
from frappe import _
import frappe.permissions
import re
from frappe.utils.csvutils import UnicodeWriter
from frappe.utils import cstr, cint, flt, formatdate, format_datetime
from frappe.utils import cstr, formatdate, format_datetime
from frappe.core.page.data_import_tool.data_import_tool import get_data_keys
reflags = {
@ -22,8 +22,10 @@ reflags = {
}
@frappe.whitelist()
def get_template(doctype=None, parent_doctype=None, all_doctypes="No", with_data="No"):
def get_template(doctype=None, parent_doctype=None, all_doctypes="No", with_data="No", select_columns=None):
all_doctypes = all_doctypes=="Yes"
if select_columns:
select_columns = json.loads(select_columns);
docs_to_export = {}
if doctype:
if isinstance(doctype, basestring):
@ -84,6 +86,7 @@ def get_template(doctype=None, parent_doctype=None, all_doctypes="No", with_data
append_field_column(frappe._dict({
"fieldname": "name",
"parent": dt,
"label": "ID",
"fieldtype": "Data",
"reqd": 1,
@ -105,16 +108,27 @@ def get_template(doctype=None, parent_doctype=None, all_doctypes="No", with_data
column_start_end[dt].end = len(columns) + 1
def append_field_column(docfield, mandatory):
if docfield and ((mandatory and docfield.reqd) or not (mandatory or docfield.reqd)) \
and (docfield.fieldname not in ('parenttype', 'trash_reason')) and not docfield.hidden:
tablerow.append("")
fieldrow.append(docfield.fieldname)
labelrow.append(_(docfield.label))
mandatoryrow.append(docfield.reqd and 'Yes' or 'No')
typerow.append(docfield.fieldtype)
inforow.append(getinforow(docfield))
columns.append(docfield.fieldname)
def append_field_column(docfield, for_mandatory):
if not docfield:
return
if for_mandatory and not docfield.reqd:
return
if not for_mandatory and docfield.reqd:
return
if docfield.fieldname in ('parenttype', 'trash_reason'):
return
if docfield.hidden:
return
if select_columns and docfield.fieldname not in select_columns.get(docfield.parent, []):
return
tablerow.append("")
fieldrow.append(docfield.fieldname)
labelrow.append(_(docfield.label))
mandatoryrow.append(docfield.reqd and 'Yes' or 'No')
typerow.append(docfield.fieldtype)
inforow.append(getinforow(docfield))
columns.append(docfield.fieldname)
def append_empty_field_column():
tablerow.append("~")
@ -238,6 +252,8 @@ def get_template(doctype=None, parent_doctype=None, all_doctypes="No", with_data
inforow = [_('Info:'), '']
columns = [key]
build_field_columns(doctype)
if all_doctypes:
for d in child_doctypes:

View file

@ -291,6 +291,9 @@ def update_site_config(key, value, validate=True):
with open(get_site_config_path(), "w") as f:
f.write(json.dumps(site_config, indent=1, sort_keys=True))
if frappe.local.conf:
frappe.local.conf[key] = value
def get_site_config_path():
return os.path.join(frappe.local.site_path, "site_config.json")

View file

@ -149,7 +149,7 @@ def update_limits(limits_dict):
limits = get_limits()
limits.update(limits_dict)
update_site_config("limits", limits, validate=False)
frappe.conf.limits = limits
frappe.local.conf.limits = limits
def clear_limit(key):
'''Remove a limit option from site_config'''

View file

@ -30,9 +30,6 @@ def write_document_file(doc, record_module=None, create_init=None):
for fieldname in frappe.model.default_fields:
if fieldname in d:
del d[fieldname]
for fieldname in d.keys():
if d[fieldname] == 0 or d[fieldname] == "":
del d[fieldname]
module = record_module or get_module_name(doc)
if create_init is None:

View file

@ -134,9 +134,9 @@ body[data-route^="Module"] .main-menu .form-sidebar {
height: 0;
padding-bottom: 100%;
border-radius: 6px;
background-size: 100% 100%;
background-size: contain;
background-repeat: no-repeat;
background-position: center center;
background-position: center;
}
.form-sidebar .sidebar-image-section .standard-image {
font-size: 72px;

View file

@ -842,6 +842,7 @@ frappe.ui.form.ControlAttach = frappe.ui.form.ControlData.extend({
me.frm.attachments.remove_attachment_by_filename(me.value, function() {
me.parse_validate_and_set_in_model(null);
me.refresh();
me.frm.save();
});
} else {
this.dataurl = null;
@ -918,6 +919,7 @@ frappe.ui.form.ControlAttach = frappe.ui.form.ControlData.extend({
if(selected) {
me.parse_validate_and_set_in_model(selected);
me.dialog.hide();
me.frm.save();
} else {
msgprint(__("Please attach a file or set a URL"));
}
@ -988,6 +990,7 @@ frappe.ui.form.ControlAttach = frappe.ui.form.ControlData.extend({
this.parse_validate_and_set_in_model(attachment.file_url);
this.refresh();
this.frm.attachments.update_attachment(attachment);
this.frm.save();
} else {
this.value = this.get_value();
this.refresh();

View file

@ -64,6 +64,7 @@ frappe.form.formatters = {
},
Link: function(value, docfield, options, doc) {
var doctype = docfield._options || docfield.options;
var original_value = value;
if(value && value.match(/^['"].*['"]$/)) {
value.replace(/^.(.*).$/, "$1");
}
@ -85,7 +86,7 @@ frappe.form.formatters = {
} else if(docfield && doctype) {
return repl('<a class="grey" href="#Form/%(doctype)s/%(name)s" data-doctype="%(doctype)s">%(label)s</a>', {
doctype: encodeURIComponent(doctype),
name: encodeURIComponent(value),
name: encodeURIComponent(original_value),
label: __(options && options.label || value)
});
} else {

View file

@ -4,6 +4,15 @@
frappe.provide('frappe.utils');
frappe.utils = {
get_random: function(len) {
var text = "";
var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
for( var i=0; i < len; i++ )
text += possible.charAt(Math.floor(Math.random() * possible.length));
return text;
},
get_file_link: function(filename) {
filename = cstr(filename);
if(frappe.utils.is_url(filename)) {

View file

@ -170,7 +170,7 @@ frappe.upload = {
return;
}
var attachment = r.message;
opts.callback(attachment, r);
opts.callback && opts.callback(attachment, r);
$(document).trigger("upload_complete", attachment);
},
error: function(r) {

View file

@ -13,7 +13,7 @@ frappe.template.compile = function(str, name) {
// replace jinja style tags
str = str.replace(/{{/g, "{%=").replace(/}}/g, "%}");
// {% if not test %} --> {% if (!test) { %}
str = str.replace(/{%\s?if\s?\s?not\s?([^\(][^%{]+)\s?%}/g, "{% if (! $1) { %}")
@ -22,7 +22,13 @@ frappe.template.compile = function(str, name) {
// {% for item in list %}
// --> {% for (var i=0, len=list.length; i<len; i++) { var item = list[i]; %}
str = str.replace(/{%\s?for\s([a-z]+)\sin\s([a-z]+)\s?%}/g, "{% for (var i=0, len=$2.length; i<len; i++) { var $1 = $2[i]; %}");
function replacer(match, p1, p2, offset, string) {
var i = frappe.utils.get_random(3);
var len = frappe.utils.get_random(3);
return "{% for (var "+i+"=0, "+len+"="+p2+".length; "+i+"<"+len+"; "+i+"++) { var "
+p1+" = "+p2+"["+i+"]; %}";
}
str = str.replace(/{%\s?for\s([a-z]+)\sin\s([a-z._]+)\s?%}/g, replacer);
// {% endfor %} --> {% } %}
str = str.replace(/{%\s?endif\s?%}/g, "{% }; %}");
@ -46,7 +52,7 @@ frappe.template.compile = function(str, name) {
.split("\r").join("\\'")
+ "');}return _p.join('');";
frappe.template.debug[str] = fn_str;
frappe.template.debug[name] = fn_str;
try {
frappe.template.compiled[key] = new Function("obj", fn_str);
} catch (e) {

View file

@ -181,9 +181,9 @@ body[data-route^="Module"] .main-menu {
height: 0;
padding-bottom: 100%;
border-radius: 6px;
background-size: 100% 100%;
background-size: contain;
background-repeat: no-repeat;
background-position: center center;
background-position: center;
}
.standard-image {

View file

@ -57,15 +57,21 @@ def clear_sessions(user=None, keep_current=False, device=None):
if not device:
device = frappe.session.data.device or "desktop"
for sid in frappe.db.sql_list("""select sid from tabSessions where user=%s and device=%s""", (user, device)):
if keep_current and frappe.session.sid==sid:
continue
else:
delete_session(sid)
simultaneous_sessions = frappe.db.get_value('User', user, 'simultaneous_sessions') or 1
condition = ''
if keep_current:
condition = ' and sid != "{0}"'.format(frappe.session.sid)
limit = simultaneous_sessions - 1
for i, sid in enumerate(frappe.db.sql_list("""select sid from tabSessions
where user=%s and device=%s {condition}
order by lastupdate desc limit {limit}, 100""".format(condition=condition, limit=limit),
(user, device))):
delete_session(sid)
def delete_session(sid=None, user=None):
if not user:
user = hasattr(frappe.local, "session") and frappe.session.user or "Guest"
frappe.cache().hdel("session", sid)
frappe.cache().hdel("last_db_session_update", sid)
frappe.db.sql("""delete from tabSessions where sid=%s""", sid)
@ -298,7 +304,7 @@ class Session:
return (cint(parts[0]) * 3600) + (cint(parts[1]) * 60) + cint(parts[2])
def delete_session(self):
delete_session(self.sid, user=self.user)
delete_session(self.sid)
def start_as_guest(self):
"""all guests share the same 'Guest' session"""

View file

@ -12,15 +12,7 @@ class TestAPI(unittest.TestCase):
frappe.db.sql('delete from `tabToDo` where description like "Test API%"')
frappe.db.commit()
host = get_url()
if not host.startswith('http'):
host = 'http://' + host
if not host.endswith(':8000'):
host = host + ':8000'
server = FrappeClient(host, "Administrator", "admin", verify=False)
server = FrappeClient(get_url(), "Administrator", "admin", verify=False)
server.insert_many([
{"doctype": "ToDo", "description": "Test API 1"},

View file

@ -76,12 +76,13 @@ class TestDataImport(unittest.TestCase):
def test_import_with_children(self):
exporter.get_template("Event", all_doctypes="Yes", with_data="No")
content = read_csv_content(frappe.response.result)
content.append([None] * len(content[-2]))
content[-1][2] = "__Test Event"
content[-1][2] = "__Test Event with children"
content[-1][3] = "Private"
content[-1][4] = "2014-01-01 10:00:00.000000"
content[-1][content[15].index("role")] = "System Manager"
importer.upload(content)
ev = frappe.get_doc("Event", {"subject":"__Test Event"})
ev = frappe.get_doc("Event", {"subject":"__Test Event with children"})
self.assertTrue("System Manager" in [d.role for d in ev.roles])

View file

@ -18,8 +18,10 @@ def read_csv_content_from_uploaded_file(ignore_encoding=False):
return read_csv_content(fcontent, ignore_encoding)
def read_csv_content_from_attached_file(doc):
fileid = frappe.db.get_value("File", {"attached_to_doctype": doc.doctype,
"attached_to_name":doc.name}, "name")
fileid = frappe.get_all("File", fields = ["name"], filters = {"attached_to_doctype": doc.doctype,
"attached_to_name":doc.name}, order_by="creation desc")
if fileid : fileid = fileid[0].name
if not fileid:
msgprint(_("File not attached"))

View file

@ -570,7 +570,7 @@ def get_url(uri=None, full_address=False):
if not host_name:
if hasattr(frappe.local, "request") and frappe.local.request and frappe.local.request.host:
protocol = 'https' == frappe.get_request_header('X-Forwarded-Proto', "") and 'https://' or 'http://'
protocol = 'https://' if 'https' == frappe.get_request_header('X-Forwarded-Proto', "") else 'http://'
host_name = protocol + frappe.local.request.host
elif frappe.local.site:
@ -589,17 +589,22 @@ def get_url(uri=None, full_address=False):
else:
host_name = frappe.db.get_value("Website Settings", "Website Settings",
"subdomain")
if host_name and "http" not in host_name:
host_name = "http://" + host_name
if not host_name:
host_name = "http://localhost"
if host_name and not (host_name.startswith("http://") or host_name.startswith("https://")):
host_name = "http://" + host_name
if not uri and full_address:
uri = frappe.get_request_header("REQUEST_URI", "")
url = urllib.basejoin(host_name, uri) if uri else host_name
# add port if not added
if frappe.conf.webserver_port and not url.rsplit(':', 1)[-1].isdigit():
url = url + ':' + str(frappe.conf.webserver_port)
return url
def get_host_name():