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