document: fix regressions at storage and node_descriptor
[odoo/odoo.git] / addons / document / document_directory.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
23 from osv import osv, fields
24 from osv.orm import except_orm
25
26 import os
27 import nodes
28 from tools.translate import _
29
30 class document_directory(osv.osv):
31     _name = 'document.directory'
32     _description = 'Directory'
33     _order = 'name'
34     _columns = {
35         'name': fields.char('Name', size=64, required=True, select=1),
36         'write_date': fields.datetime('Date Modified', readonly=True),
37         'write_uid':  fields.many2one('res.users', 'Last Modification User', readonly=True),
38         'create_date': fields.datetime('Date Created', readonly=True),
39         'create_uid':  fields.many2one('res.users', 'Creator', readonly=True),
40         'domain': fields.char('Domain', size=128, help="Use a domain if you want to apply an automatic filter on visible resources."),
41         'user_id': fields.many2one('res.users', 'Owner'),
42         'storage_id': fields.many2one('document.storage', 'Storage', change_default=True),
43         'group_ids': fields.many2many('res.groups', 'document_directory_group_rel', 'item_id', 'group_id', 'Groups'),
44         'parent_id': fields.many2one('document.directory', 'Parent Directory', select=1, change_default=True),
45         'child_ids': fields.one2many('document.directory', 'parent_id', 'Children'),
46         'file_ids': fields.one2many('ir.attachment', 'parent_id', 'Files'),
47         'content_ids': fields.one2many('document.directory.content', 'directory_id', 'Virtual Files'),
48         'type': fields.selection([
49             ('directory','Static Directory'),
50             ('ressource','Folders per resource'),
51             ],
52             'Type', required=True, select=1, change_default=True,
53             help="Each directory can either have the type Static or be linked to another resource. A static directory, as with Operating Systems, is the classic directory that can contain a set of files. The directories linked to systems resources automatically possess sub-directories for each of resource types defined in the parent directory."),
54         'ressource_type_id': fields.many2one('ir.model', 'Resource model', change_default=True,
55             help="Select an object here and there will be one folder per record of that resource."),
56         'resource_field': fields.many2one('ir.model.fields', 'Name field', help='Field to be used as name on resource directories. If empty, the "name" will be used.'),
57         'resource_find_all': fields.boolean('Find all resources', required=True,
58                 help="If true, all attachments that match this resource will " \
59                     " be located. If false, only ones that have this as parent." ),
60         'ressource_parent_type_id': fields.many2one('ir.model', 'Parent Model', change_default=True,
61             help="If you put an object here, this directory template will appear bellow all of these objects. " \
62                  "Such directories are \"attached\" to the specific model or record, just like attachments. " \
63                  "Don't put a parent directory if you select a parent model."),
64         'ressource_id': fields.integer('Resource ID',
65             help="Along with Parent Model, this ID attaches this folder to a specific record of Parent Model."),
66         'ressource_tree': fields.boolean('Tree Structure',
67             help="Check this if you want to use the same tree structure as the object selected in the system."),
68         'dctx_ids': fields.one2many('document.directory.dctx', 'dir_id', 'Context fields'),
69         'company_id': fields.many2one('res.company', 'Company', change_default=True),
70     }
71
72
73     def _get_root_directory(self, cr,uid, context=None):
74         objid=self.pool.get('ir.model.data')
75         try:
76             mid = objid._get_id(cr, uid, 'document', 'dir_root')
77             if not mid:
78                 return False
79             root_id = objid.read(cr, uid, mid, ['res_id'])['res_id']
80             return root_id
81         except Exception, e:
82             import netsvc
83             logger = netsvc.Logger()
84             logger.notifyChannel("document", netsvc.LOG_WARNING, 'Cannot set directory root:'+ str(e))
85             return False
86         return objid.browse(cr, uid, mid, context=context).res_id
87
88     def _get_def_storage(self, cr, uid, context=None):
89         if context and context.has_key('default_parent_id'):
90                 # Use the same storage as the parent..
91                 diro = self.browse(cr, uid, context['default_parent_id'])
92                 if diro.storage_id:
93                         return diro.storage_id.id
94         objid=self.pool.get('ir.model.data')
95         try:
96                 mid =  objid._get_id(cr, uid, 'document', 'storage_default')
97                 return objid.browse(cr, uid, mid, context=context).res_id
98         except Exception:
99                 return None
100
101     _defaults = {
102         'company_id': lambda s,cr,uid,c: s.pool.get('res.company')._company_default_get(cr, uid, 'document.directory', context=c),
103         'user_id': lambda self,cr,uid,ctx: uid,
104         'domain': '[]',
105         'type': 'directory',
106         'ressource_id': 0,
107         'storage_id': _get_def_storage, # Still, it is bad practice to set it everywhere.
108         'resource_find_all': True,
109     }
110     _sql_constraints = [
111         ('dirname_uniq', 'unique (name,parent_id,ressource_id,ressource_parent_type_id)', 'The directory name must be unique !'),
112         ('no_selfparent', 'check(parent_id <> id)', 'Directory cannot be parent of itself!'),
113         ('dir_parented', 'check(parent_id IS NOT NULL OR storage_id IS NOT NULL)', 'Directory must have a parent or a storage')
114     ]
115     def name_get(self, cr, uid, ids, context=None):
116         res = []
117         if not self.search(cr,uid,[('id','in',ids)]):
118             ids = []
119         for d in self.browse(cr, uid, ids, context=context):
120             s = ''
121             d2 = d
122             while d2 and d2.parent_id:
123                 s = d2.name + (s and ('/' + s) or '')
124                 d2 = d2.parent_id
125             res.append((d.id, s or d.name))
126         return res
127
128     def get_full_path(self, cr, uid, dir_id, context=None):
129         """ Return the full path to this directory, in a list, root first
130         """
131         if isinstance(dir_id, (tuple, list)):
132             assert len(dir_id) == 1
133             dir_id = dir_id[0]
134
135         def _parent(dir_id, path):
136             parent=self.browse(cr, uid, dir_id)
137             if parent.parent_id and not parent.ressource_parent_type_id:
138                 _parent(parent.parent_id.id,path)
139                 path.append(parent.name)
140             else:
141                 path.append(parent.name)
142                 return path
143         path = []
144         _parent(dir_id, path)
145         return path
146
147     def _check_recursion(self, cr, uid, ids):
148         level = 100
149         while len(ids):
150             cr.execute('select distinct parent_id from document_directory where id in ('+','.join(map(str,ids))+')')
151             ids = filter(None, map(lambda x:x[0], cr.fetchall()))
152             if not level:
153                 return False
154             level -= 1
155         return True
156
157     _constraints = [
158         (_check_recursion, 'Error! You can not create recursive Directories.', ['parent_id'])
159     ]
160
161     def __init__(self, *args, **kwargs):
162         super(document_directory, self).__init__(*args, **kwargs)
163
164     def onchange_content_id(self, cr, uid, ids, ressource_type_id):
165         return {}
166
167     """
168         PRE:
169             uri: of the form "Sales Order/SO001"
170         PORT:
171             uri
172             object: the object.directory or object.directory.content
173             object2: the other object linked (if object.directory.content)
174     """
175     def get_object(self, cr, uid, uri, context=None):
176         """ Return a node object for the given uri.
177            This fn merely passes the call to node_context
178         """
179         if not context:
180                 context = {}
181
182         return nodes.get_node_context(cr, uid, context).get_uri(cr, uri)
183
184     def get_node_class(self, cr, uid, ids, dbro=None, dynamic=False, context=None):
185         """Retrieve the class of nodes for this directory
186            
187            This function can be overriden by inherited classes ;)
188            @param dbro The browse object, if caller already has it
189         """
190         if dbro is None:
191             dbro = self.browse(cr, uid, ids, context=context)
192
193         if dynamic:
194             return nodes.node_res_obj
195         elif dbro.type == 'directory':
196             return nodes.node_dir
197         elif dbro.type == 'ressource':
198             return nodes.node_res_dir
199         else:
200             raise ValueError("dir node for %s type", dbro.type)
201
202     def _prepare_context(self, cr, uid, nctx, context):
203         """ Fill nctx with properties for this database
204         @param nctx instance of nodes.node_context, to be filled
205         @param context ORM context (dict) for us
206         
207         Note that this function is called *without* a list of ids, 
208         it should behave the same for the whole database (based on the
209         ORM instance of document.directory).
210         
211         Some databases may override this and attach properties to the
212         node_context. See WebDAV, CalDAV.
213         """
214         return
215
216     def get_dir_permissions(self, cr, uid, ids, context=None ):
217         """Check what permission user 'uid' has on directory 'id'
218         """
219         assert len(ids) == 1
220         id = ids[0]
221
222         res = 0
223         for pperms in [('read', 5), ('write', 2), ('unlink', 8)]:
224             try:
225                 self.check_access_rule(cr, uid, ids, pperms[0], context=context)
226                 res |= pperms[1]
227             except except_orm:
228                 pass
229         return res
230
231     def _locate_child(self, cr, uid, root_id, uri,nparent, ncontext):
232         """ try to locate the node in uri,
233             Return a tuple (node_dir, remaining_path)
234         """
235         return (nodes.node_database(context=ncontext), uri)
236
237     def copy(self, cr, uid, id, default=None, context=None):
238         if not default:
239             default ={}
240         name = self.read(cr, uid, [id])[0]['name']
241         default.update({'name': name+ " (copy)"})
242         return super(document_directory,self).copy(cr, uid, id, default, context)
243
244     def _check_duplication(self, cr, uid, vals, ids=[], op='create'):
245         name=vals.get('name',False)
246         parent_id=vals.get('parent_id',False)
247         ressource_parent_type_id=vals.get('ressource_parent_type_id',False)
248         ressource_id=vals.get('ressource_id',0)
249         if op=='write':
250             for directory in self.browse(cr, uid, ids):
251                 if not name:
252                     name=directory.name
253                 if not parent_id:
254                     parent_id=directory.parent_id and directory.parent_id.id or False
255                 # TODO fix algo
256                 if not ressource_parent_type_id:
257                     ressource_parent_type_id=directory.ressource_parent_type_id and directory.ressource_parent_type_id.id or False
258                 if not ressource_id:
259                     ressource_id=directory.ressource_id and directory.ressource_id or 0
260                 res=self.search(cr,uid,[('id','<>',directory.id),('name','=',name),('parent_id','=',parent_id),('ressource_parent_type_id','=',ressource_parent_type_id),('ressource_id','=',ressource_id)])
261                 if len(res):
262                     return False
263         if op=='create':
264             res=self.search(cr,uid,[('name','=',name),('parent_id','=',parent_id),('ressource_parent_type_id','=',ressource_parent_type_id),('ressource_id','=',ressource_id)])
265             if len(res):
266                 return False
267         return True
268     def write(self, cr, uid, ids, vals, context=None):
269         if not self._check_duplication(cr, uid, vals, ids, op='write'):
270             raise osv.except_osv(_('ValidateError'), _('Directory name must be unique!'))
271         return super(document_directory,self).write(cr, uid, ids, vals, context=context)
272
273     def create(self, cr, uid, vals, context=None):
274         if not self._check_duplication(cr, uid, vals):
275             raise osv.except_osv(_('ValidateError'), _('Directory name must be unique!'))
276         newname = vals.get('name',False)
277         if newname:
278             for illeg in ('/', '@', '$', '#'):
279                 if illeg in newname:
280                     raise osv.except_osv(_('ValidateError'), _('Directory name contains special characters!'))
281         return super(document_directory,self).create(cr, uid, vals, context)
282
283     # TODO def unlink(...
284
285 document_directory()
286
287 class document_directory_dctx(osv.osv):
288     """ In order to evaluate dynamic folders, child items could have a limiting
289         domain expression. For that, their parents will export a context where useful
290         information will be passed on.
291         If you define sth like "s_id" = "this.id" at a folder iterating over sales, its
292         children could have a domain like [('sale_id', = ,dctx_s_id )]
293         This system should be used recursively, that is, parent dynamic context will be
294         appended to all children down the tree.
295     """
296     _name = 'document.directory.dctx'
297     _description = 'Directory Dynamic Context'
298     _columns = {
299         'dir_id': fields.many2one('document.directory', 'Directory', required=True, ondelete="cascade"),
300         'field': fields.char('Field', size=20, required=True, select=1, help="The name of the field. Note that the prefix \"dctx_\" will be prepended to what is typed here."),
301         'expr': fields.char('Expression', size=64, required=True, help="A python expression used to evaluate the field.\n" + \
302                 "You can use 'dir_id' for current dir, 'res_id', 'res_model' as a reference to the current record, in dynamic folders"),
303         }
304
305 document_directory_dctx()
306
307
308 class document_directory_node(osv.osv):
309     _inherit = 'process.node'
310     _columns = {
311         'directory_id':  fields.many2one('document.directory', 'Document directory', ondelete="set null"),
312     }
313 document_directory_node()