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