[IMP] use const SUPERUSER_ID insteand of int 1
[odoo/odoo.git] / addons / document / document.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 ##############################################################################
21
22 import base64
23 from osv import osv, fields
24 import os
25
26 # from psycopg2 import Binary
27 #from tools import config
28 import tools
29 from tools.translate import _
30 import nodes
31 import logging
32
33 _logger = logging.getLogger(__name__)
34
35 DMS_ROOT_PATH = tools.config.get('document_path', os.path.join(tools.config['root_path'], 'filestore'))
36
37 class document_file(osv.osv):
38     _inherit = 'ir.attachment'
39     _rec_name = 'datas_fname'
40    
41    
42     def _attach_parent_id(self, cr, uid, ids=None, context=None):
43         """Migrate ir.attachments to the document module.
44
45         When the 'document' module is loaded on a db that has had plain attachments,
46         they will need to be attached to some parent folder, and be converted from
47         base64-in-bytea to raw-in-bytea format.
48         This function performs the internal migration, once and forever, for these
49         attachments. It cannot be done through the nominal ORM maintenance code,
50         because the root folder is only created after the document_data.xml file
51         is loaded.
52         It also establishes the parent_id NOT NULL constraint that ir.attachment
53         should have had (but would have failed if plain attachments contained null
54         values).
55         It also updates the  File Size for the previously created attachments.
56         """
57
58         parent_id = self.pool.get('document.directory')._get_root_directory(cr,uid)
59         if not parent_id:
60             _logger.warning("at _attach_parent_id(), still not able to set the parent!")
61             return False
62
63         if ids is not None:
64             raise NotImplementedError("Ids is just there by convention! Don't use it yet, please.")
65
66         cr.execute("UPDATE ir_attachment " \
67                     "SET parent_id = %s, db_datas = decode(encode(db_datas,'escape'), 'base64') " \
68                     "WHERE parent_id IS NULL", (parent_id,))
69
70         cr.execute("UPDATE ir_attachment SET file_size=length(db_datas) WHERE file_size = 0 and type = 'binary'")
71
72         cr.execute("ALTER TABLE ir_attachment ALTER parent_id SET NOT NULL")
73
74         return True
75
76     def _get_filestore(self, cr):
77         return os.path.join(DMS_ROOT_PATH, cr.dbname)
78
79     def _data_get(self, cr, uid, ids, name, arg, context=None):
80         if context is None:
81             context = {}
82         fbrl = self.browse(cr, uid, ids, context=context)
83         nctx = nodes.get_node_context(cr, uid, context={})
84         # nctx will /not/ inherit the caller's context. Most of
85         # it would be useless, anyway (like active_id, active_model,
86         # bin_size etc.)
87         result = {}
88         bin_size = context.get('bin_size', False)
89         for fbro in fbrl:
90             fnode = nodes.node_file(None, None, nctx, fbro)
91             if not bin_size:
92                     data = fnode.get_data(cr, fbro)
93                     result[fbro.id] = base64.encodestring(data or '')
94             else:
95                     result[fbro.id] = fnode.get_data_len(cr, fbro)
96
97         return result
98
99     #
100     # This code can be improved
101     #
102     def _data_set(self, cr, uid, id, name, value, arg, context=None):
103         if not value:
104             return True
105         fbro = self.browse(cr, uid, id, context=context)
106         nctx = nodes.get_node_context(cr, uid, context={})
107         fnode = nodes.node_file(None, None, nctx, fbro)
108         res = fnode.set_data(cr, base64.decodestring(value), fbro)
109         return res
110
111     _columns = {
112         # Columns from ir.attachment:
113         'create_date': fields.datetime('Date Created', readonly=True),
114         'create_uid':  fields.many2one('res.users', 'Creator', readonly=True),
115         'write_date': fields.datetime('Date Modified', readonly=True),
116         'write_uid':  fields.many2one('res.users', 'Last Modification User', readonly=True),
117         'res_model': fields.char('Attached Model', size=64, readonly=True, change_default=True),
118         'res_id': fields.integer('Attached ID', readonly=True),
119
120         # If ir.attachment contained any data before document is installed, preserve
121         # the data, don't drop the column!
122         'db_datas': fields.binary('Data', oldname='datas'),
123         'datas': fields.function(_data_get, fnct_inv=_data_set, string='File Content', type="binary", nodrop=True),
124
125         # Fields of document:
126         'user_id': fields.many2one('res.users', 'Owner', select=1),
127         # 'group_ids': fields.many2many('res.groups', 'document_group_rel', 'item_id', 'group_id', 'Groups'),
128         # the directory id now is mandatory. It can still be computed automatically.
129         'parent_id': fields.many2one('document.directory', 'Directory', select=1, required=True, change_default=True),
130         'index_content': fields.text('Indexed Content'),
131         'partner_id':fields.many2one('res.partner', 'Partner', select=1),
132         'file_size': fields.integer('File Size', required=True),
133         'file_type': fields.char('Content Type', size=128),
134
135         # fields used for file storage
136         'store_fname': fields.char('Stored Filename', size=200),
137     }
138     _order = "id desc"
139
140     def __get_def_directory(self, cr, uid, context=None):
141         dirobj = self.pool.get('document.directory')
142         return dirobj._get_root_directory(cr, uid, context)
143
144     _defaults = {
145         'user_id': lambda self, cr, uid, ctx:uid,
146         'parent_id': __get_def_directory,
147         'file_size': lambda self, cr, uid, ctx:0,
148     }
149     _sql_constraints = [
150         # filename_uniq is not possible in pure SQL
151     ]
152     def _check_duplication(self, cr, uid, vals, ids=[], op='create'):
153         name = vals.get('name', False)
154         parent_id = vals.get('parent_id', False)
155         res_model = vals.get('res_model', False)
156         res_id = vals.get('res_id', 0)
157         if op == 'write':
158             for file in self.browse(cr, uid, ids): # FIXME fields_only
159                 if not name:
160                     name = file.name
161                 if not parent_id:
162                     parent_id = file.parent_id and file.parent_id.id or False
163                 if not res_model:
164                     res_model = file.res_model and file.res_model or False
165                 if not res_id:
166                     res_id = file.res_id and file.res_id or 0
167                 res = self.search(cr, uid, [('id', '<>', file.id), ('name', '=', name), ('parent_id', '=', parent_id), ('res_model', '=', res_model), ('res_id', '=', res_id)])
168                 if len(res):
169                     return False
170         if op == 'create':
171             res = self.search(cr, uid, [('name', '=', name), ('parent_id', '=', parent_id), ('res_id', '=', res_id), ('res_model', '=', res_model)])
172             if len(res):
173                 return False
174         return True
175
176     def check(self, cr, uid, ids, mode, context=None, values=None):
177         """Check access wrt. res_model, relax the rule of ir.attachment parent
178
179         With 'document' installed, everybody will have access to attachments of
180         any resources they can *read*.
181         """
182         return super(document_file, self).check(cr, uid, ids, mode='read',
183                                             context=context, values=values)
184
185     def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
186         # Grab ids, bypassing 'count'
187         ids = super(document_file, self).search(cr, uid, args, offset=offset,
188                                                 limit=limit, order=order,
189                                                 context=context, count=False)
190         if not ids:
191             return 0 if count else []
192
193         # Filter out documents that are in directories that the user is not allowed to read.
194         # Must use pure SQL to avoid access rules exceptions (we want to remove the records,
195         # not fail), and the records have been filtered in parent's search() anyway.
196         cr.execute('SELECT id, parent_id from "%s" WHERE id in %%s' % self._table, (tuple(ids),))
197         doc_pairs = cr.fetchall()
198         parent_ids = set(zip(*doc_pairs)[1])
199         visible_parent_ids = self.pool.get('document.directory').search(cr, uid, [('id', 'in', list(parent_ids))])
200         disallowed_parents = parent_ids.difference(visible_parent_ids)
201         for doc_id, parent_id in doc_pairs:
202             if parent_id in disallowed_parents:
203                 ids.remove(doc_id)
204         return len(ids) if count else ids
205
206
207     def copy(self, cr, uid, id, default=None, context=None):
208         if not default:
209             default = {}
210         if 'name' not in default:
211             name = self.read(cr, uid, [id], ['name'])[0]['name']
212             default.update({'name': name + " " + _("(copy)")})
213         return super(document_file, self).copy(cr, uid, id, default, context=context)
214
215     def write(self, cr, uid, ids, vals, context=None):
216         result = False
217         if not isinstance(ids, list):
218             ids = [ids]
219         res = self.search(cr, uid, [('id', 'in', ids)])
220         if not len(res):
221             return False
222         if not self._check_duplication(cr, uid, vals, ids, 'write'):
223             raise osv.except_osv(_('ValidateError'), _('File name must be unique!'))
224
225         # if nodes call this write(), they must skip the code below
226         from_node = context and context.get('__from_node', False)
227         if (('parent_id' in vals) or ('name' in vals)) and not from_node:
228             # perhaps this file is renaming or changing directory
229             nctx = nodes.get_node_context(cr,uid,context={})
230             dirobj = self.pool.get('document.directory')
231             if 'parent_id' in vals:
232                 dbro = dirobj.browse(cr, uid, vals['parent_id'], context=context)
233                 dnode = nctx.get_dir_node(cr, dbro)
234             else:
235                 dbro = None
236                 dnode = None
237             ids2 = []
238             for fbro in self.browse(cr, uid, ids, context=context):
239                 if ('parent_id' not in vals or fbro.parent_id.id == vals['parent_id']) \
240                     and ('name' not in vals or fbro.name == vals['name']):
241                         ids2.append(fbro.id)
242                         continue
243                 fnode = nctx.get_file_node(cr, fbro)
244                 res = fnode.move_to(cr, dnode or fnode.parent, vals.get('name', fbro.name), fbro, dbro, True)
245                 if isinstance(res, dict):
246                     vals2 = vals.copy()
247                     vals2.update(res)
248                     wid = res.get('id', fbro.id)
249                     result = super(document_file,self).write(cr,uid,wid,vals2,context=context)
250                     # TODO: how to handle/merge several results?
251                 elif res == True:
252                     ids2.append(fbro.id)
253                 elif res == False:
254                     pass
255             ids = ids2
256         if 'file_size' in vals: # only write that field using direct SQL calls
257             del vals['file_size']
258         if ids and vals:
259             result = super(document_file,self).write(cr, uid, ids, vals, context=context)
260         return result
261
262     def create(self, cr, uid, vals, context=None):
263         if context is None:
264             context = {}
265         vals['parent_id'] = context.get('parent_id', False) or vals.get('parent_id', False)
266         if not vals['parent_id']:
267             vals['parent_id'] = self.pool.get('document.directory')._get_root_directory(cr,uid, context)
268         if not vals.get('res_id', False) and context.get('default_res_id', False):
269             vals['res_id'] = context.get('default_res_id', False)
270         if not vals.get('res_model', False) and context.get('default_res_model', False):
271             vals['res_model'] = context.get('default_res_model', False)
272         if vals.get('res_id', False) and vals.get('res_model', False) \
273                 and not vals.get('partner_id', False):
274             vals['partner_id'] = self.__get_partner_id(cr, uid, \
275                 vals['res_model'], vals['res_id'], context)
276
277         datas = None
278         if vals.get('link', False) :
279             import urllib
280             datas = base64.encodestring(urllib.urlopen(vals['link']).read())
281         else:
282             datas = vals.get('datas', False)
283
284         if datas:
285             vals['file_size'] = len(datas)
286         else:
287             if vals.get('file_size'):
288                 del vals['file_size']
289         result = self._check_duplication(cr, uid, vals)
290         if not result:
291             domain = [
292                 ('res_id', '=', vals['res_id']),
293                 ('res_model', '=', vals['res_model']),
294                 ('datas_fname', '=', vals['datas_fname']),
295             ]
296             attach_ids = self.search(cr, uid, domain, context=context)
297             super(document_file, self).write(cr, uid, attach_ids, 
298                                              {'datas' : vals['datas']},
299                                              context=context)
300             result = attach_ids[0]
301         else:
302             #raise osv.except_osv(_('ValidateError'), _('File name must be unique!'))
303             result = super(document_file, self).create(cr, uid, vals, context)
304         return result
305
306     def __get_partner_id(self, cr, uid, res_model, res_id, context=None):
307         """ A helper to retrieve the associated partner from any res_model+id
308             It is a hack that will try to discover if the mentioned record is
309             clearly associated with a partner record.
310         """
311         obj_model = self.pool.get(res_model)
312         if obj_model._name == 'res.partner':
313             return res_id
314         elif 'partner_id' in obj_model._columns and obj_model._columns['partner_id']._obj == 'res.partner':
315             bro = obj_model.browse(cr, uid, res_id, context=context)
316             return bro.partner_id.id
317         return False
318
319     def unlink(self, cr, uid, ids, context=None):
320         stor = self.pool.get('document.storage')
321         unres = []
322         # We have to do the unlink in 2 stages: prepare a list of actual
323         # files to be unlinked, update the db (safer to do first, can be
324         # rolled back) and then unlink the files. The list wouldn't exist
325         # after we discard the objects
326         ids = self.search(cr, uid, [('id','in',ids)])
327         for f in self.browse(cr, uid, ids, context=context):
328             # TODO: update the node cache
329             par = f.parent_id
330             storage_id = None
331             while par:
332                 if par.storage_id:
333                     storage_id = par.storage_id
334                     break
335                 par = par.parent_id
336             #assert storage_id, "Strange, found file #%s w/o storage!" % f.id #TOCHECK: after run yml, it's fail
337             if storage_id:
338                 r = stor.prepare_unlink(cr, uid, storage_id, f)
339                 if r:
340                     unres.append(r)
341             else:
342                 self.loggerdoc.warning("Unlinking attachment #%s %s that has no storage",
343                                                 f.id, f.name)
344         res = super(document_file, self).unlink(cr, uid, ids, context)
345         stor.do_unlink(cr, uid, unres)
346         return res
347
348 document_file()
349
350
351 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: