diff --git a/README b/README
new file mode 100644
index 0000000000..06857ed75a
--- /dev/null
+++ b/README
@@ -0,0 +1 @@
+Web Notes Framework: A web application framework with client-side and server-side libraries including metadata definition, forms, virtual pages- Ideal for developing js driven database apps.
diff --git a/cgi-bin/webnotes/db.py b/cgi-bin/webnotes/db.py
index 912d70437c..1f9336c0f0 100644
--- a/cgi-bin/webnotes/db.py
+++ b/cgi-bin/webnotes/db.py
@@ -246,8 +246,9 @@ class Database:
return r and (len(r) > 1 and (i[0] for i in r) or r[0][0]) or None
def set_value(self, dt, dn, field, val):
+ from webnotes.utils import now
if dn and dt!=dn:
- self.sql("update `tab"+dt+"` set `"+field+"`=%s where name=%s", (val, dn))
+ self.sql("update `tab"+dt+"` set `"+field+"`=%s, modified=%s where name=%s", (val, now(), dn))
else:
if self.sql("select value from tabSingles where field=%s and doctype=%s", (field, dt)):
self.sql("update tabSingles set value=%s where field=%s and doctype=%s", (val, field, dt))
diff --git a/cgi-bin/webnotes/install_lib/install.py b/cgi-bin/webnotes/install_lib/install.py
index 5612f36ce2..2a014b4398 100755
--- a/cgi-bin/webnotes/install_lib/install.py
+++ b/cgi-bin/webnotes/install_lib/install.py
@@ -95,6 +95,9 @@ class Installer:
"""
a very simplified version, just for the time being..will eventually be deprecated once the framework stabilizes.
"""
+ #Storing passed source path
+ passed_source_path = source_path
+
# get the path of the sql file to import
if not source_path:
@@ -125,7 +128,9 @@ class Installer:
self.dbman.restore_database(target, source_path, self.root_password)
if verbose: print "Imported from database %s" % source_path
- self.import_core_module()
+ #If source path is passed
+ #i.e. importing from master sql, dont import core modules
+ if not passed_source_path: self.import_core_module()
# framework cleanups
self.framework_cleanups(target)
diff --git a/cgi-bin/webnotes/model/__init__.py b/cgi-bin/webnotes/model/__init__.py
index 8a95daf0e9..2fc0da731c 100644
--- a/cgi-bin/webnotes/model/__init__.py
+++ b/cgi-bin/webnotes/model/__init__.py
@@ -38,11 +38,17 @@ def delete_doc(doctype=None, name=None, doclist = None):
import webnotes.model.meta
sql = webnotes.conn.sql
+ # get from form
if not doctype:
doctype = webnotes.form_dict.get('dt')
name = webnotes.form_dict.get('dn')
- if not doctype:
- webnotes.msgprint('Nothing to delete!', raise_exception =1)
+
+ if not doctype:
+ webnotes.msgprint('Nothing to delete!', raise_exception =1)
+
+ # already deleted..?
+ if not webnotes.conn.exists(doctype, name):
+ return
tablefields = webnotes.model.meta.get_table_fields(doctype)
diff --git a/cgi-bin/webnotes/model/doc.py b/cgi-bin/webnotes/model/doc.py
old mode 100644
new mode 100755
index f6dc31bbca..bb15ebb92f
--- a/cgi-bin/webnotes/model/doc.py
+++ b/cgi-bin/webnotes/model/doc.py
@@ -227,7 +227,7 @@ class Document:
self.name = self.name.strip() # no leading and trailing blanks
- forbidden = ['%', "'", '"', ',', '#', '*', '?', '&', '`']
+ forbidden = ['%', "'", '"', '#', '*', '?', '`']
for f in forbidden:
if f in self.name:
webnotes.msgprint('%s not allowed in ID (name)' % f, raise_exception =1)
diff --git a/cgi-bin/webnotes/utils/backups.py b/cgi-bin/webnotes/utils/backups.py
index 804750fa29..903683c196 100644
--- a/cgi-bin/webnotes/utils/backups.py
+++ b/cgi-bin/webnotes/utils/backups.py
@@ -1,107 +1,190 @@
-import webnotes
+"""
+ This module handles the On Demand Backup utility
+"""
+#Imports
+import os, webnotes
+from datetime import datetime
-backup_folder = '/backups'
-mysql_path = ''
-download_folder = 'backups'
-def mysqldump(db, folder=''):
- global mysql_path
- import os
- import webnotes.defs
-
- os.system('%(path)smysqldump %(db)s > %(folder)s%(db)s.sql -u %(db)s -p%(pwd)s --ignore-table=%(db)s.__DocTypeCache' % {'path':mysql_path, 'db':db, 'pwd':webnotes.defs.db_password, 'folder':folder})
+#Global constants
+from webnotes.defs import backup_path, backup_link_path, backup_url
+verbose = 0
-def backup_db(db, conn, from_all=0):
- import os
- global backup_folder
-
- try:
- # Check processlist
- if from_all or len(conn.sql("show processlist")) == 1:
- p = backup_folder
- if from_all: p = backup_folder + '/dumps'
-
- # clear old file
- os.system('rm %s/%s.tar.gz' % (p,db))
-
- # dump
- mysqldump(db, p+'/')
-
- # zip
- os.system('tar czf %s/%s.tar.gz %s/%s.sql' % (p, db, p, db))
- os.system('rm %s/%s.sql' % (p, db))
- else:
- print("Another process is running in database. Please try again")
- except Exception, e:
- #sql('unlock tables')
- raise e
+#-------------------------------------------------------------------------------
+class BackupGenerator:
+ """
+ This class contains methods to perform On Demand Backup
-def backup_all():
- # backups folder
- import os
- import webnotes.db
- global backup_folder
-
- conn = webnotes.db.Database(use_default=1)
- dblist = conn.sql('select db_name from tabAccount')
+ To initialize, specify (db_name, user, password, db_file_name=None)
+ If specifying db_file_name, also append ".sql.gz"
+ """
+ def __init__(self, db_name, user, password, db_file_name=None):
+ self.db_name = db_name
+ self.user = user
+ self.password = password
+ self.db_file_name = db_file_name and db_file_name \
+ or (backup_path + db_name + ".sql.gz")
- # backup -all in /backups folder
- for d in (('accounts',),) + dblist:
- backup_db(d[0], conn, 1)
+ def take_dump(self):
+ """
+ Dumps a db via mysqldump
+ """
+ os.system("""mysqldump -u %(user)s -p%(password)s %(db_name)s |
+ gzip -c > %(db_file_name)s""" % self.__dict__)
- conn.close()
- # dump all in /daily folder
- import time, datetime
- fname = 'daily-' + time.strftime('%Y-%m-%d') + '.tar.gz'
+ def copy_to_backup_link(self):
+ """
+ Copies the backup file from backup path to shared link path
+ It also gives the file a random name, thus hiding the db_name
+ """
+ import random
+ todays_date = "".join(str(datetime.date(datetime.today())).split("-"))
+ random_number = str(int(random.random()*99999999))
+
+ #Generate a random name using today's date and a 8 digit random number
+ random_name = todays_date + "_" + random_number + ".sql.gz"
+
+ os.system("""cp -f %(src_file)s %(dest_file)s""" % \
+ {"src_file":self.db_file_name,
+ "dest_file":(backup_link_path + random_name)})
+ if verbose: print "file copied"
+ return random_name
- # daily dump
- os.system('tar czf %s/daily/%s %s/dumps' % (backup_folder, fname, backup_folder))
-
- # keep only three files
- if len(os.listdir(backup_folder + '/daily')) > 3:
- delete_oldest_file(backup_folder + '/daily')
-
- # if sunday, then copy to weekly
- if datetime.datetime.now().weekday()==6:
- os.system('cp '+backup_folder+'/daily/'+fname+' '+backup_folder+'/weekly/'+fname)
-
- # keep only three files
- if len(os.listdir(backup_folder + '/weekly')) > 3:
- delete_oldest_file(backup_folder + '/weekly')
-
-def delete_oldest_file(folder):
- import os
- a = sorted(os.listdir(folder), key=lambda fn: os.stat(folder+'/'+fn).st_mtime, reverse=False)
- if a:
- os.system('rm %s/%s' % (folder, a[0]))
+
+ def get_recipients(self):
+ """
+ Get recepient's email address
+ """
+ #import webnotes.db
+ #webnotes.conn = webnotes.db.Database(use_default=1)
+ recipient_list = webnotes.conn.sql(\
+ """SELECT parent FROM tabUserRole
+ WHERE role='System Manager'
+ AND parent!='Administrator'
+ AND parent IN
+ (SELECT email FROM tabProfile WHERE enabled=1)""")
+ return [i[0] for i in recipient_list]
+
+
+ def send_email(self, backup_file):
+ """
+ Sends the link to backup file located at erpnext/backups
+ """
+ file_url = backup_url + backup_file
+ from webnotes.utils.email_lib import sendmail
+
+ recipient_list = self.get_recipients()
+ msg = """Click here to begin downloading\
+ your backup
+
+ This link will be valid for 24 hours.
+
+ Also, a new backup will be available for download (if requested)\
+ only after 24 hours.""" % {"file_url":file_url}
+
+ datetime_str = datetime.fromtimestamp(os.stat(self.db_file_name).st_ctime)
+
+ subject = datetime_str.strftime("%d/%m/%Y %H:%M:%S") + """ - Backup ready to be downloaded"""
+ sendmail(recipients=recipient_list, msg=msg, subject=subject)
+ return recipient_list
+
+
+ def get_backup(self):
+ """
+ Takes a new dump if existing file is old
+ and sends the link to the file as email
+ """
+ #Check if file exists and is less than a day old
+ #If not Take Dump
+ if is_file_old(self.db_file_name):
+ self.take_dump()
+
+ #Copy file to backup_link_path
+ #This is a security hole. When the user calls get_backup repeatedly
+ #a file will be generated each time.
+ backup_file = self.copy_to_backup_link()
+ #Email Link
+ recipient_list = self.send_email(backup_file)
+
+ return recipient_list
+
+#-------------------------------------------------------------------------------
def get_backup():
- import webnotes
- import os, time
+ """
+ This function is executed when the user clicks on
+ Toos > Download Backup
+ """
+ #if verbose: print webnotes.conn.cur_db_name + " " + webnotes.defs.db_password
+ odb = BackupGenerator(webnotes.conn.cur_db_name, webnotes.conn.cur_db_name,\
+ webnotes.defs.db_password)
+ recipient_list = odb.get_backup()
+ delete_temp_backups()
+ webnotes.msgprint("""A download link to your backup will be emailed \
+ to you shortly on the following email address:
+ %s""" % (str(recipient_list),))
- global backup_folder, download_folder
- # get the last nightly backup file from the backups folder
- os.chdir(download_folder)
+def delete_temp_backups():
+ """
+ Cleans up the backup_link_path directory by deleting files older than 24 hours
+ """
+ file_list = os.listdir(backup_link_path)
+ for this_file in file_list:
+ this_file_path = backup_link_path + this_file
+ if is_file_old(this_file_path):
+ os.remove(this_file_path)
- if webnotes.conn.cur_db_name:
- fname = webnotes.conn.cur_db_name + '.tar.gz'
- # rename it
- from random import choice
- lnd='0123456789'
- new_name = ''.join(map(lambda x,y=lnd: choice(y), range(8))) + '.sql.gz'
- folder = backup_folder + '/dumps/'
+def is_file_old(db_file_name, older_than=24):
+ """
+ Checks if file exists and is older than specified hours
+ Returns ->
+ True: file does not exist or file is old
+ False: file is new
+ """
+ if os.path.isfile(db_file_name):
+ from datetime import timedelta
+ import time
+ #Get timestamp of the file
+ file_datetime = datetime.fromtimestamp\
+ (os.stat(db_file_name).st_ctime)
+ if datetime.today() - file_datetime >= timedelta(hours = older_than):
+ if verbose: print "File is old"
+ return True
+ else:
+ if verbose: print "File is recent"
+ return False
+ else:
+ if verbose: print "File does not exist"
+ return True
- # get the newest file
- if os.path.exists(folder):
- os.system('cp '+ folder + webnotes.conn.cur_db_name+'.sql.gz' + ' ./' + new_name)
- webnotes.msgprint('Your nightly backup is available for download by clicking here (only for the next few hours)')
+#-------------------------------------------------------------------------------
+
+if __name__ == "__main__":
+ """
+ is_file_old db_name user password
+ get_backup db_name user password
+ """
+ import sys
+ cmd = sys.argv[1]
+ if cmd == "is_file_old":
+ odb = BackupGenerator(sys.argv[2], sys.argv[3], sys.argv[4])
+ is_file_old(odb.db_file_name)
- # delete any files older than a day
- now = time.time()
- for f in os.listdir('.'):
- if os.stat(f).st_mtime < (now - 86400):
- if os.path.isfile(f):
- os.remove(f)
+ if cmd == "get_backup":
+ odb = BackupGenerator(sys.argv[2], sys.argv[3], sys.argv[4])
+ odb.get_backup()
+
+ if cmd == "take_dump":
+ odb = BackupGenerator(sys.argv[2], sys.argv[3], sys.argv[4])
+ odb.take_dump()
+
+ if cmd == "send_email":
+ odb = BackupGenerator(sys.argv[2], sys.argv[3], sys.argv[4])
+ odb.send_email("abc.sql.gz")
+
+ if cmd == "delete_temp_backups":
+ delete_temp_backups()
+
diff --git a/cgi-bin/webnotes/utils/email_lib/form_email.py b/cgi-bin/webnotes/utils/email_lib/form_email.py
index 5bd3be8007..979f1f8b86 100644
--- a/cgi-bin/webnotes/utils/email_lib/form_email.py
+++ b/cgi-bin/webnotes/utils/email_lib/form_email.py
@@ -148,9 +148,11 @@ class FormEmail:
self.email.add_attachment(self.dn.replace(' ','').replace('/','-') + '.html', self.body)
# attachments
+ # self.with_attachments comes from http form variables
+ # i.e. with_attachments=1
if cint(self.with_attachments):
for a in self.set_attachments():
- a and self.email.attach(a.split(',')[0])
+ a and self.email.attach_file(a.split(',')[0])
# cc
if self.cc: