Merge branch 'develop'

This commit is contained in:
Nabin Hait 2017-04-28 15:38:37 +05:30
commit 53cfd2bba2
28 changed files with 163 additions and 94 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__ = '8.0.26'
__version__ = '8.0.27'
__title__ = "Frappe Framework"
local = Local()

View file

@ -11,7 +11,6 @@ from werkzeug.local import LocalManager
from werkzeug.exceptions import HTTPException, NotFound
from werkzeug.contrib.profiler import ProfilerMiddleware
from werkzeug.wsgi import SharedDataMiddleware
from werkzeug.serving import run_with_reloader
import frappe
import frappe.handler
@ -20,7 +19,7 @@ import frappe.api
import frappe.async
import frappe.utils.response
import frappe.website.render
from frappe.utils import get_site_name, get_site_path
from frappe.utils import get_site_name
from frappe.middlewares import StaticDataMiddleware
from frappe.utils.error import make_error_snapshot
from frappe.core.doctype.communication.comment import update_comments_in_parent_after_request
@ -222,5 +221,9 @@ def serve(port=8000, profile=False, site=None, sites_path='.'):
'SERVER_NAME': 'localhost:8000'
}
run_simple('0.0.0.0', int(port), application, use_reloader=True,
use_debugger=True, use_evalex=True, threaded=True)
in_test_env = os.environ.get('CI')
run_simple('0.0.0.0', int(port), application,
use_reloader=not in_test_env,
use_debugger=not in_test_env,
use_evalex=not in_test_env,
threaded=True)

View file

