diff --git a/addons/account/account_view.xml b/addons/account/account_view.xml
index aaabc86d865..41da96bdd98 100644
--- a/addons/account/account_view.xml
+++ b/addons/account/account_view.xml
@@ -77,6 +77,7 @@
account.fiscalyearformtree,form,search
+ Define your company's fiscal year depending on the period you have chosen to follow. A fiscal year is a 1 year period over which a company budgets its spending. It may run over any period of 12 months. The fiscal year is referred to by the date in which it ends. For example, if a company's fiscal year ends November 30, 2011, then everything between December 1, 2010 and November 30, 2011 would be referred to as FY 2011. Not using the actual calendar year gives many companies an advantage, allowing them to close their books at a time which is most convenient for them.
@@ -144,6 +145,7 @@
formtree,form,search{'search_default_draft': 1}
+ Here, you can define a period, an interval of time between successive closings of the books of your company. An accounting period typically is a month or a quarter, corresponding to the tax year used by the business. Create and manage them from here and decide whether a period should be left open or closed depending on your company's activities over a specific period.
@@ -254,6 +256,7 @@
tree,form,graph
+ Create and manage accounts you will need to record financial entries in. Accounts are financial records of your company that register all financial transactions. Companies present their annual accounts in two main parts: the balance sheet and the income statement (profit and loss account). The annual accounts of a company are required by law to disclose a certain amount of information. They have to be certified by an external auditor yearly.
@@ -351,6 +354,7 @@
formtree,form
+ Here you can personalize and create each view of your financial journals by selecting the fields you want to appear and the sequence they will appear.
@@ -463,6 +467,7 @@
account.journalformtree,form
+ Create and manage your company's financial journals from this menu. A journal is a business diary in which all financial data related to the day to day business transactions of your company is recorded using double-entry book keeping system. Depending on the nature of its activities and number of daily transactions, a company may keep several types of specialized journals such as a cash journal, purchases journal, and sales journal.
@@ -742,6 +747,7 @@
formtree,form
+ An account type is a name or code given to an account that indicates its purpose. For example, the account type could be linked to an asset account, expense account or payable account. From this view, you can create and manage the account types you need to be used for your company management.
diff --git a/addons/crm/i18n/el.po b/addons/crm/i18n/el.po
index 6fcbd9f09e7..1775000fd34 100644
--- a/addons/crm/i18n/el.po
+++ b/addons/crm/i18n/el.po
@@ -7,13 +7,13 @@ msgstr ""
"Project-Id-Version: OpenERP Server 6.0dev\n"
"Report-Msgid-Bugs-To: support@openerp.com\n"
"POT-Creation-Date: 2010-10-18 17:46+0000\n"
-"PO-Revision-Date: 2010-11-10 16:54+0000\n"
+"PO-Revision-Date: 2010-11-11 18:19+0000\n"
"Last-Translator: Dimitris Andavoglou \n"
"Language-Team: nls@hellug.gr \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"X-Launchpad-Export-Date: 2010-11-11 04:44+0000\n"
+"X-Launchpad-Export-Date: 2010-11-12 04:46+0000\n"
"X-Generator: Launchpad (build Unknown)\n"
"X-Poedit-Country: GREECE\n"
"X-Poedit-Language: Greek\n"
@@ -34,7 +34,7 @@ msgstr "Υποψήφιος Πελάτης"
#. module: crm
#: model:crm.case.categ,name:crm.categ_oppor3
msgid "Need Services"
-msgstr ""
+msgstr "Απαίτηση Υπηρεσιών"
#. module: crm
#: selection:crm.meeting,rrule_type:0
@@ -49,7 +49,7 @@ msgstr "Προγραμμάτισε ένα Τηλεφώνημα"
#. module: crm
#: model:ir.model,name:crm.model_crm_case_stage
msgid "Stage of case"
-msgstr ""
+msgstr "στάδιο της υπόθεσης"
#. module: crm
#: view:crm.meeting:0
@@ -92,18 +92,18 @@ msgstr "Επιλογή Εκαιριών"
#: view:crm.phonecall2phonecall:0
#: view:crm.send.mail:0
msgid " "
-msgstr ""
+msgstr " "
#. module: crm
#: view:crm.lead.report:0
#: field:crm.phonecall.report,delay_close:0
msgid "Delay to close"
-msgstr ""
+msgstr "Καθηστέρηση για κλείσιμο"
#. module: crm
#: view:crm.lead:0
msgid "Previous Stage"
-msgstr ""
+msgstr "Προηγούμενο Στάδιο"
#. module: crm
#: code:addons/crm/wizard/crm_add_note.py:0
@@ -142,7 +142,7 @@ msgstr ""
#: code:addons/crm/crm_lead.py:0
#, python-format
msgid "The lead '%s' has been closed."
-msgstr ""
+msgstr "Ο Υποψήφιος Πελάτης '%s' έχει κλείσει"
#. module: crm
#: selection:crm.meeting,freq:0
@@ -267,7 +267,7 @@ msgstr ""
#: code:addons/crm/crm_opportunity.py:0
#, python-format
msgid "The opportunity '%s' has been marked as lost."
-msgstr ""
+msgstr "Αυτή η ευκαιρία '%s' χαρακτηρίστηκε χαμένη"
#. module: crm
#: view:crm.lead:0
@@ -298,7 +298,7 @@ msgstr "Τμήματα"
#. module: crm
#: view:crm.merge.opportunity:0
msgid "_Merge"
-msgstr ""
+msgstr "_Συγχώνευση"
#. module: crm
#: view:crm.lead.report:0
@@ -348,7 +348,7 @@ msgstr ""
#. module: crm
#: model:process.transition,name:crm.process_transition_leadpartner0
msgid "Prospect Partner"
-msgstr ""
+msgstr "Προσδοκούμενος Συνεργάτης"
#. module: crm
#: field:crm.lead,contact_name:0
@@ -372,7 +372,7 @@ msgstr "Επαφή"
#. module: crm
#: field:crm.case.stage,on_change:0
msgid "Change Probability Automatically"
-msgstr ""
+msgstr "Αλλαξε την Πιθανότητα Αυτόματα"
#. module: crm
#: field:base.action.rule,regex_history:0
@@ -383,7 +383,7 @@ msgstr ""
#: code:addons/crm/crm_lead.py:0
#, python-format
msgid "The lead '%s' has been opened."
-msgstr ""
+msgstr "Ο Υποψήφιος '%s' έχει ανοίξει"
#. module: crm
#: model:process.transition,name:crm.process_transition_opportunitymeeting0
@@ -399,12 +399,12 @@ msgstr "Ημέρες για το κλείσιμο της υπόθεσης"
#. module: crm
#: model:process.node,note:crm.process_node_opportunities0
msgid "When a real project/opportunity is detected"
-msgstr ""
+msgstr "Όταν ένα παραγματικό έργο/ευκαιρία εντοπίζεται"
#. module: crm
#: field:crm.installer,crm_fundraising:0
msgid "Fundraising"
-msgstr ""
+msgstr "Συλλογή Κεφαλαίου"
#. module: crm
#: view:res.partner:0
@@ -430,7 +430,7 @@ msgstr "Αλλαγή Υπεύθυνου"
#. module: crm
#: field:crm.merge.opportunity,state:0
msgid "Set State To"
-msgstr ""
+msgstr "Θέσε την Κατάσταση Σε"
#. module: crm
#: model:ir.model,name:crm.model_crm_lead2opportunity_action
@@ -451,7 +451,7 @@ msgstr ""
#: view:crm.lead2opportunity.action:0
#: field:crm.lead2opportunity.action,name:0
msgid "Select Action"
-msgstr ""
+msgstr "Επιλογή Ενέργειας"
#. module: crm
#: field:base.action.rule,trg_categ_id:0
@@ -499,7 +499,7 @@ msgstr "Τόπος Γεγονότος"
#. module: crm
#: field:crm.meeting,rrule:0
msgid "Recurrent Rule"
-msgstr ""
+msgstr "Επαναλαμβανόμενος Κανόνας"
#. module: crm
#: help:crm.installer,fetchmail:0
@@ -514,7 +514,7 @@ msgstr "Κανονική ή συνάντηση μέσω τηλεφώνου γι
#. module: crm
#: model:process.node,note:crm.process_node_leads0
msgid "Very first contact with new prospect"
-msgstr ""
+msgstr "Προταρχική επαφή με νέα προοπτική"
#. module: crm
#: code:addons/crm/crm_lead.py:0
@@ -584,7 +584,7 @@ msgstr ""
#. module: crm
#: field:crm.meeting,count:0
msgid "Count"
-msgstr ""
+msgstr "Υπολόγισε"
#. module: crm
#: selection:crm.lead.report,month:0
@@ -674,7 +674,7 @@ msgstr ""
#: code:addons/crm/crm_meeting.py:0
#, python-format
msgid "The meeting '%s' has been confirmed."
-msgstr ""
+msgstr "Η Συνάντηση '%s' έχει επιβεβαιωθεί"
#. module: crm
#: field:crm.case.section,user_id:0
@@ -693,6 +693,8 @@ msgid ""
"The email address put in the 'Reply-To' of all emails sent by OpenERP about "
"cases in this sales team"
msgstr ""
+"Η διεύθυνση email που τοποθετήθηκε στο ' Απάντησε- Σε' όλα τα email που "
+"στάλθηκαν από το OpenERP σχετικά με υποθέσεις σε αυτή την ομάδα πώλησης"
#. module: crm
#: view:res.users:0
@@ -717,6 +719,8 @@ msgid ""
"If the active field is set to true, it will allow you to hide the event "
"alarm information without removing it."
msgstr ""
+"Εάν το ενεργό πεδίο είναι επιλεγμένο, σας επιτρέπει να κρύψεται την "
+"υπενθύμιση χωρίς να την απομακρύνεται."
#. module: crm
#: view:crm.lead:0
@@ -744,6 +748,8 @@ msgid ""
"Manages the suppliers and customers claims, including your corrective or "
"preventive actions."
msgstr ""
+"Διαχειρίζεται τους προμηθευτές τις απαιτήσεις πελατών, περιλαμβάνοντας "
+"διορθωτικές ή προληπτικές δράσεις"
#. module: crm
#: view:crm.lead:0
@@ -759,7 +765,7 @@ msgstr "Καταμερισμός Συνεργατών"
#. module: crm
#: field:crm.lead.report,probable_revenue:0
msgid "Probable Revenue"
-msgstr ""
+msgstr "Εκτιμώμενα Έσοδα"
#. module: crm
#: help:crm.segmentation,name:0
@@ -860,7 +866,7 @@ msgstr "Αποκλειστικά"
#: code:addons/crm/crm_opportunity.py:0
#, python-format
msgid "The opportunity '%s' has been won."
-msgstr ""
+msgstr "Η ευκαιρία '%s' κερδήθηκε"
#. module: crm
#: help:crm.case.section,active:0
@@ -868,6 +874,8 @@ msgid ""
"If the active field is set to true, it will allow you to hide the sales team "
"without removing it."
msgstr ""
+"Εάν το ενεργό πεδίο είναι επιλεγμένο, σας επιτρέπει να κρήψεται την ομάδα "
+"πωλήσεων χωρίς να την απομακρύνεται"
#. module: crm
#: help:crm.meeting,alarm_id:0
@@ -946,7 +954,7 @@ msgstr "Μετατροπή σε Ευκαιρία "
#. module: crm
#: help:crm.case.stage,sequence:0
msgid "Gives the sequence order when displaying a list of case stages."
-msgstr ""
+msgstr "Δίνει την σειρά που εμφανίζονται τα στάδια των υποθέσεων"
#. module: crm
#: view:crm.lead:0
@@ -976,7 +984,7 @@ msgstr "Προμηθευτής"
#. module: crm
#: help:crm.send.mail,reply_to:0
msgid "Reply-to of the Sales team defined on this case"
-msgstr ""
+msgstr "Απάντηση-στην ομάδα Πωλήσεων που ορίστικε σε αυτήν την υπόθεση"
#. module: crm
#: view:crm.lead:0
@@ -1004,7 +1012,7 @@ msgstr "Μάρτιος"
#: code:addons/crm/crm_lead.py:0
#, python-format
msgid "The opportunity '%s' has been closed."
-msgstr ""
+msgstr "Η ευκαιρία '%s' έχει κλείσει"
#. module: crm
#: field:crm.lead,day_open:0
@@ -1037,7 +1045,7 @@ msgstr "Κινητό"
#. module: crm
#: view:crm.lead:0
msgid "Next Stage"
-msgstr ""
+msgstr "Επόμενο Στάδιο"
#. module: crm
#: view:board.board:0
@@ -1080,12 +1088,12 @@ msgstr "Επόμενη Ενέργεια"
#. module: crm
#: field:crm.meeting,end_date:0
msgid "Repeat Until"
-msgstr ""
+msgstr "Επανάληψη Μέχρι"
#. module: crm
#: view:crm.meeting:0
msgid "Extended Options..."
-msgstr ""
+msgstr "Εκτεταμένες Επιλογές"
#. module: crm
#: field:crm.meeting,date_deadline:0
@@ -1127,12 +1135,12 @@ msgstr ""
#: view:crm.lead2opportunity.action:0
#: view:res.partner:0
msgid "Next"
-msgstr ""
+msgstr "Επόμενο"
#. module: crm
#: view:crm.lead:0
msgid "Stage:"
-msgstr ""
+msgstr "Στάδιο:"
#. module: crm
#: model:crm.case.stage,name:crm.stage_lead5
@@ -1202,7 +1210,7 @@ msgstr "Επανάληψη κάθε χ"
#. module: crm
#: model:crm.case.categ,name:crm.categ_oppor5
msgid "Need a Website Design"
-msgstr ""
+msgstr "Χρειάζεται Σχεδιαδμό Ιστοσελίδας"
#. module: crm
#: field:crm.meeting,recurrent_uid:0
@@ -1229,7 +1237,7 @@ msgstr ""
#: view:crm.lead.report:0
#: field:crm.lead.report,stage_id:0
msgid "Stage"
-msgstr ""
+msgstr "Στάδιο"
#. module: crm
#: view:crm.lead:0
@@ -1244,17 +1252,17 @@ msgstr "Άκυρο XML για Αρχιτεκτονική Όψης!"
#. module: crm
#: field:base.action.rule,act_mail_to_partner:0
msgid "Mail to Partner"
-msgstr ""
+msgstr "Αληλλογραφία σε Συνεργάτη"
#. module: crm
#: view:crm.lead:0
msgid "Mailings"
-msgstr ""
+msgstr "Αλληλογραφία"
#. module: crm
#: field:crm.meeting,class:0
msgid "Mark as"
-msgstr ""
+msgstr "Μαρκάρισμα ως"
#. module: crm
#: help:crm.meeting,rrule_type:0
@@ -1274,7 +1282,7 @@ msgstr ""
#: model:ir.actions.act_window,name:crm.crm_lead_stage_act
#: model:ir.ui.menu,name:crm.menu_crm_lead_stage_act
msgid "Stages"
-msgstr ""
+msgstr "Στάδια"
#. module: crm
#: field:crm.lead,planned_revenue:0
@@ -1282,7 +1290,7 @@ msgstr ""
#: field:crm.partner2opportunity,planned_revenue:0
#: field:crm.phonecall2opportunity,planned_revenue:0
msgid "Expected Revenue"
-msgstr ""
+msgstr "Προσδοκόμενα Έσοδα"
#. module: crm
#: selection:crm.lead.report,month:0
@@ -1306,7 +1314,7 @@ msgstr "Ανάλυση Τηλεφωνημάτων"
#: field:crm.lead.report,opening_date:0
#: field:crm.phonecall.report,opening_date:0
msgid "Opening Date"
-msgstr ""
+msgstr "Μέρα Έναρξης"
#. module: crm
#: help:crm.phonecall,duration:0
@@ -1387,14 +1395,14 @@ msgstr "Δευτέρα"
#. module: crm
#: field:crm.lead,day_close:0
msgid "Days to Close"
-msgstr ""
+msgstr "Μέρες για Κλείσιμο"
#. module: crm
#: field:crm.add.note,attachment_ids:0
#: field:crm.case.section,complete_name:0
#: field:crm.send.mail,attachment_ids:0
msgid "unknown"
-msgstr ""
+msgstr "άγνωστο"
#. module: crm
#: field:crm.lead,id:0
@@ -1424,7 +1432,7 @@ msgstr "Ημερ/νία"
#: view:crm.lead.report:0
#: view:crm.phonecall.report:0
msgid "Extended Filters..."
-msgstr ""
+msgstr "Εκτεταμένα Φίλτρα..."
#. module: crm
#: model:ir.model,name:crm.model_calendar_attendee
@@ -1473,7 +1481,7 @@ msgstr ""
#: view:crm.phonecall.report:0
#: field:crm.phonecall.report,state:0
msgid "State"
-msgstr ""
+msgstr "Κατάσταση"
#. module: crm
#: model:crm.case.resource.type,name:crm.type_lead1
@@ -1525,7 +1533,7 @@ msgstr ""
#: view:crm.meeting:0
#: view:crm.phonecall:0
msgid "Current"
-msgstr ""
+msgstr "Τρέχον"
#. module: crm
#: field:crm.meeting,exrule:0
@@ -1545,7 +1553,7 @@ msgstr "Κατηγορίες Τηλεφωνημάτων"
#. module: crm
#: view:crm.meeting:0
msgid "Invite People"
-msgstr ""
+msgstr "Κάλεσε Κόσμο"
#. module: crm
#: view:board.board:0
@@ -1555,7 +1563,7 @@ msgstr "Ευκαιρίες ανά Κατηγορία"
#. module: crm
#: constraint:crm.case.section:0
msgid "Error ! You cannot create recursive Sales team."
-msgstr ""
+msgstr "Λάθος! δεν μπορείς να δημιουργήσεις κενή ομάδα Πωλήσεων"
#. module: crm
#: view:crm.meeting:0
@@ -1640,7 +1648,7 @@ msgstr ""
#: view:crm.lead:0
#: view:crm.phonecall:0
msgid "Creation"
-msgstr ""
+msgstr "Δημιουργία"
#. module: crm
#: selection:crm.lead,priority:0
@@ -1668,7 +1676,7 @@ msgstr "Σάββατο"
#. module: crm
#: selection:crm.meeting,byday:0
msgid "Fifth"
-msgstr ""
+msgstr "Πέμπτος"
#. module: crm
#: view:crm.phonecall2phonecall:0
@@ -1702,7 +1710,7 @@ msgstr "Προγραμματισμένα Έσοδα"
#: view:crm.phonecall:0
#: view:crm.phonecall.report:0
msgid "Group By..."
-msgstr ""
+msgstr "Ομαδοποίσηση Κατά..."
#. module: crm
#: help:crm.lead,partner_id:0
@@ -1743,7 +1751,7 @@ msgstr "Τ.Κ."
#: code:addons/crm/crm_lead.py:0
#, python-format
msgid "The case '%s' has been opened."
-msgstr ""
+msgstr "Η Υπόθεση '%s' έχει ανοίξει"
#. module: crm
#: view:crm.installer:0
@@ -1845,7 +1853,7 @@ msgstr "Λεπτά"
#. module: crm
#: view:crm.case.section:0
msgid "Select stages for this Sales Team"
-msgstr ""
+msgstr "Επιλέξτε στάδια για αυτήν την Ομάδα Πωλήσεων"
#. module: crm
#: code:addons/crm/wizard/crm_merge_opportunities.py:0
@@ -1864,7 +1872,7 @@ msgstr ""
#: view:crm.partner2opportunity:0
#: model:ir.actions.act_window,name:crm.phonecall2opportunity_act
msgid "Convert To Opportunity"
-msgstr ""
+msgstr "Μετατροπή σε Ευκαιρία"
#. module: crm
#: view:crm.phonecall:0
@@ -1894,7 +1902,7 @@ msgstr "Συγχώνευση Ευκαιριών"
#. module: crm
#: model:crm.case.resource.type,name:crm.type_lead5
msgid "Google Adwords"
-msgstr ""
+msgstr "Google Adwords"
#. module: crm
#: model:ir.model,name:crm.model_crm_phonecall
@@ -1904,17 +1912,17 @@ msgstr ""
#. module: crm
#: model:crm.case.resource.type,name:crm.type_lead3
msgid "Mail Campaign 2"
-msgstr ""
+msgstr "Εξτρατεία Αλληλογραφίας 1"
#. module: crm
#: model:crm.case.resource.type,name:crm.type_lead2
msgid "Mail Campaign 1"
-msgstr ""
+msgstr "Εξτρατεία Αλληλογραφίας 1"
#. module: crm
#: view:crm.lead:0
msgid "Create"
-msgstr ""
+msgstr "Δημιουργία"
#. module: crm
#: code:addons/crm/crm.py:0
diff --git a/addons/document/__openerp__.py b/addons/document/__openerp__.py
index 5741a7223e0..5d0088579a1 100644
--- a/addons/document/__openerp__.py
+++ b/addons/document/__openerp__.py
@@ -22,7 +22,7 @@
{
'name': 'Integrated Document Management System',
- 'version': '2.0',
+ 'version': '2.1',
'category': 'Generic Modules/Others',
'description': """This is a complete document management system:
* User Authentication
diff --git a/addons/document/content_index.py b/addons/document/content_index.py
index df2b12b5456..6d49a29873f 100644
--- a/addons/document/content_index.py
+++ b/addons/document/content_index.py
@@ -71,8 +71,10 @@ class indexer(object):
pass
fp = open(realfile,'rb')
- content2 = fp.read()
- fp.close()
+ try:
+ content2 = fp.read()
+ finally:
+ fp.close()
# The not-handled exception may be raised here
return self._doIndexContent(content2)
@@ -164,8 +166,10 @@ class contentIndex(object):
os.close(fd)
fp = Popen(['file','-b','--mime',fname], shell=False, stdout=PIPE).stdout
- result = fp.read()
- fp.close()
+ try:
+ result = fp.read()
+ finally:
+ fp.close()
mime2 = result.split(';')[0]
self.__logger.debug('File gave us: %s', mime2)
# Note that the temporary file still exists now.
diff --git a/addons/document/doc/access_permissions.rst b/addons/document/doc/access_permissions.rst
new file mode 100644
index 00000000000..2cdc45dadab
--- /dev/null
+++ b/addons/document/doc/access_permissions.rst
@@ -0,0 +1,65 @@
+Access control in the Document Management system
+================================================
+
+The purpose is to let the DMS act as a real-life management system for
+the file handling of some small business.
+The key concept, there, is the separation of access according to users
+and groups.
+
+Fact 1: Users, in general, must NOT see each other's documents, not even
+their names (because they usually imply sensitive data, like eg. a doc:
+"Acme Company's Patent 012356 about fooobars.odf" )
+Fact 2: Users, sometimes, fail to comprehend complex ACL schemes, so we
+owe to keep things simple, a main principle applied all over the place.
+Fact 3: our system has both statically placed files and directories, as
+well as dynamic (aka "resources" in our terminology) nodes.
+
+We allow/limit the access based on 3 factors (fields):
+ - The "owner" field, which holds the user that created or "owns" the
+ file or directory.
+ - The "group_ids" field, on directories, which specifieds group-wise
+ access
+ - The "company_id" field, for multi-company access rules [1]
+
+[1] at multi-company setups, we may want the same file hierarchy to apply
+to different companies, and also nodes to be company-specific in it.
+
+Principle of "owner"
+----------------------
+Files or directories that have an empty "owner" field are public. All users
+will be able to _read_ them. Only the OpenERP Administrator or specified
+groups, however, will be able to modify them!
+Files or directories that have an "owner" are private. Only their users will
+be able to read or modify (including delete) them.
+By default, all user's files are created with "owner" field set, thus private.
+
+Principle of "group_ids"
+-------------------------
+Directories that have any group ids set will only (apart from their owner)
+allow members of these groups to read them.
+Directories that are created into the above directories will initially inherit
+(that is, copy) the group_ids of their parents, so that they also allow
+access to the same users.
+
+Implementation note
+---------------------
+Most of the principles are applied through record rules (see ir.rule object),
+so an administrator can actually readjust them.
+In order to have logical "areas" of folders, where different policies apply
+(like group folders, personal areas), default values for directories' owners
+and group_ids can be tuned (through the 'set default' functionality of
+fields).
+
+Summary
+--------
+
+Table of permissions and behavior
+
+|| Type | Owner set | Groups set | Description ||
+|| Public | - | - | Public-readable folders, admin can write ||
+|| Group | - | X | Group can read, write, delete in them ||
+|| Group-read | X | X | Group can read[2], owner can write/delete ||
+|| Private | X | - | Only owner can read, write, delete in. ||
+
+[2] hint: using a wide group, like "Internal users" at this setup creates the
+ effect of public-readable folders, with write permission to a non-admin user.
diff --git a/addons/document/document.py b/addons/document/document.py
index 470d67c582b..e3d810668b4 100644
--- a/addons/document/document.py
+++ b/addons/document/document.py
@@ -74,7 +74,7 @@ class document_file(osv.osv):
'create_uid': fields.many2one('res.users', 'Creator', readonly=True),
'write_date': fields.datetime('Date Modified', readonly=True),
'write_uid': fields.many2one('res.users', 'Last Modification User', readonly=True),
- 'res_model': fields.char('Attached Model', size=64, readonly=True),
+ 'res_model': fields.char('Attached Model', size=64, readonly=True, change_default=True),
'res_id': fields.integer('Attached ID', readonly=True),
# If ir.attachment contained any data before document is installed, preserve
@@ -86,7 +86,7 @@ class document_file(osv.osv):
'user_id': fields.many2one('res.users', 'Owner', select=1),
# 'group_ids': fields.many2many('res.groups', 'document_group_rel', 'item_id', 'group_id', 'Groups'),
# the directory id now is mandatory. It can still be computed automatically.
- 'parent_id': fields.many2one('document.directory', 'Directory', select=1, required=True),
+ 'parent_id': fields.many2one('document.directory', 'Directory', select=1, required=True, change_default=True),
'index_content': fields.text('Indexed Content'),
'partner_id':fields.many2one('res.partner', 'Partner', select=1),
'file_size': fields.integer('File Size', required=True),
@@ -245,7 +245,7 @@ class document_file(osv.osv):
# files to be unlinked, update the db (safer to do first, can be
# rolled back) and then unlink the files. The list wouldn't exist
# after we discard the objects
-
+ ids = self.search(cr, uid, [('id','in',ids)])
for f in self.browse(cr, uid, ids, context):
# TODO: update the node cache
par = f.parent_id
diff --git a/addons/document/document_data.xml b/addons/document/document_data.xml
index ef829b6e258..a4f546cbf83 100644
--- a/addons/document/document_data.xml
+++ b/addons/document/document_data.xml
@@ -16,13 +16,13 @@
Documents
-
+ 0
- My Folder
+ Admin Folder0
@@ -37,7 +37,7 @@
0
-
+
@@ -46,7 +46,7 @@
[('category_id','in',[active_id])]
-
+ 0
@@ -55,14 +55,14 @@
Personal Foldersressource
+ 0
-
Products
-
+ 0
@@ -70,7 +70,7 @@
Sales Order
-
+ 0
@@ -78,7 +78,7 @@
All Sales Order
-
+ 0
@@ -86,7 +86,7 @@
Quotations
-
+ 0
@@ -94,7 +94,7 @@
Projects
-
+ 0
diff --git a/addons/document/document_directory.py b/addons/document/document_directory.py
index 9b7ba18b16f..9412527db22 100644
--- a/addons/document/document_directory.py
+++ b/addons/document/document_directory.py
@@ -21,8 +21,8 @@
from osv import osv, fields
+from osv.orm import except_orm
-import os
import nodes
from tools.translate import _
@@ -38,9 +38,9 @@ class document_directory(osv.osv):
'create_uid': fields.many2one('res.users', 'Creator', readonly=True),
'domain': fields.char('Domain', size=128, help="Use a domain if you want to apply an automatic filter on visible resources."),
'user_id': fields.many2one('res.users', 'Owner'),
- 'storage_id': fields.many2one('document.storage', 'Storage'),
+ 'storage_id': fields.many2one('document.storage', 'Storage', change_default=True),
'group_ids': fields.many2many('res.groups', 'document_directory_group_rel', 'item_id', 'group_id', 'Groups'),
- 'parent_id': fields.many2one('document.directory', 'Parent Directory', select=1),
+ 'parent_id': fields.many2one('document.directory', 'Parent Directory', select=1, change_default=True),
'child_ids': fields.one2many('document.directory', 'parent_id', 'Children'),
'file_ids': fields.one2many('ir.attachment', 'parent_id', 'Files'),
'content_ids': fields.one2many('document.directory.content', 'directory_id', 'Virtual Files'),
@@ -48,22 +48,24 @@ class document_directory(osv.osv):
('directory','Static Directory'),
('ressource','Folders per resource'),
],
- 'Type', required=True, select=1,
+ 'Type', required=True, select=1, change_default=True,
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."),
- 'ressource_type_id': fields.many2one('ir.model', 'Resource model',
+ 'ressource_type_id': fields.many2one('ir.model', 'Resource model', change_default=True,
help="Select an object here and there will be one folder per record of that resource."),
'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.'),
'resource_find_all': fields.boolean('Find all resources', required=True,
help="If true, all attachments that match this resource will " \
" be located. If false, only ones that have this as parent." ),
- 'ressource_parent_type_id': fields.many2one('ir.model', 'Parent Model',
+ 'ressource_parent_type_id': fields.many2one('ir.model', 'Parent Model', change_default=True,
help="If you put an object here, this directory template will appear bellow all of these objects. " \
+ "Such directories are \"attached\" to the specific model or record, just like attachments. " \
"Don't put a parent directory if you select a parent model."),
- 'ressource_id': fields.integer('Resource ID'),
+ 'ressource_id': fields.integer('Resource ID',
+ help="Along with Parent Model, this ID attaches this folder to a specific record of Parent Model."),
'ressource_tree': fields.boolean('Tree Structure',
help="Check this if you want to use the same tree structure as the object selected in the system."),
'dctx_ids': fields.one2many('document.directory.dctx', 'dir_id', 'Context fields'),
- 'company_id': fields.many2one('res.company', 'Company'),
+ 'company_id': fields.many2one('res.company', 'Company', change_default=True),
}
@@ -98,10 +100,10 @@ class document_directory(osv.osv):
_defaults = {
'company_id': lambda s,cr,uid,c: s.pool.get('res.company')._company_default_get(cr, uid, 'document.directory', context=c),
'user_id': lambda self,cr,uid,ctx: uid,
- 'domain': lambda self,cr,uid,ctx: '[]',
- 'type': lambda *args: 'directory',
- 'ressource_id': lambda *a: 0,
- 'storage_id': _get_def_storage,
+ 'domain': '[]',
+ 'type': 'directory',
+ 'ressource_id': 0,
+ 'storage_id': _get_def_storage, # Still, it is bad practice to set it everywhere.
'resource_find_all': True,
}
_sql_constraints = [
@@ -188,7 +190,6 @@ class document_directory(osv.osv):
dbro = self.browse(cr, uid, ids, context=context)
if dynamic:
- assert dbro.type == 'directory'
return nodes.node_res_obj
elif dbro.type == 'directory':
return nodes.node_dir
@@ -211,22 +212,19 @@ class document_directory(osv.osv):
"""
return
- def get_dir_permissions(self, cr, uid, ids ):
+ def get_dir_permissions(self, cr, uid, ids, context=None ):
"""Check what permission user 'uid' has on directory 'id'
"""
assert len(ids) == 1
- id = ids[0]
- cr.execute( "SELECT count(dg.item_id) AS needs, count(ug.uid) AS has " \
- " FROM document_directory_group_rel dg " \
- " LEFT OUTER JOIN res_groups_users_rel ug " \
- " ON (dg.group_id = ug.gid AND ug.uid = %s) " \
- " WHERE dg.item_id = %s ", (uid, id))
- needs, has = cr.fetchone()
- if needs and not has:
- return 1 # still allow to descend into.
- else:
- return 15
+ res = 0
+ for pperms in [('read', 5), ('write', 2), ('unlink', 8)]:
+ try:
+ self.check_access_rule(cr, uid, ids, pperms[0], context=context)
+ res |= pperms[1]
+ except except_orm:
+ pass
+ return res
def _locate_child(self, cr, uid, root_id, uri,nparent, ncontext):
""" try to locate the node in uri,
diff --git a/addons/document/document_storage.py b/addons/document/document_storage.py
index 0ecc01941b4..aa2edb6b411 100644
--- a/addons/document/document_storage.py
+++ b/addons/document/document_storage.py
@@ -26,6 +26,7 @@ import tools
import base64
import errno
import logging
+import shutil
from StringIO import StringIO
import psycopg2
@@ -48,7 +49,7 @@ DMS_ROOT_PATH = tools.config.get('document_path', os.path.join(tools.config.get(
We have to consider 3 cases of data /retrieval/:
Given (context,path) we need to access the file (aka. node).
given (directory, context), we need one of its children (for listings, views)
- given (ir.attachment, context), we needs its data and metadata (node).
+ given (ir.attachment, context), we need its data and metadata (node).
For data /storage/ we have the cases:
Have (ir.attachment, context), we modify the file (save, update, rename etc).
@@ -100,10 +101,17 @@ class nodefd_file(nodes.node_descriptor):
if mode.endswith('b'):
mode = mode[:-1]
self.mode = mode
+ self._size = os.stat(path).st_size
- for attr in ('closed', 'read', 'write', 'seek', 'tell'):
+ for attr in ('closed', 'read', 'write', 'seek', 'tell', 'next'):
setattr(self,attr, getattr(self.__file, attr))
+ def size(self):
+ return self._size
+
+ def __iter__(self):
+ return self
+
def close(self):
# TODO: locking in init, close()
fname = self.__file.name
@@ -165,6 +173,7 @@ class nodefd_db(StringIO, nodes.node_descriptor):
"""
def __init__(self, parent, ira_browse, mode):
nodes.node_descriptor.__init__(self, parent)
+ self._size = 0L
if mode.endswith('b'):
mode = mode[:-1]
@@ -172,6 +181,8 @@ class nodefd_db(StringIO, nodes.node_descriptor):
cr = ira_browse._cr # reuse the cursor of the browse object, just now
cr.execute('SELECT db_datas FROM ir_attachment WHERE id = %s',(ira_browse.id,))
data = cr.fetchone()[0]
+ if data:
+ self._size = len(data)
StringIO.__init__(self, data)
elif mode in ('w', 'w+'):
StringIO.__init__(self, None)
@@ -184,6 +195,9 @@ class nodefd_db(StringIO, nodes.node_descriptor):
raise IOError(errno.EINVAL, "Invalid file mode")
self.mode = mode
+ def size(self):
+ return self._size
+
def close(self):
# we now open a *separate* cursor, to update the data.
# FIXME: this may be improved, for concurrency handling
@@ -241,11 +255,15 @@ class nodefd_db64(StringIO, nodes.node_descriptor):
"""
def __init__(self, parent, ira_browse, mode):
nodes.node_descriptor.__init__(self, parent)
+ self._size = 0L
if mode.endswith('b'):
mode = mode[:-1]
if mode in ('r', 'r+'):
- StringIO.__init__(self, base64.decodestring(ira_browse.db_datas))
+ data = base64.decodestring(ira_browse.db_datas)
+ if data:
+ self._size = len(data)
+ StringIO.__init__(self, data)
elif mode in ('w', 'w+'):
StringIO.__init__(self, None)
# at write, we start at 0 (= overwrite), but have the original
@@ -257,6 +275,9 @@ class nodefd_db64(StringIO, nodes.node_descriptor):
raise IOError(errno.EINVAL, "Invalid file mode")
self.mode = mode
+ def size(self):
+ return self._size
+
def close(self):
# we now open a *separate* cursor, to update the data.
# FIXME: this may be improved, for concurrency handling
@@ -452,11 +473,11 @@ class document_storage(osv.osv):
elif boo.type == 'realstore':
path, npath = self.__prepare_realpath(cr, file_node, ira, boo.path,
- do_create = (mode[1] in ('w','a')) )
+ do_create = (mode[0] in ('w','a')) )
fpath = os.path.join(path, npath[-1])
- if (not os.path.exists(fpath)) and mode[1] == 'r':
+ if (not os.path.exists(fpath)) and mode[0] == 'r':
raise IOError("File not found: %s" % fpath)
- elif mode[1] in ('w', 'a') and not ira.store_fname:
+ elif mode[0] in ('w', 'a') and not ira.store_fname:
store_fname = os.path.join(*npath)
cr.execute('UPDATE ir_attachment SET store_fname = %s WHERE id = %s',
(store_fname, ira.id))
@@ -541,9 +562,11 @@ class document_storage(osv.osv):
try:
store_fname = self.__get_random_fname(path)
fname = os.path.join(path, store_fname)
- fp = file(fname, 'wb')
- fp.write(data)
- fp.close()
+ fp = open(fname, 'wb')
+ try:
+ fp.write(data)
+ finally:
+ fp.close()
self._doclog.debug( "Saved data to %s" % fname)
filesize = len(data) # os.stat(fname).st_size
@@ -568,9 +591,11 @@ class document_storage(osv.osv):
try:
path, npath = self.__prepare_realpath(cr, file_node, ira, boo.path, do_create=True)
fname = os.path.join(path, npath[-1])
- fp = file(fname,'wb')
- fp.write(data)
- fp.close()
+ fp = open(fname,'wb')
+ try:
+ fp.write(data)
+ finally:
+ fp.close()
self._doclog.debug("Saved data to %s", fname)
filesize = len(data) # os.stat(fname).st_size
store_fname = os.path.join(*npath)
@@ -649,7 +674,7 @@ class document_storage(osv.osv):
if ktype == 'file':
try:
os.unlink(fname)
- except Exception, e:
+ except Exception:
self._doclog.warning("Could not remove file %s, please remove manually.", fname, exc_info=True)
else:
self._doclog.warning("Unknown unlink key %s" % ktype)
@@ -729,26 +754,34 @@ class document_storage(osv.osv):
# nothing to do for a rename, allow to change the db field
return { 'parent_id': ndir_bro.id }
elif sbro.type == 'realstore':
- raise NotImplementedError("Cannot move in realstore, yet") # TODO
- fname = fil_bo.store_fname
+ ira = self.pool.get('ir.attachment').browse(cr, uid, file_node.file_id, context=context)
+
+ path, opath = self.__prepare_realpath(cr, file_node, ira, sbro.path, do_create=False)
+ fname = ira.store_fname
+
if not fname:
- return ValueError("Tried to rename a non-stored file")
- path = sbro.path
- oldpath = os.path.join(path, fname)
+ self._doclog.warning("Trying to rename a non-stored file")
+ if fname != os.path.join(*opath):
+ self._doclog.warning("inconsistency in realstore: %s != %s" , fname, repr(opath))
+
+ oldpath = os.path.join(path, opath[-1])
- for ch in ('*', '|', "\\", '/', ':', '"', '<', '>', '?', '..'):
- if ch in new_name:
- raise ValueError("Invalid char %s in name %s" %(ch, new_name))
-
- file_node.fix_ppath(cr, ira)
- npath = file_node.full_path() or []
- dpath = [path,]
- dpath.extend(npath[:-1])
- dpath.append(new_name)
- newpath = os.path.join(*dpath)
- # print "old, new paths:", oldpath, newpath
- os.rename(oldpath, newpath)
- return { 'name': new_name, 'datas_fname': new_name, 'store_fname': new_name }
+ npath = [sbro.path,] + (ndir_bro.get_full_path() or [])
+ npath = filter(lambda x: x is not None, npath)
+ newdir = os.path.join(*npath)
+ if not os.path.isdir(newdir):
+ self._doclog.debug("Must create dir %s", newdir)
+ os.makedirs(newdir)
+ npath.append(opath[-1])
+ newpath = os.path.join(*npath)
+
+ self._doclog.debug("Going to move %s from %s to %s", opath[-1], oldpath, newpath)
+ shutil.move(oldpath, newpath)
+
+ store_path = npath[1:] + [opath[-1],]
+ store_fname = os.path.join(*store_path)
+
+ return { 'store_fname': store_fname }
else:
raise TypeError("No %s storage" % sbro.type)
diff --git a/addons/document/nodes.py b/addons/document/nodes.py
index 68220885dff..ee506d5eb2b 100644
--- a/addons/document/nodes.py
+++ b/addons/document/nodes.py
@@ -175,6 +175,23 @@ class node_descriptor(object):
def write(self, str):
raise NotImplementedError
+ def size(self):
+ raise NotImplementedError
+
+ def __len__(self):
+ return self.size()
+
+ def __nonzero__(self):
+ """ Ensure that a node_descriptor will never equal False
+
+ Since we do define __len__ and __iter__ for us, we must avoid
+ being regarded as non-true objects.
+ """
+ return True
+
+ def next(self, str):
+ raise NotImplementedError
+
class node_class(object):
""" this is a superclass for our inodes
It is an API for all code that wants to access the document files.
@@ -366,12 +383,14 @@ class node_class(object):
def create_child(self, cr, path, data=None):
""" Create a regular file under this node
"""
- raise NotImplementedError(repr(self))
+ logger.warning("Attempted to create a file under %r, not possible.", self)
+ raise IOError(errno.EPERM, "Not allowed to create files here")
def create_child_collection(self, cr, objname):
""" Create a child collection (directory) under self
"""
- raise NotImplementedError(repr(self))
+ logger.warning("Attempted to create a collection under %r, not possible.", self)
+ raise IOError(errno.EPERM, "Not allowed to create folders here")
def rm(self, cr):
raise NotImplementedError(repr(self))
@@ -433,12 +452,12 @@ class node_database(node_class):
return res[0]
return None
- def _child_get(self, cr, name=False, parent_id=False, domain=None):
+ def _child_get(self, cr, name=False, domain=None):
dirobj = self.context._dirobj
uid = self.context.uid
ctx = self.context.context.copy()
ctx.update(self.dctx)
- where = [('parent_id','=',parent_id)]
+ where = [('parent_id','=', False), ('ressource_parent_type_id','=',False)]
if name:
where.append(('name','=',name))
is_allowed = self.check_perms(1)
@@ -448,23 +467,14 @@ class node_database(node_class):
if not is_allowed:
raise IOError(errno.EPERM, "Permission into directory denied")
- if not domain:
- domain = []
-
- where2 = where + domain + ['|', ('type', '=', 'directory'), \
- '&', ('type', '=', 'ressource'), ('ressource_parent_type_id','=',False)]
- ids = dirobj.search(cr, uid, where2, context=ctx)
+ if domain:
+ where = where + domain
+ ids = dirobj.search(cr, uid, where, context=ctx)
res = []
for dirr in dirobj.browse(cr, uid, ids, context=ctx):
klass = dirr.get_node_class(dirr, context=ctx)
res.append(klass(dirr.name, self, self.context,dirr))
- fil_obj = dirobj.pool.get('ir.attachment')
- ids = fil_obj.search(cr, uid, where, context=ctx)
- if ids:
- for fil in fil_obj.browse(cr, uid, ids, context=ctx):
- klass = self.context.node_file_class
- res.append(klass(fil.name, self, self.context, fil))
return res
def _file_get(self,cr, nodename=False):
@@ -485,6 +495,22 @@ def mkdosname(company_name, default='noname'):
return n
+def _uid2unixperms(perms, has_owner):
+ """ Convert the uidperms and the owner flag to full unix bits
+ """
+ res = 0
+ if has_owner:
+ res |= (perms & 0x07) << 6
+ res |= (perms & 0x05) << 3
+ elif perms & 0x02:
+ res |= (perms & 0x07) << 6
+ res |= (perms & 0x07) << 3
+ else:
+ res |= (perms & 0x07) << 6
+ res |= (perms & 0x05) << 3
+ res |= 0x05
+ return res
+
class node_dir(node_database):
our_type = 'collection'
def __init__(self, path, parent, context, dirr, dctx=None):
@@ -499,13 +525,13 @@ class node_dir(node_database):
# TODO: the write date should be MAX(file.write)..
self.write_date = dirr and (dirr.write_date or dirr.create_date) or False
self.content_length = 0
- self.unixperms = 040750
try:
self.uuser = (dirr.user_id and dirr.user_id.login) or 'nobody'
except Exception:
self.uuser = 'nobody'
self.ugroup = mkdosname(dirr.company_id and dirr.company_id.name, default='nogroup')
self.uidperms = dirr.get_dir_permissions()
+ self.unixperms = 040000 | _uid2unixperms(self.uidperms, dirr and dirr.user_id)
if dctx:
self.dctx.update(dctx)
dc2 = self.context.context
@@ -516,7 +542,7 @@ class node_dir(node_database):
for dfld in dirr.dctx_ids:
try:
self.dctx['dctx_' + dfld.field] = safe_eval(dfld.expr,dc2)
- except Exception:
+ except Exception,e:
print "Cannot eval %s" % dfld.expr
print e
pass
@@ -559,7 +585,46 @@ class node_dir(node_database):
return res
def _child_get(self, cr, name=None, domain=None):
- return super(node_dir,self)._child_get(cr, name, self.dir_id, domain=domain)
+ dirobj = self.context._dirobj
+ uid = self.context.uid
+ ctx = self.context.context.copy()
+ ctx.update(self.dctx)
+ where = [('parent_id','=',self.dir_id)]
+ if name:
+ where.append(('name','=',name))
+ is_allowed = self.check_perms(1)
+ else:
+ is_allowed = self.check_perms(5)
+
+ if not is_allowed:
+ raise IOError(errno.EPERM, "Permission into directory denied")
+
+ if not domain:
+ domain = []
+
+ where2 = where + domain + [('ressource_parent_type_id','=',False)]
+ ids = dirobj.search(cr, uid, where2, context=ctx)
+ res = []
+ for dirr in dirobj.browse(cr, uid, ids, context=ctx):
+ klass = dirr.get_node_class(dirr, context=ctx)
+ res.append(klass(dirr.name, self, self.context,dirr))
+
+ # Static directories should never return files with res_model/res_id
+ # because static dirs are /never/ related to a record.
+ # In fact, files related to some model and parented by the root dir
+ # (the default), will NOT be accessible in the node system unless
+ # a resource folder for that model exists (with resource_find_all=True).
+ # Having resource attachments in a common folder is bad practice,
+ # because they would be visible to all users, and their names may be
+ # the same, conflicting.
+ where += [('res_model', '=', False)]
+ fil_obj = dirobj.pool.get('ir.attachment')
+ ids = fil_obj.search(cr, uid, where, context=ctx)
+ if ids:
+ for fil in fil_obj.browse(cr, uid, ids, context=ctx):
+ klass = self.context.node_file_class
+ res.append(klass(fil.name, self, self.context, fil))
+ return res
def rmcol(self, cr):
uid = self.context.uid
@@ -699,13 +764,13 @@ class node_res_dir(node_class):
# TODO: the write date should be MAX(file.write)..
self.write_date = dirr.write_date or dirr.create_date
self.content_length = 0
- self.unixperms = 040750
try:
self.uuser = (dirr.user_id and dirr.user_id.login) or 'nobody'
except Exception:
self.uuser = 'nobody'
self.ugroup = mkdosname(dirr.company_id and dirr.company_id.name, default='nogroup')
self.uidperms = dirr.get_dir_permissions()
+ self.unixperms = 040000 | _uid2unixperms(self.uidperms, dirr and dirr.user_id)
self.res_model = dirr.ressource_type_id and dirr.ressource_type_id.model or False
self.resm_id = dirr.ressource_id
self.res_find_all = dirr.resource_find_all
@@ -759,7 +824,7 @@ class node_res_dir(node_class):
ctx.update(self.dctx)
where = []
if self.domain:
- app = safe_eval(self.domain, self.dctx)
+ app = safe_eval(self.domain, ctx)
if not app:
pass
elif isinstance(app, list):
@@ -823,8 +888,8 @@ class node_res_obj(node_class):
# TODO: the write date should be MAX(file.write)..
self.write_date = parent.write_date
self.content_length = 0
- self.unixperms = 040750
self.uidperms = parent.uidperms & 15
+ self.unixperms = 040000 | _uid2unixperms(self.uidperms, True)
self.uuser = parent.uuser
self.ugroup = parent.ugroup
self.res_model = res_model
@@ -842,7 +907,7 @@ class node_res_obj(node_class):
for fld,expr in self.dctx_dict.items():
try:
self.dctx[fld] = safe_eval(expr, dc2)
- except Exception:
+ except Exception,e:
print "Cannot eval %s for %s" % (expr, fld)
print e
pass
@@ -953,9 +1018,9 @@ class node_res_obj(node_class):
where1 = []
if obj._parent_name in obj.fields_get(cr, uid):
where1 = where + [(obj._parent_name, '=', self.res_id)]
+ namefield = directory.resource_field.name or 'name'
resids = obj.search(cr, uid, where1, context=ctx)
for bo in obj.browse(cr, uid, resids, context=ctx):
- namefield = directory.resource_field.name or 'name'
if not bo:
continue
res_name = getattr(bo, namefield)
@@ -963,7 +1028,7 @@ class node_res_obj(node_class):
continue
# TODO Revise
klass = directory.get_node_class(directory, dynamic=True, context=ctx)
- res.append(klass(res_name, self.dir_id, self, self.context, self.res_model, res_bo = bo))
+ res.append(klass(res_name, dir_id=self.dir_id, parent=self, context=self.context, res_model=self.res_model, res_bo=bo))
where2 = where + [('parent_id','=',self.dir_id) ]
@@ -1361,11 +1426,13 @@ class nodefd_content(StringIO, node_descriptor):
def __init__(self, parent, cr, mode, ctx):
node_descriptor.__init__(self, parent)
self._context=ctx
+ self._size = 0L
if mode in ('r', 'r+'):
cntobj = parent.context._dirobj.pool.get('document.directory.content')
data = cntobj.process_read(cr, parent.context.uid, parent, ctx)
if data:
+ self._size = len(data)
parent.content_length = len(data)
StringIO.__init__(self, data)
elif mode in ('w', 'w+'):
@@ -1379,6 +1446,9 @@ class nodefd_content(StringIO, node_descriptor):
raise IOError(errno.EINVAL, "Invalid file mode")
self.mode = mode
+ def size(self):
+ return self._size
+
def close(self):
# we now open a *separate* cursor, to update the data.
# FIXME: this may be improved, for concurrency handling
@@ -1404,4 +1474,56 @@ class nodefd_content(StringIO, node_descriptor):
cr.close()
StringIO.close(self)
+class nodefd_static(StringIO, node_descriptor):
+ """ A descriptor to nodes with static data.
+ """
+ def __init__(self, parent, cr, mode, ctx=None):
+ node_descriptor.__init__(self, parent)
+ self._context=ctx
+ self._size = 0L
+
+ if mode in ('r', 'r+'):
+ data = parent.get_data(cr)
+ if data:
+ self._size = len(data)
+ parent.content_length = len(data)
+ StringIO.__init__(self, data)
+ elif mode in ('w', 'w+'):
+ StringIO.__init__(self, None)
+ # at write, we start at 0 (= overwrite), but have the original
+ # data available, in case of a seek()
+ elif mode == 'a':
+ StringIO.__init__(self, None)
+ else:
+ logging.getLogger('document.nodes').error("Incorrect mode %s specified", mode)
+ raise IOError(errno.EINVAL, "Invalid file mode")
+ self.mode = mode
+
+ def size(self):
+ return self._size
+
+ def close(self):
+ # we now open a *separate* cursor, to update the data.
+ # FIXME: this may be improved, for concurrency handling
+ if self.mode == 'r':
+ StringIO.close(self)
+ return
+
+ par = self._get_parent()
+ # uid = par.context.uid
+ cr = pooler.get_db(par.context.dbname).cursor()
+ try:
+ if self.mode in ('w', 'w+', 'r+'):
+ data = self.getvalue()
+ par.set_data(cr, data)
+ elif self.mode == 'a':
+ raise NotImplementedError
+ cr.commit()
+ except Exception:
+ logging.getLogger('document.nodes').exception('Cannot update db content #%d for close:', par.cnt_id)
+ raise
+ finally:
+ cr.close()
+ StringIO.close(self)
+
#eof
\ No newline at end of file
diff --git a/addons/document/security/document_security.xml b/addons/document/security/document_security.xml
index 2bf1a2adb57..ca55a8219c1 100644
--- a/addons/document/security/document_security.xml
+++ b/addons/document/security/document_security.xml
@@ -14,6 +14,28 @@
-
+
+
+ ['|','|',('group_ids','in',[g.id for g in user.groups_id]), ('user_id', '=', user.id), '&', ('user_id', '=', False), ('group_ids','=',False), '|', ('company_id','=',False), ('company_id','child_of',[user.company_id.id])]
+ Read public directories
+
+
+
+
+
+
+
+
+
+
+ [ '|', ('user_id', '=', user.id), '&', ('group_ids','in',[g.id for g in user.groups_id]), ('user_id','=',False), '|', ('company_id','=',False), ('company_id','child_of',[user.company_id.id])]
+ Document modify own directories
+
+
+
+
+
+
+
diff --git a/addons/document/security/ir.model.access.csv b/addons/document/security/ir.model.access.csv
index 132daa1b7a6..d7aba7cbde7 100644
--- a/addons/document/security/ir.model.access.csv
+++ b/addons/document/security/ir.model.access.csv
@@ -1,6 +1,7 @@
"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink"
"access_document_directory_all","document.directory all","model_document_directory",,1,0,0,0
"access_document_directory_group_document_manager","document.directory document manager","model_document_directory","base.group_system",1,1,1,1
+"access_document_directory_group_knowledge","document.directory modify","model_document_directory","base.group_document_user",1,1,1,1
"access_document_directory_group_system","document.directory group system","model_document_directory","base.group_system",1,1,1,1
"access_document_directory_content_all","document.directory.content all","model_document_directory_content",,1,0,0,0
"access_document_directory_content_group_document_manager","document.directory.content document manager","model_document_directory_content","base.group_system",1,1,1,1
diff --git a/addons/document_ftp/ftpserver/abstracted_fs.py b/addons/document_ftp/ftpserver/abstracted_fs.py
index 9c16e7dde55..65b17a52c51 100644
--- a/addons/document_ftp/ftpserver/abstracted_fs.py
+++ b/addons/document_ftp/ftpserver/abstracted_fs.py
@@ -146,16 +146,18 @@ class abstracted_fs(object):
ret = child.open_data(cr, mode)
cr.commit()
+ assert ret, "Cannot create descriptor for %r: %r" % (child, ret)
return ret
except EnvironmentError:
raise
- except Exception,e:
+ except Exception:
self._log.exception('Cannot locate item %s at node %s', objname, repr(node))
pass
try:
child = node.create_child(cr, objname, data=None)
ret = child.open_data(cr, mode)
+ assert ret, "cannot create descriptor for %r" % child
cr.commit()
return ret
except EnvironmentError:
diff --git a/addons/document_ftp/ftpserver/ftpserver.py b/addons/document_ftp/ftpserver/ftpserver.py
index 710494bb9db..bd727427541 100644
--- a/addons/document_ftp/ftpserver/ftpserver.py
+++ b/addons/document_ftp/ftpserver/ftpserver.py
@@ -2223,6 +2223,7 @@ class FTPHandler(asynchat.async_chat):
if self.restart_position:
mode = 'r+'
fd = self.try_as_current_user(self.fs.create, (datacr, datacr[2], mode + 'b'))
+ assert fd
except FTPExceptionSent:
self.fs.close_cr(datacr)
return
diff --git a/addons/document_ftp/test/document_ftp_test2.yml b/addons/document_ftp/test/document_ftp_test2.yml
index da55c353767..5da4bcd83ef 100644
--- a/addons/document_ftp/test/document_ftp_test2.yml
+++ b/addons/document_ftp/test/document_ftp_test2.yml
@@ -7,6 +7,7 @@
from document_ftp import test_easyftp as te
ftp = te.get_plain_ftp(timeout=2.0)
assert ftp.sock and (ftp.lastresp == '220'), ftp.lastresp
+ ftp.close()
-
I read the list of databases at port 8021 and confirm our db is
there
@@ -15,6 +16,7 @@
from document_ftp import test_easyftp as te
ftp = te.get_ftp_login(cr, uid, self)
assert cr.dbname in ftp.nlst("/")
+ ftp.close()
-
I try to locate the default "Documents" folder in the db.
-
@@ -22,6 +24,7 @@
from document_ftp import test_easyftp as te
ftp = te.get_ftp_login(cr, uid, self)
ftp.cwd('Documents')
+ ftp.close()
-
I create a "test.txt" file at the server (directly). The file
should have the "abcd" content
@@ -32,6 +35,7 @@
ftp = te.get_ftp_folder(cr, uid, self, 'Documents')
fdata = StringIO('abcd')
ftp.storbinary('STOR test.txt', fdata)
+ ftp.close()
-
I look for the "test.txt" file at the server
-
@@ -39,6 +43,7 @@
from document_ftp import test_easyftp as te
ftp = te.get_ftp_folder(cr, uid, self, 'Documents')
assert ftp.nlst("test.txt") == ['test.txt']
+ ftp.close()
-
I check that the content of "test.txt" is "abcd"
-
@@ -46,7 +51,9 @@
from document_ftp import test_easyftp as te
from cStringIO import StringIO
ftp = te.get_ftp_folder(cr, uid, self, 'Documents')
- assert te.get_ftp_fulldata(ftp, "test.txt") == 'abcd'
+ gotdata = te.get_ftp_fulldata(ftp, "test.txt")
+ ftp.close()
+ assert gotdata == 'abcd', 'Data: %r' % gotdata
-
I append the string 'defgh' to "test.txt"
-
@@ -56,6 +63,7 @@
ftp = te.get_ftp_folder(cr, uid, self, 'Documents')
fdata = StringIO('defgh')
ftp.storbinary('APPE test.txt', fdata)
+ ftp.close()
-
I check that the content of "text.txt" is 'abcddefgh'
-
@@ -63,7 +71,9 @@
from document_ftp import test_easyftp as te
from cStringIO import StringIO
ftp = te.get_ftp_folder(cr, uid, self, 'Documents')
- assert te.get_ftp_fulldata(ftp, "test.txt") == 'abcddefgh'
+ gotdata = te.get_ftp_fulldata(ftp, "test.txt")
+ ftp.close()
+ assert gotdata == 'abcddefgh', 'Data: %r' % gotdata
-
I try to cd into an non-existing folder 'Not-This'
-
@@ -77,7 +87,9 @@
except ftplib.error_perm:
pass
except OSError, err:
+ ftp.close()
assert err.errno == 2, err.errno
+ ftp.close()
-
I create a "test2.txt" file through FTP.
-
@@ -87,6 +99,7 @@
ftp = te.get_ftp_folder(cr, uid, self, 'Documents')
fdata = StringIO('abcd')
ftp.storbinary('STOR test2.txt', fdata)
+ ftp.close()
-
I look for the "test2.txt" file at the server
-
@@ -101,6 +114,7 @@
from cStringIO import StringIO
ftp = te.get_ftp_folder(cr, uid, self, 'Documents')
ftp.delete('test2.txt')
+ ftp.close()
-
I check at the server that test2.txt is deleted
-
@@ -116,6 +130,7 @@
ftp = te.get_ftp_folder(cr, uid, self, 'Documents')
fdata = StringIO('abcd')
ftp.storbinary('STOR test2.txt', fdata)
+ ftp.close()
-
I delete the test2.txt from the server (RPC).
-
@@ -137,6 +152,7 @@
except ftplib.error_perm: # 550 error: 'path not exists'
nlst_result = []
assert "test2.txt" not in nlst_result, "Files: %r" % nlst_result
+ ftp.close()
-
I create a "test-name.txt" file
-
@@ -146,6 +162,7 @@
ftp = te.get_ftp_folder(cr, uid, self, 'Documents')
fdata = StringIO('abcd')
ftp.storbinary('STOR test-name.txt', fdata)
+ ftp.close()
-
I rename the "test-name.txt" file through ftp.
-
@@ -153,6 +170,7 @@
from document_ftp import test_easyftp as te
ftp = te.get_ftp_folder(cr, uid, self, 'Documents')
ftp.rename("test-name.txt", "test-renamed.txt")
+ ftp.close()
-
I check that test-name.txt has been renamed.
-
@@ -166,6 +184,7 @@
except error_perm, e:
pass
assert ftp.nlst("test-renamed.txt") == ['test-renamed.txt']
+ ftp.close()
-
I create a new folder 'Test-Folder2' through FTP
-
@@ -173,6 +192,7 @@
from document_ftp import test_easyftp as te
ftp = te.get_ftp_folder(cr, uid, self, 'Documents')
ftp.mkd("Test-Folder2")
+ ftp.close()
-
I create a file 'test3.txt' at the 'Test-Folder2'
-
@@ -182,6 +202,7 @@
ftp = te.get_ftp_folder(cr, uid, self, 'Documents/Test-Folder2')
fdata = StringIO('abcd')
ftp.storbinary('STOR test3.txt', fdata)
+ ftp.close()
-
I try to retrieve test3.txt
-
@@ -189,6 +210,7 @@
from document_ftp import test_easyftp as te
ftp = te.get_ftp_folder(cr, uid, self, 'Documents/Test-Folder2')
assert ftp.nlst("test3.txt") == ['test3.txt'], "File test3.txt is not there!"
+ ftp.close()
-
I create a new folder, 'Test-Folder3', through FTP
I try to move test3.txt to 'Test-Folder3'
@@ -197,6 +219,7 @@
from document_ftp import test_easyftp as te
ftp = te.get_ftp_folder(cr, uid, self, 'Documents')
ftp.mkd("Test-Folder3")
+ ftp.close()
# TODO move
-
I remove the 'Test-Folder3'
@@ -205,6 +228,7 @@
from document_ftp import test_easyftp as te
ftp = te.get_ftp_folder(cr, uid, self, 'Documents')
ftp.rmd("Test-Folder3")
+ ftp.close()
-
I check that test3.txt is removed.
-
@@ -219,6 +243,7 @@
for i in range(0, 200):
fdata.seek(0)
ftp.storbinary('STOR test-name%s.txt' %i, fdata)
+ ftp.close()
-
I list the 200 files, check speed
-
@@ -245,6 +270,7 @@
ftp.delete('test3.txt')
for i in range(0, 200):
ftp.delete('test-name%s.txt' %i)
+ ftp.close()
-
I remove the 'Test-Folder2'
-
@@ -252,3 +278,4 @@
from document_ftp import test_easyftp as te
ftp = te.get_ftp_folder(cr, uid, self, 'Documents')
ftp.rmd("Test-Folder2")
+ ftp.close()
diff --git a/addons/document_ftp/test/document_ftp_test4.yml b/addons/document_ftp/test/document_ftp_test4.yml
index 2bb616dc1bd..2441be3bf92 100644
--- a/addons/document_ftp/test/document_ftp_test4.yml
+++ b/addons/document_ftp/test/document_ftp_test4.yml
@@ -113,6 +113,35 @@
- parent_id.name == 'Documents'
- res_model == 'res.partner'
- res_id != False
+-
+ I try to create a file directly under the Partners Testing folder
+-
+ !python {model: ir.attachment}: |
+ from document_ftp import test_easyftp as te
+ import ftplib
+ from cStringIO import StringIO
+ ftp = te.get_ftp_folder(cr, uid, self, 'Documents/Partners Testing')
+ fdata = StringIO('abcd')
+ try:
+ ftp.storbinary('STOR stray.txt', fdata)
+ assert False, "We should't be able to create files here"
+ except ftplib.error_perm:
+ # That's what should happen
+ pass
+-
+ I try to create a folder directly under the Partners Testing folder
+-
+ !python {model: ir.attachment}: |
+ from document_ftp import test_easyftp as te
+ import ftplib
+ from cStringIO import StringIO
+ ftp = te.get_ftp_folder(cr, uid, self, 'Documents/Partners Testing')
+ try:
+ ftp.mkd('Weird folder')
+ assert False, "We should't be able to create folders here"
+ except ftplib.error_perm:
+ # That's what should happen
+ pass
-
I check that all/Partner1 also has the file
- |
diff --git a/addons/document_ftp/test/document_ftp_test5.yml b/addons/document_ftp/test/document_ftp_test5.yml
new file mode 100644
index 00000000000..a66f409905b
--- /dev/null
+++ b/addons/document_ftp/test/document_ftp_test5.yml
@@ -0,0 +1,30 @@
+-
+ In order to check the permissions setup and functionality of the
+ document module:
+-
+ I create a testing user for the documents
+-
+ I assign some ... group to the testing user
+-
+ I create a "group testing" user, which also belongs to the same ... group
+-
+ I create a "blocked" user.
+-
+ I create (as root) a testing folder in the document hierarchy, and
+ assign ownership to the testing user, groups to the ... group.
+-
+ I create a "private" folder inside the testing folder.
+-
+ I try to read the testing folder as the testing user
+-
+ I try to read the folder as the group user, it should fail.
+-
+ I try to read the folder as the blocked user.
+-
+ I create a "group" folder, with the ... group.
+-
+ I try to read the "group" folder as the testing user
+-
+ I try to read the "group" folder as the group user
+-
+ I try to read the "group" folder as the blocked user
diff --git a/addons/document_webdav/__openerp__.py b/addons/document_webdav/__openerp__.py
index e6b8cfd3f94..6c1e24d1e96 100644
--- a/addons/document_webdav/__openerp__.py
+++ b/addons/document_webdav/__openerp__.py
@@ -57,6 +57,8 @@
'webdav_setup.xml',
],
"demo_xml" : [],
+ "test": [ #'test/webdav_test1.yml',
+ ],
"active": False,
"installable": True
}
diff --git a/addons/document_webdav/dav_fs.py b/addons/document_webdav/dav_fs.py
index 3e8e1c2b3e5..c34a323fadb 100644
--- a/addons/document_webdav/dav_fs.py
+++ b/addons/document_webdav/dav_fs.py
@@ -20,18 +20,16 @@
##############################################################################
import pooler
-import base64
-import sys
import os
import time
-from string import joinfields, split, lower
+import errno
import netsvc
import urlparse
-from DAV.constants import COLLECTION, OBJECT
-from DAV.errors import *
-from DAV.iface import *
+from DAV.constants import COLLECTION #, OBJECT
+from DAV.errors import DAV_Error, DAV_Forbidden, DAV_NotFound
+from DAV.iface import dav_interface
import urllib
from DAV.davcmd import copyone, copytree, moveone, movetree, delone, deltree
@@ -74,6 +72,89 @@ def _str2time(cre):
cre = cre[:fdot]
return time.mktime(time.strptime(cre,'%Y-%m-%d %H:%M:%S')) + frac
+class BoundStream2(object):
+ """Wraps around a seekable buffer, reads a determined range of data
+
+ Note that the supplied stream object MUST support a size() which
+ should return its data length (in bytes).
+
+ A variation of the class in websrv_lib.py
+ """
+
+ def __init__(self, stream, offset=None, length=None, chunk_size=None):
+ self._stream = stream
+ self._offset = offset or 0
+ self._length = length or self._stream.size()
+ self._rem_length = length
+ assert length and isinstance(length, (int, long))
+ assert length and length >= 0, length
+ self._chunk_size = chunk_size
+ if offset is not None:
+ self._stream.seek(offset)
+
+ def read(self, size=-1):
+ if not self._stream:
+ raise IOError(errno.EBADF, "read() without stream")
+
+ if self._rem_length == 0:
+ return ''
+ elif self._rem_length < 0:
+ raise EOFError()
+
+ rsize = self._rem_length
+ if size > 0 and size < rsize:
+ rsize = size
+ if self._chunk_size and self._chunk_size < rsize:
+ rsize = self._chunk_size
+
+ data = self._stream.read(rsize)
+ self._rem_length -= len(data)
+
+ return data
+
+ def __len__(self):
+ return self._length
+
+ def tell(self):
+ res = self._stream.tell()
+ if self._offset:
+ res -= self._offset
+ return res
+
+ def __iter__(self):
+ return self
+
+ def next(self):
+ return self.read(65536)
+
+ def seek(self, pos, whence=os.SEEK_SET):
+ """ Seek, computing our limited range
+ """
+ if whence == os.SEEK_SET:
+ if pos < 0 or pos > self._length:
+ raise IOError(errno.EINVAL,"Cannot seek")
+ self._stream.seek(pos - self._offset)
+ self._rem_length = self._length - pos
+ elif whence == os.SEEK_CUR:
+ if pos > 0:
+ if pos > self._rem_length:
+ raise IOError(errno.EINVAL,"Cannot seek past end")
+ elif pos < 0:
+ oldpos = self.tell()
+ if oldpos + pos < 0:
+ raise IOError(errno.EINVAL,"Cannot seek before start")
+ self._stream.seek(pos, os.SEEK_CUR)
+ self._rem_length -= pos
+ elif whence == os.SEEK_END:
+ if pos > 0:
+ raise IOError(errno.EINVAL,"Cannot seek past end")
+ else:
+ if self._length + pos < 0:
+ raise IOError(errno.EINVAL,"Cannot seek before start")
+ newpos = self._offset + self._length + pos
+ self._stream.seek(newpos, os.SEEK_SET)
+ self._rem_length = 0 - pos
+
class openerp_dav_handler(dav_interface):
"""
This class models a OpenERP interface for the DAV server
@@ -396,11 +477,28 @@ class openerp_dav_handler(dav_interface):
node = self.uri2object(cr, uid, pool, uri2)
if not node:
raise DAV_NotFound2(uri2)
+ # TODO: if node is a collection, for some specific set of
+ # clients ( web browsers; available in node context),
+ # we may return a pseydo-html page with the directory listing.
try:
+ res = node.open_data(cr,'r')
if rrange:
- self.parent.log_error("Doc get_data cannot use range")
- raise DAV_Error(409)
- datas = node.get_data(cr)
+ assert isinstance(rrange, (tuple,list))
+ start, end = map(long, rrange)
+ if not start:
+ start = 0
+ assert start >= 0
+ if end and end < start:
+ self.parent.log_error("Invalid range for data: %s-%s" %(start, end))
+ raise DAV_Error(416, "Invalid range for data")
+ if end:
+ if end >= res.size():
+ raise DAV_Error(416, "Requested data exceeds available size")
+ length = (end + 1) - start
+ else:
+ length = res.size() - start
+ res = BoundStream2(res, offset=start, length=length)
+
except TypeError,e:
# for the collections that return this error, the DAV standard
# says we'd better just return 200 OK with empty data
@@ -413,7 +511,7 @@ class openerp_dav_handler(dav_interface):
self.parent.log_error("GET exception: %s",str(e))
self.parent.log_message("Exc: %s", traceback.format_exc())
raise DAV_Error, 409
- return str(datas) # FIXME!
+ return res
finally:
if cr: cr.close()
@@ -584,8 +682,7 @@ class openerp_dav_handler(dav_interface):
node = False
objname = uri2[-1]
- ext = objname.find('.') >0 and objname.split('.')[1] or False
-
+
ret = None
if not node:
dir_node = self.uri2object(cr, uid, pool, uri2[:-1])
@@ -668,7 +765,7 @@ class openerp_dav_handler(dav_interface):
"""
if uri[-1]=='/':uri=uri[:-1]
res=delone(self,uri)
- parent='/'.join(uri.split('/')[:-1])
+ # parent='/'.join(uri.split('/')[:-1])
return res
def deltree(self, uri):
@@ -680,7 +777,7 @@ class openerp_dav_handler(dav_interface):
"""
if uri[-1]=='/':uri=uri[:-1]
res=deltree(self, uri)
- parent='/'.join(uri.split('/')[:-1])
+ # parent='/'.join(uri.split('/')[:-1])
return res
diff --git a/addons/document_webdav/document_webdav.py b/addons/document_webdav/document_webdav.py
index 28b70c8d7e4..2e1eb8cabd2 100644
--- a/addons/document_webdav/document_webdav.py
+++ b/addons/document_webdav/document_webdav.py
@@ -37,7 +37,6 @@ class document_davdir(osv.osv):
dbro = self.browse(cr, uid, ids, context=context)
if dynamic:
- assert dbro.type == 'directory'
return nodes.node_res_obj
elif dbro.type == 'directory':
return nodes.node_dir
diff --git a/addons/document_webdav/i18n/it.po b/addons/document_webdav/i18n/it.po
index 75c073edf1d..f384ee12f77 100644
--- a/addons/document_webdav/i18n/it.po
+++ b/addons/document_webdav/i18n/it.po
@@ -8,50 +8,52 @@ msgstr ""
"Project-Id-Version: openobject-addons\n"
"Report-Msgid-Bugs-To: FULL NAME \n"
"POT-Creation-Date: 2010-10-18 17:46+0000\n"
-"PO-Revision-Date: 2010-08-18 10:32+0000\n"
-"Last-Translator: OpenERP Administrators \n"
+"PO-Revision-Date: 2010-11-11 11:57+0000\n"
+"Last-Translator: Nicola Riolini - Micronaet \n"
"Language-Team: Italian \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"X-Launchpad-Export-Date: 2010-10-30 05:51+0000\n"
+"X-Launchpad-Export-Date: 2010-11-12 04:49+0000\n"
"X-Generator: Launchpad (build Unknown)\n"
#. module: document_webdav
#: model:ir.model,name:document_webdav.model_document_webdav_dir_property
msgid "document.webdav.dir.property"
-msgstr ""
+msgstr "document.webdav.dir.property"
#. module: document_webdav
#: constraint:ir.ui.view:0
msgid "Invalid XML for View Architecture!"
-msgstr ""
+msgstr "XML non valido per la struttura della vista!"
#. module: document_webdav
#: constraint:ir.model:0
msgid ""
"The Object name must start with x_ and not contain any special character !"
msgstr ""
+"Il nome dell'oggetto deve iniziare per “x_” e non contenere alcun carattere "
+"speciale!"
#. module: document_webdav
#: constraint:ir.actions.act_window:0
msgid "Invalid model name in the action definition."
-msgstr ""
+msgstr "Nome del modello non valido nella definizione dell'azione"
#. module: document_webdav
#: constraint:document.directory:0
msgid "Error! You can not create recursive Directories."
-msgstr ""
+msgstr "Errore! Non è possibile creare directory ricorsive."
#. module: document_webdav
#: view:document.webdav.dir.property:0
msgid "Group By..."
-msgstr ""
+msgstr "Raggruppa per..."
#. module: document_webdav
#: model:ir.module.module,shortdesc:document_webdav.module_meta_information
msgid "WebDAV server for Document Management"
-msgstr ""
+msgstr "server WebDAV per Gestione Documentale"
#. module: document_webdav
#: view:document.webdav.dir.property:0
@@ -62,17 +64,17 @@ msgstr ""
#. module: document_webdav
#: field:document.directory,dav_prop_ids:0
msgid "DAV properties"
-msgstr ""
+msgstr "Proprietà DAV"
#. module: document_webdav
#: view:document.directory:0
msgid "Dynamic context"
-msgstr ""
+msgstr "Contesto dinamico"
#. module: document_webdav
#: view:document.directory:0
msgid "These properties will be added to WebDAV requests"
-msgstr ""
+msgstr "Queste proprietà verranno aggiunte alle richieste WebDAV"
#. module: document_webdav
#: field:document.webdav.dir.property,do_subst:0
@@ -82,18 +84,18 @@ msgstr ""
#. module: document_webdav
#: constraint:ir.ui.menu:0
msgid "Error ! You can not create recursive Menu."
-msgstr ""
+msgstr "Errore! Non è possibile creare un menù ricorsivo."
#. module: document_webdav
#: view:document.directory:0
msgid "WebDAV properties"
-msgstr ""
+msgstr "Proprietà WebDAV"
#. module: document_webdav
#: field:document.webdav.dir.property,dir_id:0
#: model:ir.model,name:document_webdav.model_document_directory
msgid "Directory"
-msgstr ""
+msgstr "Directory"
#. module: document_webdav
#: field:document.webdav.dir.property,value:0
@@ -108,18 +110,18 @@ msgstr ""
#. module: document_webdav
#: model:ir.ui.menu,name:document_webdav.menu_dir_props
msgid "DAV properties for folders"
-msgstr ""
+msgstr "Proprietà DAV per cartelle"
#. module: document_webdav
#: view:document.directory:0
#: view:document.webdav.dir.property:0
msgid "Properties"
-msgstr ""
+msgstr "Proprietà"
#. module: document_webdav
#: view:document.webdav.dir.property:0
msgid "Dir"
-msgstr ""
+msgstr "Dir"
#. module: document_webdav
#: field:document.webdav.dir.property,name:0
diff --git a/addons/document_webdav/test/webdav_test1.yml b/addons/document_webdav/test/webdav_test1.yml
new file mode 100644
index 00000000000..a0f15c10fc9
--- /dev/null
+++ b/addons/document_webdav/test/webdav_test1.yml
@@ -0,0 +1,59 @@
+-
+ In order to test the document_ftp functionality
+-
+ I open the HTTP port and perform an OPTIONS request to the server
+-
+ !python {model: ir.attachment}: |
+ from document_webdav import test_davclient as te
+ reload(te) # reload..
+ dc = te.DAVClient()
+ dc.gd_options()
+ dc.get_creds(self, cr, uid)
+ dc.gd_options(path=cr.dbname, expect={'DAV': ['1',]})
+-
+ I will test the propnames at the document root
+-
+ !python {model: ir.attachment}: |
+ from document_webdav import test_davclient as te
+ dc = te.DAVClient()
+ dc.get_creds(self, cr, uid)
+ dc.gd_propname(path=cr.dbname+'/Documents/')
+-
+ I will test the ETags of the document root
+-
+ !python {model: ir.attachment}: |
+ from document_webdav import test_davclient as te
+ dc = te.DAVClient()
+ dc.get_creds(self, cr, uid)
+ dc.gd_getetag(path=cr.dbname+'/Documents/')
+
+-
+ I will now ls -l the document root.
+-
+ !python {model: ir.attachment}: |
+ from document_webdav import test_davclient as te
+ dc = te.DAVClient()
+ dc.get_creds(self, cr, uid)
+ res = dc.gd_lsl(path=cr.dbname+'/Documents/')
+ for lin in res:
+ print "%(type)s\t%(uid)s\t%(gid)s\t%(size)s\t%(mtime)s\t%(name)s" % lin
+-
+ I will put a file to the server
+-
+ !python {model: ir.attachment}: |
+ from document_webdav import test_davclient as te
+ import addons
+ dc = te.DAVClient()
+ dc.get_creds(self, cr, uid)
+ tdp = addons.get_module_resource('document_webdav', 'test_davclient.py')
+ res = dc.gd_put(path=cr.dbname+'/Documents/test_davclient.py', srcpath=tdp)
+-
+ I will try to get the file from the root
+-
+ !python {model: ir.attachment}: |
+ from document_webdav import test_davclient as te
+ import addons
+ dc = te.DAVClient()
+ dc.get_creds(self, cr, uid)
+ tdp = addons.get_module_resource('document_webdav', 'test_davclient.py')
+ res = dc.gd_get(path=cr.dbname+'/Documents/test_davclient.py', crange=(4,508), compare=tdp)
diff --git a/addons/document_webdav/test_davclient.py b/addons/document_webdav/test_davclient.py
new file mode 100644
index 00000000000..ac40d1a3d30
--- /dev/null
+++ b/addons/document_webdav/test_davclient.py
@@ -0,0 +1,702 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+#
+# Copyright P. Christeas 2008,2009
+# Copyright OpenERP SA. (http://www.openerp.com) 2010
+#
+#
+# WARNING: This program as such is intended to be used by professional
+# programmers who take the whole responsability of assessing all potential
+# consequences resulting from its eventual inadequacies and bugs
+# End users who are looking for a ready-to-use solution with commercial
+# garantees and support are strongly adviced to contract a Free Software
+# Service Company
+#
+# This program is Free Software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+###############################################################################
+
+""" A trivial HTTP/WebDAV client, used for testing the server
+"""
+# code taken from the 'http-client.py' script:
+# http://git.hellug.gr/?p=xrg/openerp;a=history;f=tests/http-client.py;hb=refs/heads/xrg-60
+
+import gzip
+import logging
+import xml.dom.minidom
+
+import httplib
+
+from tools import config
+from xmlrpclib import Transport, ProtocolError
+import StringIO
+import base64
+
+log = logging.getLogger('http-client')
+
+class HTTP11(httplib.HTTP):
+ _http_vsn = 11
+ _http_vsn_str = 'HTTP/1.1'
+
+class PersistentTransport(Transport):
+ """Handles an HTTP transaction to an XML-RPC server, persistently."""
+
+ def __init__(self, use_datetime=0):
+ self._use_datetime = use_datetime
+ self._http = {}
+ log.debug("Using persistent transport")
+
+ def make_connection(self, host):
+ # create a HTTP connection object from a host descriptor
+ if not self._http.has_key(host):
+ host, extra_headers, x509 = self.get_host_info(host)
+ self._http[host] = HTTP11(host)
+ log.debug("New connection to %s", host)
+ return self._http[host]
+
+ def get_host_info(self, host):
+ host, extra_headers, x509 = Transport.get_host_info(self,host)
+ if extra_headers == None:
+ extra_headers = []
+
+ extra_headers.append( ( 'Connection', 'keep-alive' ))
+
+ return host, extra_headers, x509
+
+ def _parse_response(self, file, sock, response):
+ """ read response from input file/socket, and parse it
+ We are persistent, so it is important to only parse
+ the right amount of input
+ """
+
+ p, u = self.getparser()
+
+ if response.msg.get('content-encoding') == 'gzip':
+ gzdata = StringIO.StringIO()
+ while not response.isclosed():
+ rdata = response.read(1024)
+ if not rdata:
+ break
+ gzdata.write(rdata)
+ gzdata.seek(0)
+ rbuffer = gzip.GzipFile(mode='rb', fileobj=gzdata)
+ while True:
+ respdata = rbuffer.read()
+ if not respdata:
+ break
+ p.feed(respdata)
+ else:
+ while not response.isclosed():
+ rdata = response.read(1024)
+ if not rdata:
+ break
+ p.feed(rdata)
+ if len(rdata)<1024:
+ break
+
+ p.close()
+ return u.close()
+
+ def request(self, host, handler, request_body, verbose=0):
+ # issue XML-RPC request
+
+ h = self.make_connection(host)
+ if verbose:
+ h.set_debuglevel(1)
+
+ self.send_request(h, handler, request_body)
+ self.send_host(h, host)
+ self.send_user_agent(h)
+ self.send_content(h, request_body)
+
+ resp = h._conn.getresponse()
+ # TODO: except BadStatusLine, e:
+
+ errcode, errmsg, headers = resp.status, resp.reason, resp.msg
+
+
+ if errcode != 200:
+ raise ProtocolError(
+ host + handler,
+ errcode, errmsg,
+ headers
+ )
+
+ self.verbose = verbose
+
+ try:
+ sock = h._conn.sock
+ except AttributeError:
+ sock = None
+
+ return self._parse_response(h.getfile(), sock, resp)
+
+class CompressedTransport(PersistentTransport):
+ def send_content(self, connection, request_body):
+ connection.putheader("Content-Type", "text/xml")
+
+ if len(request_body) > 512 or True:
+ buffer = StringIO.StringIO()
+ output = gzip.GzipFile(mode='wb', fileobj=buffer)
+ output.write(request_body)
+ output.close()
+ buffer.seek(0)
+ request_body = buffer.getvalue()
+ connection.putheader('Content-Encoding', 'gzip')
+
+ connection.putheader("Content-Length", str(len(request_body)))
+ connection.putheader("Accept-Encoding",'gzip')
+ connection.endheaders()
+ if request_body:
+ connection.send(request_body)
+
+ def send_request(self, connection, handler, request_body):
+ connection.putrequest("POST", handler, skip_accept_encoding=1)
+
+class SafePersistentTransport(PersistentTransport):
+ def make_connection(self, host):
+ # create a HTTP connection object from a host descriptor
+ if not self._http.has_key(host):
+ host, extra_headers, x509 = self.get_host_info(host)
+ self._http[host] = httplib.HTTPS(host, None, **(x509 or {}))
+ log.debug("New connection to %s", host)
+ return self._http[host]
+
+class AuthClient(object):
+ def getAuth(self, atype, realm):
+ raise NotImplementedError("Cannot authenticate for %s" % atype)
+
+ def resolveFailedRealm(self, realm):
+ """ Called when, using a known auth type, the realm is not in cache
+ """
+ raise NotImplementedError("Cannot authenticate for realm %s" % realm)
+
+class BasicAuthClient(AuthClient):
+ def __init__(self):
+ self._realm_dict = {}
+
+ def getAuth(self, atype, realm):
+ if atype != 'Basic' :
+ return super(BasicAuthClient,self).getAuth(atype, realm)
+
+ if not self._realm_dict.has_key(realm):
+ log.debug("realm dict: %r", self._realm_dict)
+ log.debug("missing key: \"%s\"" % realm)
+ self.resolveFailedRealm(realm)
+ return 'Basic '+ self._realm_dict[realm]
+
+ def addLogin(self, realm, username, passwd):
+ """ Add some known username/password for a specific login.
+ This function should be called once, for each realm
+ that we want to authenticate against
+ """
+ assert realm
+ auths = base64.encodestring(username + ':' + passwd)
+ if auths[-1] == "\n":
+ auths = auths[:-1]
+ self._realm_dict[realm] = auths
+
+class addAuthTransport:
+ """ Intermediate class that authentication algorithm to http transport
+ """
+
+ def setAuthClient(self, authobj):
+ """ Set the authentication client object.
+ This method must be called before any request is issued, that
+ would require http authentication
+ """
+ assert isinstance(authobj, AuthClient)
+ self._auth_client = authobj
+
+
+ def request(self, host, handler, request_body, verbose=0):
+ # issue XML-RPC request
+
+ h = self.make_connection(host)
+ if verbose:
+ h.set_debuglevel(1)
+
+ tries = 0
+ atype = None
+ realm = None
+
+ while(tries < 3):
+ self.send_request(h, handler, request_body)
+ self.send_host(h, host)
+ self.send_user_agent(h)
+ if atype:
+ # This line will bork if self.setAuthClient has not
+ # been issued. That is a programming error, fix your code!
+ auths = self._auth_client.getAuth(atype, realm)
+ log.debug("sending authorization: %s", auths)
+ h.putheader('Authorization', auths)
+ self.send_content(h, request_body)
+
+ resp = h._conn.getresponse()
+ # except BadStatusLine, e:
+ tries += 1
+
+ if resp.status == 401:
+ if 'www-authenticate' in resp.msg:
+ (atype,realm) = resp.msg.getheader('www-authenticate').split(' ',1)
+ data1 = resp.read()
+ if data1:
+ log.warning("Why have data on a 401 auth. message?")
+ if realm.startswith('realm="') and realm.endswith('"'):
+ realm = realm[7:-1]
+ log.debug("Resp: %r %r", resp.version,resp.isclosed(), resp.will_close)
+ log.debug("Want to do auth %s for realm %s", atype, realm)
+ if atype != 'Basic':
+ raise ProtocolError(host+handler, 403,
+ "Unknown authentication method: %s" % atype, resp.msg)
+ continue # with the outer while loop
+ else:
+ raise ProtocolError(host+handler, 403,
+ 'Server-incomplete authentication', resp.msg)
+
+ if resp.status != 200:
+ raise ProtocolError( host + handler,
+ resp.status, resp.reason, resp.msg )
+
+ self.verbose = verbose
+
+ try:
+ sock = h._conn.sock
+ except AttributeError:
+ sock = None
+
+ return self._parse_response(h.getfile(), sock, resp)
+
+ raise ProtocolError(host+handler, 403, "No authentication",'')
+
+class PersistentAuthTransport(addAuthTransport,PersistentTransport):
+ pass
+
+class PersistentAuthCTransport(addAuthTransport,CompressedTransport):
+ pass
+
+class HTTPSConnection(httplib.HTTPSConnection):
+ certs_file = None
+ def connect(self):
+ "Connect to a host on a given (SSL) port. check the certificate"
+ import socket, ssl
+
+ if HTTPSConnection.certs_file:
+ ca_certs = HTTPSConnection.certs_file
+ cert_reqs = ssl.CERT_REQUIRED
+ else:
+ ca_certs = None
+ cert_reqs = ssl.CERT_NONE
+ sock = socket.create_connection((self.host, self.port), self.timeout)
+ self.sock = ssl.wrap_socket(sock, self.key_file, self.cert_file,
+ ca_certs=ca_certs,
+ cert_reqs=cert_reqs)
+
+
+ def getpeercert(self):
+ import ssl
+ cert = None
+ if self.sock:
+ cert = self.sock.getpeercert()
+ else:
+ cert = ssl.get_server_certificate((self.host,self.port),
+ ssl_version=ssl.PROTOCOL_SSLv23 )
+ lf = (len(ssl.PEM_FOOTER)+1)
+ if cert[0-lf] != '\n':
+ cert = cert[:0-lf]+'\n'+cert[0-lf:]
+ log.debug("len-footer: %s cert: %r", lf, cert[0-lf])
+
+ return cert
+
+
+class DAVClient(object):
+ """An instance of a WebDAV client, connected to the OpenERP server
+ """
+
+ def __init__(self, user=None, passwd=None, dbg=0, use_ssl=False, useragent=False):
+ if use_ssl:
+ self.host = config.get_misc('httpsd', 'interface', False)
+ self.port = config.get_misc('httpsd', 'port', 8071)
+ if not self.host:
+ self.host = config.get('xmlrpcs_interface')
+ self.port = config.get('xmlrpcs_port')
+ else:
+ self.host = config.get_misc('httpd', 'interface')
+ self.port = config.get_misc('httpd', 'port', 8069)
+ if not self.host:
+ self.host = config.get('xmlrpc_interface')
+ self.port = config.get('xmlrpc_port') or self.port
+ if self.host == '0.0.0.0' or not self.host:
+ self.host = '127.0.0.1'
+ self.port = int(self.port)
+ if not config.get_misc('webdav','enable',True):
+ raise Exception("WebDAV is disabled, cannot continue")
+ self.davpath = '/' + config.get_misc('webdav','vdir','webdav')
+ self.user = user
+ self.passwd = passwd
+ self.dbg = dbg
+ self.hdrs = {}
+ if useragent:
+ self.set_useragent(useragent)
+
+ def get_creds(self, obj, cr, uid):
+ """Read back the user credentials from cr, uid
+
+ @param obj is any orm object, in order to use its pool
+ @param uid is the numeric id, which we will try to reverse resolve
+
+ note: this is a hackish way to get the credentials. It is expected
+ to break if "base_crypt" is used.
+ """
+ ruob = obj.pool.get('res.users')
+ res = ruob.read(cr, 1, [uid,], ['login', 'password'])
+ assert res, "uid %s not found" % uid
+ self.user = res[0]['login']
+ self.passwd = res[0]['password']
+ return True
+
+ def set_useragent(self, uastr):
+ """ Set the user-agent header to something meaningful.
+ Some shorthand names will be replaced by stock strings.
+ """
+ if uastr in ('KDE4', 'Korganizer'):
+ self.hdrs['User-Agent'] = "Mozilla/5.0 (compatible; Konqueror/4.4; Linux) KHTML/4.4.3 (like Gecko)"
+ elif uastr == 'iPhone3':
+ self.hdrs['User-Agent'] = "DAVKit/5.0 (765); iCalendar/5.0 (79); iPhone/4.1 8B117"
+ elif uastr == "MacOS":
+ self.hdrs['User-Agent'] = "WebDAVFS/1.8 (01808000) Darwin/9.8.0 (i386)"
+ else:
+ self.hdrs['User-Agent'] = uastr
+
+ def _http_request(self, path, method='GET', hdrs=None, body=None):
+ if not hdrs:
+ hdrs = {}
+ import base64
+ dbg = self.dbg
+ hdrs.update(self.hdrs)
+ log.debug("Getting %s http://%s:%d/%s", method, self.host, self.port, path)
+ conn = httplib.HTTPConnection(self.host, port=self.port)
+ conn.set_debuglevel(dbg)
+ if not path:
+ path = "/index.html"
+ if not hdrs.has_key('Connection'):
+ hdrs['Connection']= 'keep-alive'
+ conn.request(method, path, body, hdrs )
+ try:
+ r1 = conn.getresponse()
+ except httplib.BadStatusLine, bsl:
+ log.warning("Bad status line: %s", bsl.line)
+ raise Exception('Bad status line')
+ if r1.status == 401: # and r1.headers:
+ if 'www-authenticate' in r1.msg:
+ (atype,realm) = r1.msg.getheader('www-authenticate').split(' ',1)
+ data1 = r1.read()
+ if not self.user:
+ raise Exception('Must auth, have no user/pass!')
+ log.debug("Ver: %s, closed: %s, will close: %s", r1.version,r1.isclosed(), r1.will_close)
+ log.debug("Want to do auth %s for realm %s", atype, realm)
+ if atype == 'Basic' :
+ auths = base64.encodestring(self.user + ':' + self.passwd)
+ if auths[-1] == "\n":
+ auths = auths[:-1]
+ hdrs['Authorization']= 'Basic '+ auths
+ #sleep(1)
+ conn.request(method, path, body, hdrs )
+ r1 = conn.getresponse()
+ else:
+ raise Exception("Unknown auth type %s" %atype)
+ else:
+ log.warning("Got 401, cannot auth")
+ raise Exception('No auth')
+
+ log.debug("Reponse: %s %s",r1.status, r1.reason)
+ data1 = r1.read()
+ if method != 'GET':
+ log.debug("Body:\n%s\nEnd of body", data1)
+ try:
+ ctype = r1.msg.getheader('content-type')
+ if ctype and ';' in ctype:
+ ctype, encoding = ctype.split(';',1)
+ if ctype == 'text/xml':
+ doc = xml.dom.minidom.parseString(data1)
+ log.debug("XML Body:\n %s", doc.toprettyxml(indent="\t"))
+ except Exception:
+ log.warning("could not print xml", exc_info=True)
+ pass
+ conn.close()
+ return r1.status, r1.msg, data1
+
+ def _assert_headers(self, expect, msg):
+ """ Assert that the headers in msg contain the expect values
+ """
+ for k, v in expect.items():
+ hval = msg.getheader(k)
+ if not hval:
+ raise AssertionError("Header %s not defined in http response" % k)
+ if isinstance(v, (list, tuple)):
+ delim = ','
+ hits = map(str.strip, hval.split(delim))
+ mvits= []
+ for vit in v:
+ if vit not in hits:
+ mvits.append(vit)
+ if mvits:
+ raise AssertionError("HTTP header \"%s\" is missing: %s" %(k, ', '.join(mvits)))
+ else:
+ if hval.strip() != v.strip():
+ raise AssertionError("HTTP header \"%s: %s\"" % (k, hval))
+
+ def gd_options(self, path='*', expect=None):
+ """ Test the http options functionality
+ If a dictionary is defined in expect, those options are
+ asserted.
+ """
+ if path != '*':
+ path = self.davpath + path
+ hdrs = { 'Content-Length': 0
+ }
+ s, m, d = self._http_request(path, method='OPTIONS', hdrs=hdrs)
+ assert s == 200, "Status: %r" % s
+ assert 'OPTIONS' in m.getheader('Allow')
+ log.debug('Options: %r', m.getheader('Allow'))
+
+ if expect:
+ self._assert_headers(expect, m)
+
+ def _parse_prop_response(self, data):
+ """ Parse a propfind/propname response
+ """
+ def getText(node):
+ rc = []
+ for node in node.childNodes:
+ if node.nodeType == node.TEXT_NODE:
+ rc.append(node.data)
+ return ''.join(rc)
+
+ def getElements(node, namespaces=None, strict=False):
+ for cnod in node.childNodes:
+ if cnod.nodeType != node.ELEMENT_NODE:
+ if strict:
+ log.debug("Found %r inside <%s>", cnod, node.tagName)
+ continue
+ if namespaces and (cnod.namespaceURI not in namespaces):
+ log.debug("Ignoring <%s> in <%s>", cnod.tagName, node.localName)
+ continue
+ yield cnod
+
+ nod = xml.dom.minidom.parseString(data)
+ nod_r = nod.documentElement
+ res = {}
+ assert nod_r.localName == 'multistatus', nod_r.tagName
+ for resp in nod_r.getElementsByTagNameNS('DAV:', 'response'):
+ href = None
+ status = 200
+ res_nss = {}
+ for cno in getElements(resp, namespaces=['DAV:',]):
+ if cno.localName == 'href':
+ assert href is None, "Second href in same response"
+ href = getText(cno)
+ elif cno.localName == 'propstat':
+ for pno in getElements(cno, namespaces=['DAV:',]):
+ rstatus = None
+ if pno.localName == 'prop':
+ for prop in getElements(pno):
+ key = prop.localName
+ tval = getText(prop).strip()
+ val = tval or (True, rstatus or status)
+ if prop.namespaceURI == 'DAV:' and prop.localName == 'resourcetype':
+ val = 'plain'
+ for rte in getElements(prop, namespaces=['DAV:',]):
+ # Note: we only look at DAV:... elements, we
+ # actually expect only one DAV:collection child
+ val = rte.localName
+ res_nss.setdefault(prop.namespaceURI,{})[key] = val
+ elif pno.localName == 'status':
+ rstr = getText(pno)
+ htver, sta, msg = rstr.split(' ', 3)
+ assert htver == 'HTTP/1.1'
+ rstatus = int(sta)
+ else:
+ log.debug("What is <%s> inside a ?", pno.tagName)
+
+ else:
+ log.debug("Unknown node: %s", cno.tagName)
+
+ res.setdefault(href,[]).append((status, res_nss))
+
+ return res
+
+ def gd_propfind(self, path, props=None, depth=0):
+ if not props:
+ propstr = ''
+ else:
+ propstr = ''
+ nscount = 0
+ for p in props:
+ ns = None
+ if isinstance(p, tuple):
+ p, ns = p
+ if ns is None or ns == 'DAV:':
+ propstr += '<%s/>' % p
+ else:
+ propstr += '' %(nscount, p, nscount, ns)
+ nscount += 1
+ propstr += ''
+
+ body="""
+ %s""" % propstr
+ hdrs = { 'Content-Type': 'text/xml; charset=utf-8',
+ 'Accept': 'text/xml',
+ 'Depth': depth,
+ }
+
+ s, m, d = self._http_request(self.davpath + path, method='PROPFIND',
+ hdrs=hdrs, body=body)
+ assert s == 207, "Bad status: %s" % s
+ ctype = m.getheader('Content-Type').split(';',1)[0]
+ assert ctype == 'text/xml', m.getheader('Content-Type')
+ res = self._parse_prop_response(d)
+ if depth == 0:
+ assert len(res) == 1
+ res = res.values()[0]
+ else:
+ assert len(res) >= 1
+ return res
+
+
+ def gd_propname(self, path, depth=0):
+ body="""
+ """
+ hdrs = { 'Content-Type': 'text/xml; charset=utf-8',
+ 'Accept': 'text/xml',
+ 'Depth': depth
+ }
+ s, m, d = self._http_request(self.davpath + path, method='PROPFIND',
+ hdrs=hdrs, body=body)
+ assert s == 207, "Bad status: %s" % s
+ ctype = m.getheader('Content-Type').split(';',1)[0]
+ assert ctype == 'text/xml', m.getheader('Content-Type')
+ res = self._parse_prop_response(d)
+ if depth == 0:
+ assert len(res) == 1
+ res = res.values()[0]
+ else:
+ assert len(res) >= 1
+ return res
+
+ def gd_getetag(self, path, depth=0):
+ return self.gd_propfind(path, props=['getetag',], depth=depth)
+
+ def gd_lsl(self, path):
+ """ Return a list of 'ls -l' kind of data for a folder
+
+ This is based on propfind.
+ """
+
+ lspairs = [ ('name', 'displayname', 'n/a'), ('size', 'getcontentlength', '0'),
+ ('type', 'resourcetype', '----------'), ('uid', 'owner', 'nobody'),
+ ('gid', 'group', 'nogroup'), ('mtime', 'getlastmodified', 'n/a'),
+ ('mime', 'getcontenttype', 'application/data'), ]
+
+ propnames = [ l[1] for l in lspairs]
+ propres = self.gd_propfind(path, props=propnames, depth=1)
+
+ res = []
+ for href, pr in propres.items():
+ lsline = {}
+ for st, nsdic in pr:
+ davprops = nsdic['DAV:']
+ if st == 200:
+ for lsp in lspairs:
+ if lsp[1] in davprops:
+ if lsp[1] == 'resourcetype':
+ if davprops[lsp[1]] == 'collection':
+ lsline[lsp[0]] = 'dr-xr-x---'
+ else:
+ lsline[lsp[0]] = '-r-xr-x---'
+ else:
+ lsline[lsp[0]] = davprops[lsp[1]]
+ elif st in (404, 403):
+ for lsp in lspairs:
+ if lsp[1] in davprops:
+ lsline[lsp[0]] = lsp[2]
+ else:
+ log.debug("Strange status: %s", st)
+
+ res.append(lsline)
+
+ return res
+
+ def gd_get(self, path, crange=None, mime=None, compare=None):
+ """ HTTP GET for path, supporting Partial ranges
+ """
+ hdrs = { 'Accept': mime or '*/*', }
+ if crange:
+ if isinstance(crange, tuple):
+ crange = [crange,]
+ if not isinstance(crange, list):
+ raise TypeError("Range must be a tuple or list of tuples")
+ rs = []
+ for r in crange:
+ rs.append('%d-%d' % r)
+ hdrs['Range'] = 'bytes='+ (','.join(rs))
+ s, m, d = self._http_request(self.davpath + path, method='GET', hdrs=hdrs)
+ assert s in (200, 206), "Bad status: %s" % s
+ ctype = m.getheader('Content-Type')
+ if ctype and ';' in ctype:
+ ctype = ctype.split(';',1)[0]
+ if mime:
+ assert ctype == mime, m.getheader('Content-Type')
+ rrange = None
+ rrh = m.getheader('Content-Range')
+ if rrh:
+ assert rrh.startswith('bytes '), rrh
+ rrh=rrh[6:].split('/',1)[0]
+ rrange = map(int, rrh.split('-',1))
+ if compare:
+ # we need to compare the returned data with that of compare
+ fd = open(compare, 'rb')
+ d2 = fd.read()
+ fd.close()
+ if crange:
+ if len(crange) > 1:
+ raise NotImplementedError
+ r = crange[0]
+ d2 = d2[r[0]:r[1]+1]
+ assert d2 == d, "Data does not match"
+ return ctype, rrange, d
+
+ def gd_put(self, path, body=None, srcpath=None, mime=None, noclobber=False, ):
+ """ HTTP PUT
+ @param noclobber will prevent overwritting a resource (If-None-Match)
+ @param mime will set the content-type
+ """
+ hdrs = { }
+ if not (body or srcpath):
+ raise ValueError("PUT must have something to send")
+ if (not body) and srcpath:
+ fd = open(srcpath, 'rb')
+ body = fd.read()
+ fd.close()
+ if mime:
+ hdrs['Content-Type'] = mime
+ if noclobber:
+ hdrs['If-None-Match'] = '*'
+ s, m, d = self._http_request(self.davpath + path, method='PUT',
+ hdrs=hdrs, body=body)
+ assert s == (201), "Bad status: %s" % s
+ etag = m.getheader('ETag')
+ return etag or True
+
+#eof
\ No newline at end of file
diff --git a/addons/document_webdav/webdav_server.py b/addons/document_webdav/webdav_server.py
index df2c4a871f6..0e5973c95b3 100644
--- a/addons/document_webdav/webdav_server.py
+++ b/addons/document_webdav/webdav_server.py
@@ -28,7 +28,6 @@
import netsvc
-import tools
from dav_fs import openerp_dav_handler
from tools.config import config
from DAV.WebDAVServer import DAVRequestHandler
@@ -36,10 +35,13 @@ from service.websrv_lib import HTTPDir, FixSendError, HttpOptions
from BaseHTTPServer import BaseHTTPRequestHandler
import urlparse
import urllib
-from string import atoi,split
+import re
+from string import atoi
from DAV.errors import *
# from DAV.constants import DAV_VERSION_1, DAV_VERSION_2
+khtml_re = re.compile(r' KHTML/([0-9\.]+) ')
+
def OpenDAVConfig(**kw):
class OpenDAV:
def __init__(self, **kw):
@@ -96,7 +98,6 @@ class DAVHandler(HttpOptions, FixSendError, DAVRequestHandler):
""" Our uri scheme removes the /webdav/ component from there, so we
need to mangle the header, too.
"""
- dest = self.headers['Destination']
up = urlparse.urlparse(urllib.unquote(self.headers['Destination']))
if up.path.startswith(self.davpath):
self.headers['Destination'] = up.path[len(self.davpath):]
@@ -131,6 +132,16 @@ class DAVHandler(HttpOptions, FixSendError, DAVRequestHandler):
# the BufferingHttpServer will send Connection: close , while
# the BaseHTTPRequestHandler will only accept int code.
# workaround both of them.
+ if self.command == 'PROPFIND' and int(code) == 404:
+ kh = khtml_re.search(self.headers.get('User-Agent',''))
+ if kh and (kh.group(1) < '4.5'):
+ # There is an ugly bug in all khtml < 4.5.x, where the 404
+ # response is treated as an immediate error, which would even
+ # break the flow of a subsequent PUT request. At the same time,
+ # the 200 response (rather than 207 with content) is treated
+ # as "path not exist", so we send this instead
+ # https://bugs.kde.org/show_bug.cgi?id=166081
+ code = 200
BaseHTTPRequestHandler.send_response(self, int(code), message)
def send_header(self, key, value):
@@ -216,7 +227,7 @@ class DAVHandler(HttpOptions, FixSendError, DAVRequestHandler):
try:
location = dc.put(uri, body, ct)
except DAV_Error, (ec,dd):
- self._logger.warning("Cannot PUT to %s: %s", uri, dd, exc_info=True)
+ self.log_error("Cannot PUT to %s: %s", uri, dd)
return self.send_status(ec)
headers = {}
diff --git a/addons/document_webdav/webdav_setup.xml b/addons/document_webdav/webdav_setup.xml
index 2bd41ddb69c..6ea894b6a22 100644
--- a/addons/document_webdav/webdav_setup.xml
+++ b/addons/document_webdav/webdav_setup.xml
@@ -12,7 +12,7 @@
[]
-
+ directory
@@ -24,7 +24,7 @@
[]
-
+
@@ -45,9 +45,10 @@
resources
- []
+ [('id','=',uid)]
+
@@ -58,10 +59,10 @@
__uids__
- []
+ [('id','=',uid)]
-
+
diff --git a/addons/email_template/email_template_mailbox_view.xml b/addons/email_template/email_template_mailbox_view.xml
index 38757ba7d68..8f4d1fc8d60 100644
--- a/addons/email_template/email_template_mailbox_view.xml
+++ b/addons/email_template/email_template_mailbox_view.xml
@@ -106,6 +106,7 @@
tree,form{'group_by': [], 'search_default_outbox': 1}
+ An email template is an email document that will be sent as part of a marketing campaign. You can personalize it based on specific customer profile fields so that their names or other partner related information can be automatically inserted in it.
diff --git a/addons/fetchmail/i18n/fr.po b/addons/fetchmail/i18n/fr.po
index b9f0a3ecd3a..088205d9054 100644
--- a/addons/fetchmail/i18n/fr.po
+++ b/addons/fetchmail/i18n/fr.po
@@ -7,13 +7,13 @@ msgstr ""
"Project-Id-Version: OpenERP Server 6.0dev\n"
"Report-Msgid-Bugs-To: support@openerp.com\n"
"POT-Creation-Date: 2010-10-18 17:46+0000\n"
-"PO-Revision-Date: 2010-11-10 07:36+0000\n"
+"PO-Revision-Date: 2010-11-11 09:34+0000\n"
"Last-Translator: Open Net Sàrl \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"X-Launchpad-Export-Date: 2010-11-11 04:46+0000\n"
+"X-Launchpad-Export-Date: 2010-11-12 04:49+0000\n"
"X-Generator: Launchpad (build Unknown)\n"
#. module: fetchmail
diff --git a/addons/google_map/i18n/el.po b/addons/google_map/i18n/el.po
index 1770ef53323..ed83753ca5b 100644
--- a/addons/google_map/i18n/el.po
+++ b/addons/google_map/i18n/el.po
@@ -8,13 +8,13 @@ msgstr ""
"Project-Id-Version: openobject-addons\n"
"Report-Msgid-Bugs-To: FULL NAME \n"
"POT-Creation-Date: 2010-10-18 17:46+0000\n"
-"PO-Revision-Date: 2010-08-03 06:15+0000\n"
-"Last-Translator: mga (Open ERP) \n"
+"PO-Revision-Date: 2010-11-11 14:10+0000\n"
+"Last-Translator: Dimitris Andavoglou \n"
"Language-Team: Greek \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"X-Launchpad-Export-Date: 2010-10-30 05:07+0000\n"
+"X-Launchpad-Export-Date: 2010-11-12 04:45+0000\n"
"X-Generator: Launchpad (build Unknown)\n"
#. module: google_map
@@ -33,6 +33,8 @@ msgstr "Άκυρο XML για την Αρχιτεκτονική Όψης!"
msgid ""
"The Object name must start with x_ and not contain any special character !"
msgstr ""
+"Το αντικείμενο θα πρέπει να ξεκινά με x_ και ν μην περιέχει ειδικούς "
+"χαρακτήρες!"
#. module: google_map
#: view:res.partner:0
@@ -59,4 +61,4 @@ msgstr ""
#. module: google_map
#: model:ir.model,name:google_map.model_res_partner_address
msgid "Partner Addresses"
-msgstr ""
+msgstr "Διεύθυνση Συνεργάτη"
diff --git a/addons/google_map/i18n/it.po b/addons/google_map/i18n/it.po
index b7c1bc0ef98..29289c994f2 100644
--- a/addons/google_map/i18n/it.po
+++ b/addons/google_map/i18n/it.po
@@ -7,13 +7,13 @@ msgstr ""
"Project-Id-Version: OpenERP Server 6.0dev\n"
"Report-Msgid-Bugs-To: support@openerp.com\n"
"POT-Creation-Date: 2010-10-18 17:46+0000\n"
-"PO-Revision-Date: 2010-11-10 20:14+0000\n"
-"Last-Translator: simone.sandri \n"
+"PO-Revision-Date: 2010-11-11 09:15+0000\n"
+"Last-Translator: OpenERP Administrators \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"X-Launchpad-Export-Date: 2010-11-11 04:43+0000\n"
+"X-Launchpad-Export-Date: 2010-11-12 04:45+0000\n"
"X-Generator: Launchpad (build Unknown)\n"
#. module: google_map
diff --git a/addons/hr/hr_view.xml b/addons/hr/hr_view.xml
index e4bdad2c3d1..ce0a7193683 100644
--- a/addons/hr/hr_view.xml
+++ b/addons/hr/hr_view.xml
@@ -150,7 +150,7 @@
{"search_default_department_id": department_id,"search_default_active":eval('True')}
- The employee directory contains all data related to your employees : from their photo up to their hourly estimated costs for the timesheets.Employees are managed by departments and can be linked to users to manage their access rights.
+ Here you can manage your work force by creating employees and assigning them specific properties in the system. Maintain all employee related information and keep track of anything that needs to be recorded for them. The personal information tab will help you maintain their identity data. The Categories tab gives you the opportunity to assign them related employee categories depending on their position and activities within the company. A category can be a seniority level within the company or a department. The Timesheets tab allows to assign them a specific timesheet and analytic journal where they will be able to enter time through the system. In the note tab, you can enter text data that should be recorded for a specific employee.
@@ -401,7 +401,7 @@
tree,form{"search_default_Current":1,"search_default_department_id": department_id}
- Job Positions are used to define the jobs, the requirements. You can attach a survey to a job position. This survey will be used in the recruitement process to evaluate the applicants for this job position.
+ JJob Positions is used to define jobs and their requirements. You can attach a survey to a job position. This survey will be used in the recruitment process to evaluate the applicants for this job position.
diff --git a/addons/hr_attendance/wizard/hr_attendance_sign_in_out_view.xml b/addons/hr_attendance/wizard/hr_attendance_sign_in_out_view.xml
index dbabd9463f1..5121a2d18a0 100644
--- a/addons/hr_attendance/wizard/hr_attendance_sign_in_out_view.xml
+++ b/addons/hr_attendance/wizard/hr_attendance_sign_in_out_view.xml
@@ -7,8 +7,10 @@
form
+
+
+
-
diff --git a/addons/hr_timesheet/wizard/hr_timesheet_sign_in_out_view.xml b/addons/hr_timesheet/wizard/hr_timesheet_sign_in_out_view.xml
index 99d9230a188..e9114e1fe3c 100644
--- a/addons/hr_timesheet/wizard/hr_timesheet_sign_in_out_view.xml
+++ b/addons/hr_timesheet/wizard/hr_timesheet_sign_in_out_view.xml
@@ -7,9 +7,9 @@
form
-
-
-
+
+
+
diff --git a/addons/hr_timesheet_invoice/hr_timesheet_invoice_view.xml b/addons/hr_timesheet_invoice/hr_timesheet_invoice_view.xml
index 408835fe993..87e8b8b0feb 100644
--- a/addons/hr_timesheet_invoice/hr_timesheet_invoice_view.xml
+++ b/addons/hr_timesheet_invoice/hr_timesheet_invoice_view.xml
@@ -169,6 +169,7 @@
form
+ OpenERP allows you to create default invoicing types. You could need to regularly make discounts based on a specific contract or agreement with a customer. From this menu, you can create additional default types of invoicing that will speed up your invoicing.