@ -2,6 +2,7 @@ from __future__ import unicode_literals, absolute_import
import click
import hashlib, os, sys
import frappe
from _mysql_exceptions import ProgrammingError
from frappe.commands import pass_context, get_site
from frappe.commands.scheduler import _is_scheduler_enabled
from frappe.limits import update_limits, get_limits
@ -323,7 +324,8 @@ def uninstall(context, app, dry_run=False, yes=False):
@click.option('--root-login', default='root')
@click.option('--root-password')
@click.option('--archived-sites-path')
def drop_site(site, root_login='root', root_password=None, archived_sites_path=None):
@click.option('--force', help='Force drop-site even if an error is encountered', is_flag=True, default=False)
def drop_site(site, root_login='root', root_password=None, archived_sites_path=None, force=False):
"Remove site from database and filesystem"
from frappe.installer import get_root_connection
from frappe.model.db_schema import DbManager
@ -331,7 +333,22 @@ def drop_site(site, root_login='root', root_password=None, archived_sites_path=N
frappe.init(site=site)
frappe.connect()
scheduled_backup(ignore_files=False, force=True)
try:
scheduled_backup(ignore_files=False, force=True)
except ProgrammingError as err:
if err[0] == 1146:
if force:
pass
else:
click.echo("="*80)
click.echo("Error: The operation has stopped because backup of {s}'s database failed.".format(s=site))
click.echo("Reason: {reason}{sep}".format(reason=err[1], sep="\n"))
click.echo("Fix the issue and try again.")
click.echo(
"Hint: Use 'bench drop-site {s} --force' to force the removal of {s}".format(sep="\n", tab="\t", s=site)
)
sys.exit(1)
db_name = frappe.local.conf.db_name
frappe.local.db = get_root_connection(root_login, root_password)

View file

@ -28,9 +28,10 @@
</tbody>
</table>
<p>
<span class="indicator green" style="margin-right: 20px;">Started</span>
<span class="indicator blue" style="margin-right: 20px;">Started</span>
<span class="indicator orange" style="margin-right: 20px;">Queued</span>
<span class="indicator red">Failed</span>
<span class="indicator red" style="margin-right: 20px;">Failed</span>
<span class="indicator green">Finished</span>
</p>
{% else %}
<p class="text-muted">No pending or current jobs for this site</p>

View file

@ -11,7 +11,8 @@ from frappe.utils import format_datetime, cint
colors = {
'queued': 'orange',
'failed': 'red',
'started': 'green'
'started': 'blue',
'finished': 'green'
}
@frappe.whitelist()

View file

@ -66,10 +66,7 @@ def add(args=None):
notify_assignment(d.assigned_by, d.owner, d.reference_type, d.reference_name, action='ASSIGN',\
description=args.get("description"), notify=args.get('notify'))
if not args.get("bulk_assign"):
return get(args)
else:
return {}
return get(args)
@frappe.whitelist()
def add_multiple(args=None):

View file

@ -40,7 +40,7 @@ class AutoEmailReport(Document):
count = frappe.db.sql('select count(*) from `tabAuto Email Report` where user=%s and enabled=1', self.user)[0][0]
if count > max_reports_per_user + (-1 if self.flags.in_insert else 0):
frappe.throw(_('Only {0} emailed reports are allowed per user').format(max_reports_per_user))
def validate_report_format(self):
""" check if user has select correct report format """
valid_report_formats = ["HTML", "XLS", "CSV"]
@ -59,6 +59,11 @@ class AutoEmailReport(Document):
columns, data = report.get_data(limit=self.no_of_rows or 100, user = self.user,
filters = self.filters, as_dict=True)
# add serial numbers
columns.insert(0, frappe._dict(fieldname='idx', label='', width='30px'))
for i in range(len(data)):
data[i]['idx'] = i+1
if len(data)==0 and self.send_if_data:
return None
@ -77,7 +82,7 @@ class AutoEmailReport(Document):
def get_html_table(self, columns, data):
return frappe.render_template('frappe/templates/includes/print_table.html', {
'columns': columns,
'data': data[1:]
'data': data
})
def get_csv(self, columns, data):
@ -96,7 +101,7 @@ class AutoEmailReport(Document):
def send(self):
if self.filter_meta and not self.filters:
frappe.throw(_("Please set filters value in Report Filter table."))
data = self.get_report_content()
if not data:
return
@ -135,7 +140,7 @@ class AutoEmailReport(Document):
def get_report_footer(self):
return """<hr style="margin: 30px 0px 15px 0px;">
<p style="font-size: 9px;">
View report in your browser:
View report in your browser:
<a href= {{report_url}} target="_blank">{{report_name}}</a><br><br>
Edit Auto Email Report Settings: {{edit_report_settings}}
</p>"""

View file

@ -61,7 +61,7 @@ class EmailAccount(Document):
if (not self.awaiting_password and not frappe.local.flags.in_install
and not frappe.local.flags.in_patch):
if self.password:
if self.password or self.smtp_server in ('127.0.0.1', 'localhost'):
if self.enable_incoming:
self.get_incoming_server()
self.no_failed = 0
@ -331,7 +331,7 @@ class EmailAccount(Document):
raise SentEmailInInbox
if email.message_id:
names = frappe.db.sql("""select distinct name from tabCommunication
names = frappe.db.sql("""select distinct name from tabCommunication
where message_id='{message_id}'
order by creation desc limit 1""".format(
message_id=email.message_id
@ -483,7 +483,7 @@ class EmailAccount(Document):
parent = frappe.new_doc(self.append_to)
if self.subject_field:
parent.set(self.subject_field, frappe.as_unicode(email.subject))
parent.set(self.subject_field, frappe.as_unicode(email.subject)[:140])
if self.sender_field:
parent.set(self.sender_field, frappe.as_unicode(email.from_email))
@ -591,7 +591,7 @@ class EmailAccount(Document):
if not self.use_imap:
return
flags = frappe.db.sql("""select name, communication, uid, action from
flags = frappe.db.sql("""select name, communication, uid, action from
`tabEmail Flag Queue` where is_completed=0 and email_account='{email_account}'
""".format(email_account=self.name), as_dict=True)
@ -614,7 +614,7 @@ class EmailAccount(Document):
self.set_communication_seen_status(docnames, seen=0)
docnames = ",".join([ "'%s'"%flag.get("name") for flag in flags ])
frappe.db.sql(""" update `tabEmail Flag Queue` set is_completed=1
frappe.db.sql(""" update `tabEmail Flag Queue` set is_completed=1
where name in ({docnames})""".format(docnames=docnames))
def set_communication_seen_status(self, docnames, seen=0):
@ -622,7 +622,7 @@ class EmailAccount(Document):
if not docnames:
return
frappe.db.sql(""" update `tabCommunication` set seen={seen}
frappe.db.sql(""" update `tabCommunication` set seen={seen}
where name in ({docnames})""".format(docnames=docnames, seen=seen))
@frappe.whitelist()
@ -716,4 +716,4 @@ def get_max_email_uid(email_account):
return 1
else:
max_uid = int(result[0].get("uid", 0)) + 1
return max_uid
return max_uid

View file

@ -112,6 +112,10 @@ class EmailServer:
self.uid_reindexed = False
uid_list = email_list = self.get_new_mails()
if not email_list:
return
num = num_copy = len(email_list)
# WARNING: Hard coded max no. of messages to be popped
@ -166,11 +170,13 @@ class EmailServer:
def get_new_mails(self):
"""Return list of new mails"""
if cint(self.settings.use_imap):
email_list = []
self.check_imap_uidvalidity()
self.imap.select("Inbox", readonly=True)
response, message = self.imap.uid('search', None, self.settings.email_sync_rule)
email_list = message[0].split()
if message[0]:
email_list = message[0].split()
else:
email_list = self.pop.list()[1]

View file

@ -59,7 +59,10 @@ def get_outgoing_email_account(raise_exception_not_set=True, append_to=None):
if email_account:
if email_account.enable_outgoing and not getattr(email_account, 'from_site_config', False):
email_account.password = email_account.get_password()
raise_exception = True
if email_account.smtp_server in ['localhost','127.0.0.1']:
raise_exception = False
email_account.password = email_account.get_password(raise_exception=raise_exception)
email_account.default_sender = email.utils.formataddr((email_account.name, email_account.get("email_id")))
frappe.local.outgoing_email_account[append_to or "default"] = email_account

View file

@ -10,13 +10,23 @@ import frappe.sessions
import frappe.utils.file_manager
import frappe.desk.form.run_method
from frappe.utils.response import build_response
from werkzeug.wrappers import Response
def handle():
"""handle request"""
cmd = frappe.local.form_dict.cmd
data = None
if cmd!='login':
execute_cmd(cmd)
data = execute_cmd(cmd)
if data:
if isinstance(data, Response):
# method returns a response object, pass it on
return data
# add the response to `message` label
frappe.response['message'] = data
return build_response("json")
@ -39,11 +49,8 @@ def execute_cmd(cmd, from_async=False):
is_whitelisted(method)
ret = frappe.call(method, **frappe.form_dict)
return frappe.call(method, **frappe.form_dict)
# returns with a message
if ret:
frappe.response['message'] = ret
def is_whitelisted(method):
# check if whitelisted

View file

@ -73,7 +73,7 @@ class DatabaseQuery(object):
self.user = user or frappe.session.user
self.update = update
self.user_settings_fields = copy.deepcopy(self.fields)
# self.debug = True
#self.debug = True
if user_settings:
self.user_settings = json.loads(user_settings)
@ -300,8 +300,8 @@ class DatabaseQuery(object):
if f.operator.lower() == 'between' and \
(f.fieldname in ('creation', 'modified') or (df and (df.fieldtype=="Date" or df.fieldtype=="Datetime"))):
value = "'%s' AND '%s'" % (
get_datetime(f.value[0]).strftime("%Y-%m-%d %H:%M:%S.%f"),
add_to_date(get_datetime(f.value[1]),days=1).strftime("%Y-%m-%d %H:%M:%S.%f"))
add_to_date(get_datetime(f.value[0]),days=-1).strftime("%Y-%m-%d %H:%M:%S.%f"),
get_datetime(f.value[1]).strftime("%Y-%m-%d %H:%M:%S.%f"))
fallback = "'0000-00-00 00:00:00'"
elif df and df.fieldtype=="Date":

View file

@ -3,9 +3,24 @@
import frappe
# select doctypes that are accessed by the user (not read_only) first, so that the
# the validation message shows the user-facing doctype first.
# For example Journal Entry should be validated before GL Entry (which is an internal doctype)
dynamic_link_queries = [
"""select parent, fieldname, options from tabDocField where fieldtype='Dynamic Link'""",
"""select dt as parent, fieldname, options from `tabCustom Field` where fieldtype='Dynamic Link'""",
"""select parent,
(select read_only from `tabDocType` where name=tabDocField.parent) as read_only,
fieldname, options
from tabDocField
where fieldtype='Dynamic Link'
order by read_only""",
"""select dt as parent,
(select read_only from `tabDocType` where name=`tabCustom Field`.dt) as read_only,
fieldname, options
from `tabCustom Field`
where fieldtype='Dynamic Link'
order by read_only""",
]
def get_dynamic_link_map(for_delete=False):
@ -29,7 +44,6 @@ def get_dynamic_link_map(for_delete=False):
dynamic_link_map.setdefault(doctype, []).append(df)
frappe.local.dynamic_link_map = dynamic_link_map
return frappe.local.dynamic_link_map
def get_dynamic_links():

View file

@ -1,6 +1,11 @@
import frappe
def execute():
from frappe.website.router import get_doctypes_with_web_view
from frappe.utils.global_search import rebuild_for_doctype
for doctype in get_doctypes_with_web_view():
rebuild_for_doctype(doctype)
try:
rebuild_for_doctype(doctype)
except frappe.DoesNotExistError:
pass

View file

@ -497,6 +497,10 @@ h6.uppercase,
}
.frappe-control pre {
white-space: pre-wrap;
background-color: inherit;
border: none;
padding: 0px;
margin: 0px;
}
.hide-control {
display: none !important;

View file

@ -328,6 +328,7 @@ frappe.Application = Class.extend({
if(!frappe.app.session_expired_dialog) {
var dialog = new frappe.ui.Dialog({
title: __('Session Expired'),
keep_open: true,
fields: [
{ fieldtype:'Password', fieldname:'password',
label: __('Please Enter Your Password to Continue') },
@ -369,7 +370,7 @@ frappe.Application = Class.extend({
// add backdrop
$('.modal-backdrop').css({
'opacity': 1,
'background-color': '#EBEFF2'
'background-color': '#4B4C9D'
});
}
},

View file

@ -1343,10 +1343,7 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({
};
},
filter: function(item, input) {
var d = this.get_item(item.value);
return Awesomplete.FILTER_CONTAINS(d.value, '__link_option') ||
Awesomplete.FILTER_CONTAINS(d.value, input) ||
Awesomplete.FILTER_CONTAINS(d.description, input);
return true;
},
item: function (item, input) {
d = this.get_item(item.value);

View file

@ -333,7 +333,7 @@ frappe.ui.form.Dashboard = Class.extend({
if(!this.heatmap) {
this.heatmap = new CalHeatMap();
this.heatmap.init({
itemSelector: "#heatmap-" + this.frm.doctype,
itemSelector: "#heatmap-" + frappe.model.scrub(this.frm.doctype),
domain: "month",
subDomain: "day",
start: moment().subtract(1, 'year').add(1, 'month').toDate(),

View file

@ -80,7 +80,7 @@ frappe.ui.form.AssignTo = Class.extend({
add: function() {
var me = this;
if(this.frm.doc.__unsaved == 1) {
if(this.frm.is_new()) {
frappe.throw(__("Please save the document before assignment"));
return;
}
@ -93,7 +93,6 @@ frappe.ui.form.AssignTo = Class.extend({
docname: me.frm.docname,
callback: function(r) {
me.render(r.message);
me.frm.reload_doc();
}
});
}
@ -108,7 +107,7 @@ frappe.ui.form.AssignTo = Class.extend({
remove: function(owner) {
var me = this;
if(this.frm.doc.__unsaved == 1) {
if(this.frm.is_new()) {
frappe.throw(__("Please save the document before removing assignment"));
return;
}
@ -122,7 +121,6 @@ frappe.ui.form.AssignTo = Class.extend({
},
callback:function(r,rt) {
me.render(r.message);
me.frm.reload_doc();
}
});
}

View file

@ -4,7 +4,7 @@
<div class="progress-area hidden form-dashboard-section">
</div>
<div class="form-heatmap hidden form-dashboard-section">
<div id="heatmap-{{ frm.doctype }}"></div>
<div id="heatmap-{{ frappe.model.scrub(frm.doctype) }}"></div>
<div class="text-muted small heatmap-message hidden"></div>
</div>
<div class="form-chart form-dashboard-section hidden"></div>

View file

@ -153,8 +153,9 @@ $(window).on('hashchange', function() {
return;
// hide open dialog
if(cur_dialog && cur_dialog.hide_on_page_refresh)
if(cur_dialog && cur_dialog.hide_on_page_refresh) {
cur_dialog.hide();
}
frappe.route();

View file

@ -60,6 +60,12 @@ frappe.socket = {
if (frappe.flags.doc_subscribe) {
return;
}
frappe.flags.doc_subscribe = true;
// throttle to 1 per sec
setTimeout(function() { frappe.flags.doc_subscribe = false }, 1000);
if (frm.is_new()) {
return;
}
@ -72,11 +78,6 @@ frappe.socket = {
}
}
frappe.flags.doc_subscribe = true;
// throttle to 1 per sec
setTimeout(function() { frappe.flags.doc_subscribe = false }, 1000);
frappe.socket.doc_subscribe(frm.doctype, frm.docname);
});

View file

@ -1,6 +1,6 @@
<div class="filter-dash-item">
<div class="filter-header">
<h6 class="h6 filter-label" data-name="{{ label }}">{{ label }}</h6>
<h6 class="h6 filter-label" data-name="{{ field }}">{{ label }}</h6>
{% if (type!=="Date" && type!=="Datetime" && type!=="DateRange") { %}
<div class="dropdown search-dropdown hide pull-right">
<i class="dropdown-toggle octicon octicon-search text-muted"
@ -14,7 +14,7 @@
<div class="dropdown sort-dropdown pull-right">
<i class="pull-right dropdown-toggle
filter-sort-active octicon octicon-gear text-muted"
data-name="{{ label }}" data-sort-by="number" data-order="desc"
data-name="{{ field }}" data-sort-by="number" data-order="desc"
data-toggle="dropdown"/>
<ul class="dropdown-menu">
<li class="filter-sort-item" data-sort-by="alphabet" data-order="asc">
@ -34,7 +34,7 @@
<div class="filter-input filter-input-date" data-name="{{ field }}">
</div>
{% } else { %}
<ul class="list-unstyled sidebar-menu filter-stat" data-name="{{ label }}">
<ul class="list-unstyled sidebar-menu filter-stat" data-name="{{ field }}">
</ul>
{% } %}

View file

@ -117,7 +117,7 @@ frappe.ui.FilterList = Class.extend({
return
}
var active = this.wrapper.find(".filter-sort-active[data-name='"+__(field.label)+"']");
var active = this.wrapper.find(".filter-sort-active[data-name='"+__(field.name)+"']");
// sort filters
if(active.attr('data-sort-by')==='alphabet') {
@ -168,7 +168,7 @@ frappe.ui.FilterList = Class.extend({
label: __(field.label),
labels:labels
};
var dashboard_filter = this.wrapper.find(".filter-stat[data-name='" + __(field.label) + "']")
var dashboard_filter = this.wrapper.find(".filter-stat[data-name='" + __(field.name) + "']")
dashboard_filter.html(frappe.render_template("filter_dashboard_value", context))
.on("click", ".filter-stat-link", function() {
var fieldname = $(this).attr('data-field');

View file

@ -1,6 +1,6 @@
<div class="search-header">
<i class="octicon octicon-search"></i>
<input type="text" class="form-control search-input" style="padding: 15px">
<input type="text" class="form-control search-input" style="padding-left: 15px">
<p class="loading-state hide" style="margin: 0px 20px; color:#d4d9dd">{%= __("Searching")%}&nbsp...</p>
<a type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</a>
</div>

View file

@ -625,6 +625,10 @@ h6.uppercase, .h6.uppercase {
pre {
white-space: pre-wrap;
background-color: inherit;
border: none;
padding: 0px;
margin: 0px;
}
}

View file

@ -58,7 +58,7 @@ def rebuild_for_doctype(doctype):
filters.enabled = 1
if meta.has_field("disabled"):
filters.disabled = 0
return filters
meta = frappe.get_meta(doctype)
@ -69,36 +69,36 @@ def rebuild_for_doctype(doctype):
})
for p in parent_doctypes:
rebuild_for_doctype(p.parent)
return
# Delete records
delete_global_search_records_for_doctype(doctype)
parent_search_fields = meta.get_global_search_fields()
fieldnames = get_selected_fields(meta, parent_search_fields)
# Get all records from parent doctype table
all_records = frappe.get_all(doctype, fields=fieldnames, filters=_get_filters())
# Children data
all_children, child_search_fields = get_children_data(doctype, meta)
all_children, child_search_fields = get_children_data(doctype, meta)
all_contents = []
for doc in all_records:
content = []
for field in parent_search_fields:
value = doc.get(field.fieldname)
if value:
content.append(get_formatted_value(value, field))
# get children data
for child_doctype, records in all_children.get(doc.name, {}).items():
for field in child_search_fields.get(child_doctype):
for r in records:
if r.get(field.fieldname):
content.append(get_formatted_value(r.get(field.fieldname), field))
if content:
# if doctype published in website, push title, route etc.
published = 0
@ -108,9 +108,9 @@ def rebuild_for_doctype(doctype):
published = 1 if d.is_website_published() else 0
title = d.get_title()
route = d.get("route")
all_contents.append({
"doctype": doctype,
"doctype": frappe.db.escape(doctype),
"name": frappe.db.escape(doc.name),
"content": frappe.db.escape(' ||| '.join(content or '')),
"published": published,
@ -119,30 +119,30 @@ def rebuild_for_doctype(doctype):
})
if all_contents:
insert_values_for_multiple_docs(all_contents)
def delete_global_search_records_for_doctype(doctype):
frappe.db.sql('''
delete
from __global_search
where
doctype = %s''', doctype, as_dict=True)
def get_selected_fields(meta, global_search_fields):
fieldnames = [df.fieldname for df in global_search_fields]
if meta.istable==1:
fieldnames.append("parent")
elif "name" not in fieldnames:
fieldnames.append("name")
if meta.has_field("is_website_published"):
fieldnames.append("is_website_published")
return fieldnames
def get_children_data(doctype, meta):
"""
Get all records from all the child tables of a doctype
all_children = {
"parent1": {
"child_doctype1": [
@ -153,41 +153,41 @@ def get_children_data(doctype, meta):
]
}
}
"""
all_children = frappe._dict()
child_search_fields = frappe._dict()
for child in meta.get_table_fields():
child_meta = frappe.get_meta(child.options)
search_fields = child_meta.get_global_search_fields()
if search_fields:
child_search_fields.setdefault(child.options, search_fields)
child_fieldnames = get_selected_fields(child_meta, search_fields)
child_records = frappe.get_all(child.options, fields=child_fieldnames, filters={
child_records = frappe.get_all(child.options, fields=child_fieldnames, filters={
"docstatus": ["!=", 1],
"parenttype": doctype
})
for record in child_records:
all_children.setdefault(record.parent, frappe._dict())\
.setdefault(child.options, []).append(record)
return all_children, child_search_fields
def insert_values_for_multiple_docs(all_contents):
values = []
for content in all_contents:
values.append("( '{doctype}', '{name}', '{content}', '{published}', '{title}', '{route}')"
.format(**content))
frappe.db.sql('''
insert into __global_search
(doctype, name, content, published, title, route)
values
{0}
'''.format(", ".join(values)))
def update_global_search(doc):
'''Add values marked with `in_global_search` to
@ -205,7 +205,7 @@ def update_global_search(doc):
for field in doc.meta.get_global_search_fields():
if doc.get(field.fieldname) and field.fieldtype != "Table":
content.append(get_formatted_value(doc.get(field.fieldname), field))
# Get children
for child in doc.meta.get_table_fields():
for d in doc.get(child.fieldname):

View file

@ -123,15 +123,19 @@ def get_list_context(context, doctype):
from frappe.website.doctype.web_form.web_form import get_web_form_list
list_context = context or frappe._dict()
module = load_doctype_module(doctype)
if hasattr(module, "get_list_context"):
out = frappe._dict(module.get_list_context(list_context) or {})
if out:
list_context = out
meta = frappe.get_meta(doctype)
if not meta.custom:
# custom doctypes don't have modules
module = load_doctype_module(doctype)
if hasattr(module, "get_list_context"):
out = frappe._dict(module.get_list_context(list_context) or {})
if out:
list_context = out
# get path from '/templates/' folder of the doctype
if not list_context.row_template:
list_context.row_template = frappe.get_meta(doctype).get_row_template()
list_context.row_template = meta.get_row_template()
# is web form, show the default web form filters
# which is only the owner