Browse Source

merge with master

Guénaël Muller 6 years ago
parent
commit
5f52ff917b

+ 29 - 3
README.md View File

102
 
102
 
103
 In case you prefer using Docker:
103
 In case you prefer using Docker:
104
 
104
 
105
-    sudo docker run -e DATABASE_TYPE=sqlite \
106
-               -p 80:80 -p 3030:3030 -p 5232:5232 \
107
-               -v /var/tracim/etc:/etc/tracim -v /var/tracim/var:/var/tracim algoo/tracim
105
+You need to install docker on your server first (see [here for debian](https://docs.docker.com/install/linux/docker-ce/debian/) or [here for windows]([docker toolbox](https://docs.docker.com/toolbox/toolbox_install_windows/)))
106
+
107
+Make sure `docker.service` is started correctly.
108
+
109
+    systemctl status docker.service
110
+
111
+Then use image like following (adapt volume paths or ports as you want):
112
+
113
+    sudo docker run \
114
+        -e DATABASE_TYPE=sqlite \
115
+        -p 80:80 \
116
+        -p 3030:3030 \
117
+        -p 5232:5232 \
118
+        -v /var/tracim/etc:/etc/tracim \
119
+        -v /var/tracim/var:/var/tracim \
120
+        algoo/tracim  # docker image: tracim:unstable for development version, tracim_test:latest for nightly builds
121
+
122
+
123
+All docker images are available here : https://hub.docker.com/u/algoo/
124
+More information about build docker image: https://github.com/tracim/docker_tracim
125
+
126
+Description of versioning:
127
+
128
+* algoo/`tracim:latest` is latest stable image from branch master
129
+* algoo/`tracim:<$tag>` is a stable tag image from branch master
130
+* algoo/`tracim_testing:latest` is a latest image from branch develop
131
+* algoo/`tracim:unstable` is a latest unstable image from branch develop
132
+
133
+----
108
 
134
 
109
 ## Install Tracim on your server ##
135
 ## Install Tracim on your server ##
110
 
136
 

+ 125 - 30
doc/apache.md View File

2
 
2
 
3
 ### Installation ###
3
 ### Installation ###
4
 
4
 
5
-Install `Apache` server and its [`WSGI` module](https://github.com/GrahamDumpleton/mod_wsgi):
5
+Install `tracim` first.
6
+Install `Apache` server and uwsgi  its [`WSGI` module](https://github.com/GrahamDumpleton/mod_wsgi):
6
 
7
 
7
-    sudo apt install apache2 libapache2-mod-wsgi-py3
8
+    sudo apt install apache2 libapache2-mod-wsgi-py3 uwsgi uwsgi-plugin-python3
8
 
9
 
9
 ### Configuration ###
10
 ### Configuration ###
10
 
11
 
11
 Create a file named `/etc/apache2/sites-available/tracim.conf` containing:
12
 Create a file named `/etc/apache2/sites-available/tracim.conf` containing:
12
 
13
 
13
-    Listen 8080
14
+    Listen 80
14
 
15
 
15
-    <VirtualHost *:8080>
16
+    <VirtualHost *:80>
16
         ServerName tracim
17
         ServerName tracim
17
 
18
 
18
-        # Serve Tracim through WSGI
19
-        WSGIDaemonProcess tracim user=[your_user] group=[your_user] threads=4 python-home=[tracim_path]/tg2env python-path=[tracim_path]/tracim lang='C.UTF-8' locale='C.UTF-8'
20
-        WSGIProcessGroup tracim
21
-        WSGIScriptAlias / [tracim_path]/tracim/app.wsgi process-group=tracim
22
-        <Directory "[tracim_path]/tracim">
23
-            <Files "app.wsgi">
24
-                Require all granted
25
-            </Files>
26
-        </Directory>
27
-
28
-        # Serve static files directly
29
-        Alias /assets          [tracim_path]/tracim/tracim/public/assets
30
-        Alias /_caldavzap      [tracim_path]/tracim/tracim/public/_caldavzap
31
-        Alias /favicon.ico     [tracim_path]/tracim/tracim/public/favicon.ico
32
-        <Directory "[tracim_path]/tracim/tracim/public">
33
-            Require all granted
34
-        </Directory>
19
+    <Directory "/">
20
+        Require all granted
21
+        Dav On
22
+    </Directory>
23
+    ProxyPreserveHost On 
24
+    CustomLog /var/log/apache2/algoo-access.log combined
25
+    ErrorLog /var/log/apache2/algoo-error.log
26
+    <Location "/webdav">
27
+        # Dav On
28
+    </Location>
29
+
30
+    # RemoteIPHeader X-Forwarded-For
31
+    ProxyPass /webdav http://127.0.0.1:3030/webdav
32
+    ProxyPassReverse /webdav http://127.0.0.1:3030/webdav
33
+    ProxyPass / http://127.0.0.1:8080/
34
+    ProxyPassReverse / http://127.0.0.1:8080/
35
+
35
     </VirtualHost>
36
     </VirtualHost>
36
 
37
 
37
-Replace `[tracim_path]` and `[your_user]` above by your tracim installation path and your user.
38
+Enable this configuration file:
39
+
40
+    sudo ln -s /etc/apache2/sites-available/tracim.conf /etc/apache2/sites-enabled/
41
+
42
+Check if you just have `tracim.conf` in `/etc/apache2/sites-enabled`, if not, remove other file.
43
+
44
+
45
+Create the file named `/etc/uwsgi/apps-available/tracim.ini` containing:
46
+
47
+    [uwsgi]
48
+    plugins = python3
49
+    chdir = [tracim_path]/tracim
50
+    home = [tracim_path]/tg2env
51
+    wsgi-file = app.wsgi
52
+    callable = application
53
+    http-socket = 0.0.0.0:8080
54
+    enable-threads = true
55
+    env = PYTHON_EGG_CACHE=/tmp
56
+
57
+Replace [tracim_path] by your path of tracim
38
 
58
 
39
-Set the `APP_CONFIG` variable of the `tracim/app.wsgi` file to match your tracim installation path:
59
+Enable this configuration file:
60
+
61
+    sudo ln -s /etc/uwsgi/apps-available/tracim.ini /etc/uwsgi/apps-enabled/
62
+
63
+
64
+Create file `tracim.log` in `/var/log/uwsgi/app/`
65
+
66
+Set the `APP_CONFIG` variable of the `[tracim_path]/tracim/app.wsgi` file to match your tracim installation path:
40
 
67
 
41
     # -*- coding: utf-8 -*-
68
     # -*- coding: utf-8 -*-
42
 
69
 
43
-    APP_CONFIG = "[tracim_path]/tracim/development.ini"
70
+    APP_CONFIG = "[tracim_path]/tracim/development.ini" 
71
+    #(in file: replace /var/www/tracim by your [tracim_path] )
44
 
72
 
45
     #Setup logging
73
     #Setup logging
46
     # import logging
74
     # import logging
51
     application = loadapp('config:%s' % APP_CONFIG)
79
     application = loadapp('config:%s' % APP_CONFIG)
52
     application.debug = False
80
     application.debug = False
53
 
81
 
82
+
83
+Add `webdav` at `root_path` in the `[tracim_path]/tracim/wsgidav.conf`:
84
+
85
+    ################################################################################
86
+    # Sample WsgiDAV configuration file
87
+    #
88
+    # 1. Rename this file to `wsgidav.conf`
89
+    # 2. Adjust settings as appropriate
90
+    # 3. Run tracim as you always do :)
91
+    #
92
+    ################################################################################
93
+    
94
+    ################################################################################
95
+    # SERVER OPTIONS
96
+    #===============================================================================
97
+    
98
+    # host  = "localhost"
99
+    # host  = "192.168.0.1"
100
+    host  = "0.0.0.0"
101
+    
102
+    port = 3030
103
+    
104
+    show_history = True
105
+    show_deleted = True
106
+    show_archived = True
107
+    
108
+    manager_locks = True
109
+    
110
+    root_path = ''
111
+    
112
+    # Tracim doesn't support digest auth for webdav
113
+    acceptbasic = True
114
+    acceptdigest = False
115
+    defaultdigest = False
116
+    #===============================================================================
117
+    # Lock Manager
118
+    #
119
+    # Example: Use PERSISTENT shelve based lock manager
120
+    #from wsgidav.lock_storage import LockStorageShelve
121
+    #locksmanager = LockStorageShelve("wsgidav-locks.shelve")
122
+
123
+
124
+
125
+Open `[tracim_path]/tracim/development.ini` and make some change:
126
+
127
+
128
+    In [server:main] modify IP:
129
+    `host = 127.0.0.1` by `host = 0.0.0.0`
130
+
131
+    For Radical (CalDav server):
132
+    Uncomment `# radicale.server.host = 0.0.0.0`
133
+    Uncomment `# radicale.server.allow_origin = *`
134
+    Uncomment `# radicale.client.base_url.host = http://127.0.0.1:5232`
135
+    and modifiy IP `# radicale.client.base_url.host = http://127.0.0.1:5232`to `radicale.client.base_url.host = http://[Your_server_IP]:5232`
136
+    
137
+    For WSGIDAV
138
+    Uncomment `# wsgidav.client.base_url = 127.0.0.1:<WSGIDAV_PORT>`
139
+    and modify IP and PORT `# wsgidav.client.base_url = 127.0.0.1:<WSGIDAV_PORT>` to `wsgidav.client.base_url = [Your_server_IP]/webdav`
140
+
141
+
142
+Restart `uwsgi` configuration:
143
+
144
+    sudo systemctl restart uwsgi.service
145
+
54
 Load needed proxy modules and enable this site configuration file:
146
 Load needed proxy modules and enable this site configuration file:
55
 
147
 
56
-    sudo a2enmod proxy proxy_http
148
+    sudo a2enmod dav_fs dav proxy proxy_http
57
     sudo a2ensite tracim.conf
149
     sudo a2ensite tracim.conf
58
 
150
 
59
-Reload `Apache` configuration:
151
+Restart `Apache` configuration:
60
 
152
 
61
-    sudo systemctl reload apache2.service
153
+    sudo systemctl restart apache2.service
154
+    
155
+**Important**
156
+In case you have some permission problem, check if `www-data` can access to folder of tracim.
62
 
157
 
63
 ## Documentation Links ##
158
 ## Documentation Links ##
64
 
159
 
65
-[TurboGears](http://turbogears.readthedocs.io/en/tg2.3.7/cookbook/deploy/mod_wsgi.html)
66
-
67
-[mod_wsgi](http://modwsgi.readthedocs.io/en/develop/index.html)
160
+* [Apache](https://httpd.apache.org/docs/2.4/fr/)
161
+* [TurboGears](http://turbogears.readthedocs.io/en/tg2.3.7/cookbook/deploy/mod_wsgi.html)
162
+* [mod_wsgi](http://modwsgi.readthedocs.io/en/develop/index.html)

+ 31 - 0
doc/devtools.md View File

1
+# Devtools
2
+
3
+# Check third party licences
4
+
5
+Install `yolk3k` pip package:
6
+
7
+    pip install yolk3k
8
+
9
+Then execute command:
10
+
11
+    yolk -l -f license
12
+
13
+Output will look like:
14
+
15
+```
16
+Babel (2.2.0)
17
+    License: BSD
18
+
19
+Beaker (1.6.4)
20
+    License: BSD
21
+
22
+CherryPy (3.6.0)
23
+    License: BSD
24
+
25
+FormEncode (1.3.0a1)
26
+    License: PSF
27
+
28
+Genshi (0.7)
29
+    License: BSD
30
+...
31
+```

+ 70 - 0
doc/webdav.md View File

1
+# How to use webdav from different OS ?
2
+
3
+## Windows
4
+
5
+### Windows 7
6
+
7
+- Open Start Menu.
8
+- Click on Computer.
9
+- click on "Map network drive".
10
+
11
+### Windows 8 and 10
12
+
13
+- Open File explorer.
14
+- Right click on "This PC" (left panel)
15
+- From the dropdown menu, select "Map network drive".
16
+
17
+### Map Network drive Windows:
18
+
19
+Webdav Windows addresses are similar to:
20
+
21
+```
22
+https://<yourinstance>/webdav/ (secure)
23
+http://<yourinstance>/webdav/ (unsecure)
24
+```
25
+
26
+- Enter the address of webdav (you can find it in each workspace, under workspace details)
27
+- Check "Reconnect at sign-in" and "Connect using different credentials".
28
+- Click "Finish".
29
+- Your login/password will be ask. Use your Tracim credentials.
30
+- After that, your webdav access should be mounted.
31
+
32
+### Unsecure HTTP using Windows
33
+
34
+If you want to use webdav with tracim without https, you need to set Windows to accept basic auth in http.
35
+
36
+To enable it:
37
+- Launch regedit.
38
+- Go to "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\WebClient\Parameters\BasicAuthLevel".
39
+- set "BasicAuthLevel" to "2".
40
+
41
+## OSX
42
+
43
+Webdav OSX addresses are similar to:
44
+
45
+```
46
+https://<yourinstance>/webdav/ (secure)
47
+http://<yourinstance>/webdav/ (unsecure)
48
+```
49
+
50
+- In the Finder, choose "Go > Connect to Server".
51
+- Enter the address of webdav (you can find it in each workspace, under workspace details). Click Connect.
52
+- Your login/password will be ask. Use your Tracim credentials.
53
+- After that, your webdav access should be mounted.
54
+
55
+## Linux
56
+
57
+Webdav Linux addresses are similar to:
58
+
59
+```
60
+davs://<yourinstance>/webdav/ (secure)
61
+dav://<yourinstance>/webdav/ (unsecure)
62
+```
63
+
64
+### Gnome3 (nautilus)
65
+
66
+- Launch nautilus.
67
+- Show url bar : Ctrl+l.
68
+- Enter the address of webdav (you can find it in each workspace, under workspace details). Press Enter.
69
+- Your login/password will be ask. Use your Tracim credentials.
70
+- After that, your webdav access should be mounted.

+ 26 - 0
tracim/migration/versions/2b4043fa2502_remove_webdav_right_digest_response_.py View File

1
+"""remove webdav_right_digest_response_hash from database
2
+
3
+Revision ID: 2b4043fa2502
4
+Revises: f3852e1349c4
5
+Create Date: 2018-03-13 14:41:38.590375
6
+
7
+"""
8
+
9
+# revision identifiers, used by Alembic.
10
+revision = '2b4043fa2502'
11
+down_revision = 'f3852e1349c4'
12
+
13
+from alembic import op
14
+from sqlalchemy import Column, Unicode
15
+
16
+
17
+def upgrade():
18
+    with op.batch_alter_table('users') as batch_op:
19
+        batch_op.drop_column('webdav_left_digest_response_hash')
20
+
21
+
22
+def downgrade():
23
+    with op.batch_alter_table('users') as batch_op:
24
+        batch_op.add_column(
25
+            Column('webdav_left_digest_response_hash', Unicode(128))
26
+        )

+ 8 - 0
tracim/tracim/controllers/workspace.py View File

23
 
23
 
24
 from tracim.model.serializers import Context, CTX, DictLikeClass
24
 from tracim.model.serializers import Context, CTX, DictLikeClass
25
 
25
 
26
+from urllib.parse import urlparse
26
 
27
 
27
 class UserWorkspaceRestController(TIMRestController):
28
 class UserWorkspaceRestController(TIMRestController):
28
 
29
 
103
         videoconf_enabled = CFG.get_instance().JITSI_MEET_ACTIVATED
104
         videoconf_enabled = CFG.get_instance().JITSI_MEET_ACTIVATED
104
 
105
 
105
         dictified_workspace = Context(CTX.WORKSPACE).toDict(workspace, 'workspace')
106
         dictified_workspace = Context(CTX.WORKSPACE).toDict(workspace, 'workspace')
107
+
106
         webdav_url = CFG.get_instance().WSGIDAV_CLIENT_BASE_URL
108
         webdav_url = CFG.get_instance().WSGIDAV_CLIENT_BASE_URL
109
+        website_protocol = urlparse(CFG.get_instance().WEBSITE_BASE_URL).scheme
110
+        dav_protocol = 'dav'
111
+        if website_protocol == "https":
112
+            dav_protocol = 'davs'
107
 
113
 
108
         return DictLikeClass(
114
         return DictLikeClass(
109
             result=dictified_workspace,
115
             result=dictified_workspace,
110
             fake_api=fake_api,
116
             fake_api=fake_api,
111
             webdav_url=webdav_url,
117
             webdav_url=webdav_url,
112
             videoconf_enabled=videoconf_enabled,
118
             videoconf_enabled=videoconf_enabled,
119
+            website_protocol = website_protocol,
120
+            dav_protocol = dav_protocol,
113
             show_deleted=show_deleted,
121
             show_deleted=show_deleted,
114
             show_archived=show_archived,
122
             show_archived=show_archived,
115
         )
123
         )

+ 2 - 2
tracim/tracim/i18n/fr/LC_MESSAGES/tracim.po View File

1403
 
1403
 
1404
 #: tracim/templates/admin/workspace_getall.mak:48
1404
 #: tracim/templates/admin/workspace_getall.mak:48
1405
 msgid "<u>Note</u>: members will be added during next step."
1405
 msgid "<u>Note</u>: members will be added during next step."
1406
-msgstr "<u>Note</u> : les membres seront ajoutées à la prochaine étape."
1406
+msgstr "<u>Note</u> : les membres seront ajouté(e)s à la prochaine étape."
1407
 
1407
 
1408
 #: tracim/templates/admin/workspace_getall.mak:75
1408
 #: tracim/templates/admin/workspace_getall.mak:75
1409
 msgid "User Nb"
1409
 msgid "User Nb"
1433
 "This workspace offers a calendar that you can configure in your software:"
1433
 "This workspace offers a calendar that you can configure in your software:"
1434
 " Outlook, Thunderbird, etc."
1434
 " Outlook, Thunderbird, etc."
1435
 msgstr ""
1435
 msgstr ""
1436
-"Cet espace de travaille propose un calendrier que vous pouvez configurer "
1436
+"Cet espace de travail propose un calendrier que vous pouvez configurer "
1437
 "dans votre logiciel : Outlook, Thunderbird, etc."
1437
 "dans votre logiciel : Outlook, Thunderbird, etc."
1438
 
1438
 
1439
 #: tracim/templates/admin/workspace_getone.mak:46
1439
 #: tracim/templates/admin/workspace_getone.mak:46

+ 64 - 0
tracim/tracim/lib/content.py View File

29
 from tracim.lib import cmp_to_key
29
 from tracim.lib import cmp_to_key
30
 from tracim.lib.notifications import NotifierFactory
30
 from tracim.lib.notifications import NotifierFactory
31
 from tracim.lib.utils import SameValueError
31
 from tracim.lib.utils import SameValueError
32
+from tracim.lib.utils import current_date_for_filename
32
 from tracim.model import DBSession
33
 from tracim.model import DBSession
33
 from tracim.model import new_revision
34
 from tracim.model import new_revision
34
 from tracim.model.auth import User
35
 from tracim.model.auth import User
860
 
861
 
861
         item.revision_type = ActionDescription.MOVE
862
         item.revision_type = ActionDescription.MOVE
862
 
863
 
864
+    def copy(
865
+        self,
866
+        item: Content,
867
+        new_parent: Content=None,
868
+        new_label: str=None,
869
+        do_save: bool=True,
870
+        do_notify: bool=True,
871
+    ) -> Content:
872
+        """
873
+        Copy nearly all content, revision included. Children not included, see
874
+        "copy_children" for this.
875
+        :param item: Item to copy
876
+        :param new_parent: new parent of the new copied item
877
+        :param new_label: new label of the new copied item
878
+        :param do_notify: notify copy or not
879
+        :return: Newly copied item
880
+        """
881
+        if (not new_parent and not new_label) or (new_parent == item.parent and new_label == item.label):  # nopep8
882
+            # TODO - G.M - 08-03-2018 - Use something else than value error
883
+            raise ValueError("You can't copy file into itself")
884
+        if new_parent:
885
+            workspace = new_parent.workspace
886
+            parent = new_parent
887
+        else:
888
+            workspace = item.workspace
889
+            parent = item.parent
890
+        label = new_label or item.label
891
+
892
+        content = item.copy(parent)
893
+        # INFO - GM - 15-03-2018 - add "copy" revision
894
+        with new_revision(content, force_create_new_revision=True) as rev:
895
+            rev.parent = parent
896
+            rev.workspace = workspace
897
+            rev.label = label
898
+            rev.revision_type = ActionDescription.COPY
899
+            rev.properties['origin'] = {
900
+                'content': item.id,
901
+                'revision': item.last_revision.revision_id,
902
+            }
903
+        if do_save:
904
+            self.save(content, ActionDescription.COPY, do_notify=do_notify)
905
+        return content
906
+
907
+    def copy_children(self, origin_content: Content, new_content: Content):
908
+        for child in origin_content.children:
909
+            self.copy(child, new_content)
910
+
863
     def move_recursively(self, item: Content,
911
     def move_recursively(self, item: Content,
864
                          new_parent: Content, new_workspace: Workspace):
912
                          new_parent: Content, new_workspace: Workspace):
865
         self.move(item, new_parent, False, new_workspace)
913
         self.move(item, new_parent, False, new_workspace)
894
     def archive(self, content: Content):
942
     def archive(self, content: Content):
895
         content.owner = self._user
943
         content.owner = self._user
896
         content.is_archived = True
944
         content.is_archived = True
945
+        # TODO - G.M - 12-03-2018 - Inspect possible label conflict problem
946
+        # INFO - G.M - 12-03-2018 - Set label name to avoid trouble when
947
+        # un-archiving file.
948
+        content.label = '{label}-{action}-{date}'.format(
949
+            label=content.label,
950
+            action='archived',
951
+            date=current_date_for_filename()
952
+        )
897
         content.revision_type = ActionDescription.ARCHIVING
953
         content.revision_type = ActionDescription.ARCHIVING
898
 
954
 
899
     def unarchive(self, content: Content):
955
     def unarchive(self, content: Content):
904
     def delete(self, content: Content):
960
     def delete(self, content: Content):
905
         content.owner = self._user
961
         content.owner = self._user
906
         content.is_deleted = True
962
         content.is_deleted = True
963
+        # TODO - G.M - 12-03-2018 - Inspect possible label conflict problem
964
+        # INFO - G.M - 12-03-2018 - Set label name to avoid trouble when
965
+        # un-deleting file.
966
+        content.label = '{label}-{action}-{date}'.format(
967
+            label=content.label,
968
+            action='deleted',
969
+            date=current_date_for_filename()
970
+        )
907
         content.revision_type = ActionDescription.DELETION
971
         content.revision_type = ActionDescription.DELETION
908
 
972
 
909
     def undelete(self, content: Content):
973
     def undelete(self, content: Content):

+ 4 - 2
tracim/tracim/lib/daemons.py View File

19
 from tracim.lib.exceptions import AlreadyRunningDaemon
19
 from tracim.lib.exceptions import AlreadyRunningDaemon
20
 
20
 
21
 from tracim.lib.utils import get_rq_queue
21
 from tracim.lib.utils import get_rq_queue
22
+from tracim.lib.utils import TracimEnforceHTTPS
22
 from tracim.lib.email_fetcher import MailFetcher
23
 from tracim.lib.email_fetcher import MailFetcher
23
 
24
 
24
 
25
 
389
             print(
390
             print(
390
                 "WARNING: Could not import lxml: using xml instead (slower). Consider installing lxml from http://codespeak.net/lxml/.")
391
                 "WARNING: Could not import lxml: using xml instead (slower). Consider installing lxml from http://codespeak.net/lxml/.")
391
         from wsgidav.dir_browser import WsgiDavDirBrowser
392
         from wsgidav.dir_browser import WsgiDavDirBrowser
392
-        from tracim.lib.webdav.tracim_http_authenticator import TracimHTTPAuthenticator
393
+        from wsgidav.http_authenticator import HTTPAuthenticator
393
         from wsgidav.error_printer import ErrorPrinter
394
         from wsgidav.error_printer import ErrorPrinter
394
         from tracim.lib.webdav.utils import TracimWsgiDavDebugFilter
395
         from tracim.lib.webdav.utils import TracimWsgiDavDebugFilter
395
 
396
 
396
         config['middleware_stack'] = [
397
         config['middleware_stack'] = [
398
+            TracimEnforceHTTPS,
397
             WsgiDavDirBrowser,
399
             WsgiDavDirBrowser,
398
-            TracimHTTPAuthenticator,
400
+            HTTPAuthenticator,
399
             ErrorPrinter,
401
             ErrorPrinter,
400
             TracimWsgiDavDebugFilter,
402
             TracimWsgiDavDebugFilter,
401
         ]
403
         ]

+ 31 - 0
tracim/tracim/lib/utils.py View File

1
 # -*- coding: utf-8 -*-
1
 # -*- coding: utf-8 -*-
2
+import datetime
2
 import os
3
 import os
3
 import time
4
 import time
4
 import signal
5
 import signal
18
 from rq import Queue
19
 from rq import Queue
19
 from unidecode import unidecode
20
 from unidecode import unidecode
20
 
21
 
22
+from wsgidav.middleware import BaseMiddleware
21
 from tracim.lib.base import logger
23
 from tracim.lib.base import logger
22
 from webob import Response
24
 from webob import Response
23
 from webob.exc import WSGIHTTPException
25
 from webob.exc import WSGIHTTPException
193
         port=cfg.EMAIL_SENDER_REDIS_PORT,
195
         port=cfg.EMAIL_SENDER_REDIS_PORT,
194
         db=cfg.EMAIL_SENDER_REDIS_DB,
196
         db=cfg.EMAIL_SENDER_REDIS_DB,
195
     ))
197
     ))
198
+
199
+def current_date_for_filename() -> str:
200
+    """
201
+    ISO8601 current date, adapted to be used in filename (for
202
+    webdav feature for example), with trouble-free characters.
203
+    :return: current date as string like "2018-03-19T15.49.27.246592"
204
+    """
205
+    # INFO - G.M - 19-03-2018 - As ':' is in transform_to_bdd method in
206
+    # webdav utils, it may cause trouble. So, it should be replaced to
207
+    # a character which will not change in bdd.
208
+    return datetime.datetime.now().isoformat().replace(':', '.')
209
+
210
+class TracimEnforceHTTPS(BaseMiddleware):
211
+
212
+    def __init__(self, application, config):
213
+        super().__init__(application, config)
214
+        self._application = application
215
+        self._config = config
216
+
217
+    def __call__(self, environ, start_response):
218
+        # TODO - G.M - 06-03-2018 - Check protocol from http header first
219
+        # see http://www.bortzmeyer.org/7239.html
220
+        # if this params doesn't exist, rely on tracim config
221
+        from tracim.config.app_cfg import CFG
222
+        cfg = CFG.get_instance()
223
+
224
+        if cfg.WEBSITE_BASE_URL.startswith('https'):
225
+            environ['wsgi.url_scheme'] = 'https'
226
+        return self._application(environ, start_response)

+ 4 - 3
tracim/tracim/lib/webdav/design.py View File

120
     'unarchiving': 'Item unarchived',
120
     'unarchiving': 'Item unarchived',
121
     'undeletion': 'Item undeleted',
121
     'undeletion': 'Item undeleted',
122
     'move': 'Item moved',
122
     'move': 'Item moved',
123
-    'comment': 'Comment'
123
+    'comment': 'Comment',
124
+    'copy' : 'Item copied',
124
 }
125
 }
125
 
126
 
126
 
127
 
148
     return aff
149
     return aff
149
 
150
 
150
 def designPage(content: data.Content, content_revision: data.ContentRevisionRO) -> str:
151
 def designPage(content: data.Content, content_revision: data.ContentRevisionRO) -> str:
151
-    hist = content.get_history()
152
+    hist = content.get_history(drop_empty_revision=False)
152
     histHTML = '<table class="table table-striped table-hover">'
153
     histHTML = '<table class="table table-striped table-hover">'
153
     for event in hist:
154
     for event in hist:
154
         if isinstance(event, VirtualEvent):
155
         if isinstance(event, VirtualEvent):
237
     return page
238
     return page
238
 
239
 
239
 def designThread(content: data.Content, content_revision: data.ContentRevisionRO, comments) -> str:
240
 def designThread(content: data.Content, content_revision: data.ContentRevisionRO, comments) -> str:
240
-        hist = content.get_history()
241
+        hist = content.get_history(drop_empty_revision=False)
241
 
242
 
242
         allT = []
243
         allT = []
243
         allT += comments
244
         allT += comments

+ 11 - 11
tracim/tracim/lib/webdav/sql_domain_controller.py View File

2
 
2
 
3
 from tracim.lib.user import UserApi
3
 from tracim.lib.user import UserApi
4
 
4
 
5
+class DigestAuthNotImplemented(Exception):
6
+    pass
7
+
5
 class TracimDomainController(object):
8
 class TracimDomainController(object):
6
     """
9
     """
7
     The domain controller is used by http_authenticator to authenticate the user every time a request is
10
     The domain controller is used by http_authenticator to authenticate the user every time a request is
13
     def getDomainRealm(self, inputURL, environ):
16
     def getDomainRealm(self, inputURL, environ):
14
         return '/'
17
         return '/'
15
 
18
 
19
+    def getRealmUserPassword(self, realmname, username, environ):
20
+        """
21
+        This method is normally only use for digest auth. wsgidav need
22
+        plain password to deal with it. as we didn't
23
+        provide support for this kind of auth, this method raise an exception.
24
+        """
25
+        raise DigestAuthNotImplemented
26
+
16
     def requireAuthentication(self, realmname, environ):
27
     def requireAuthentication(self, realmname, environ):
17
         return True
28
         return True
18
 
29
 
27
         except:
38
         except:
28
             return False
39
             return False
29
 
40
 
30
-    def get_left_digest_response_hash(self, realmname, username, environ):
31
-        """
32
-        Called by our http_authenticator to get the hashed md5 digest for the current user that is also sent by
33
-        the webdav client
34
-        """
35
-        try:
36
-            user = self._api.get_one_by_email(username)
37
-            return user.webdav_left_digest_response_hash
38
-        except:
39
-            return None
40
-
41
     def authDomainUser(self, realmname, username, password, environ):
41
     def authDomainUser(self, realmname, username, password, environ):
42
         """
42
         """
43
         If you ever feel the need to send a request al-mano with a curl, this is the function that'll be called by
43
         If you ever feel the need to send a request al-mano with a curl, this is the function that'll be called by

+ 77 - 62
tracim/tracim/lib/webdav/sql_resources.py View File

36
 
36
 
37
 class ManageActions(object):
37
 class ManageActions(object):
38
     """
38
     """
39
-    This object is used to encapsulate all Deletion/Archiving related method as to not duplicate too much code
39
+    This object is used to encapsulate all Deletion/Archiving related
40
+    method as to not duplicate too much code
40
     """
41
     """
41
     def __init__(self, action_type: str, api: ContentApi, content: Content):
42
     def __init__(self, action_type: str, api: ContentApi, content: Content):
42
         self.content_api = api
43
         self.content_api = api
49
             ActionDescription.UNDELETION: self.content_api.undelete
50
             ActionDescription.UNDELETION: self.content_api.undelete
50
         }
51
         }
51
 
52
 
52
-        self._to_name = {
53
-            ActionDescription.ARCHIVING: 'archived',
54
-            ActionDescription.DELETION: 'deleted'
55
-        }
56
-
57
         self._type = action_type
53
         self._type = action_type
58
-        self._new_name = self.make_name()
59
 
54
 
60
     def action(self):
55
     def action(self):
61
-        try:
62
-            # When undeleting/unarchiving we except a content with the new name to not exist, thus if we
63
-            # don't get an error and the database request send back a result, we stop the action
64
-            self.content_api.get_one_by_label_and_parent(self._new_name, self.content.parent)
65
-            raise DAVError(HTTP_FORBIDDEN)
66
-        except NoResultFound:
67
-            with new_revision(self.content):
68
-                self.content_api.update_content(self.content, self._new_name)
69
-                self._actions[self._type](self.content)
70
-                self.content_api.save(self.content, self._type)
71
-
72
-            transaction.commit()
73
-
74
-    def make_name(self) -> str:
75
-        """
76
-        Will create the new name, either by adding '- deleted the [date]' after the name when archiving/deleting or
77
-        removing this string when undeleting/unarchiving
78
-        """
79
-        new_name = self.content.get_label_as_file()
80
-        extension = ''
81
-
82
-        # if the content has no label, the last .ext is important
83
-        # thus we want to rename a file from 'file.txt' to 'file - deleted... .txt' and not 'file.txt - deleted...'
84
-        is_file_name = self.content.label == ''
85
-        if is_file_name:
86
-            search = re.search(r'(\.[^.]+)$', new_name)
87
-            if search:
88
-                extension = search.group(0)
89
-            new_name = re.sub(r'(\.[^.]+)$', '', new_name)
90
-
91
-        if self._type in [ActionDescription.ARCHIVING, ActionDescription.DELETION]:
92
-            new_name += ' - %s the %s' % (self._to_name[self._type], datetime.now().strftime('%d-%m-%Y at %H:%M'))
93
-        else:
94
-            new_name = re.sub(
95
-                r'( - (%s|%s) the .*)$' % (self._to_name[ActionDescription.DELETION], self._to_name[ActionDescription.ARCHIVING]),
96
-                '',
97
-                new_name
98
-            )
99
-
100
-        new_name += extension
56
+        with new_revision(self.content):
57
+            self._actions[self._type](self.content)
58
+            self.content_api.save(self.content, self._type)
101
 
59
 
102
-        return new_name
60
+        transaction.commit()
103
 
61
 
104
 
62
 
105
 class Root(DAVCollection):
63
 class Root(DAVCollection):
964
         if invalid_path:
922
         if invalid_path:
965
             raise DAVError(HTTP_FORBIDDEN)
923
             raise DAVError(HTTP_FORBIDDEN)
966
 
924
 
967
-    def move_file(self, destpath):
925
+    def move_file(self, destpath: str) -> None:
926
+        """
927
+        Move file mean changing the path to access to a file. This can mean
928
+        simple renaming(1), moving file from a directory to one another(2)
929
+        but also renaming + moving file from a directory to one another at
930
+        the same time (3).
931
+
932
+        (1): move /dir1/file1 -> /dir1/file2
933
+        (2): move /dir1/file1 -> /dir2/file1
934
+        (3): move /dir1/file1 -> /dir2/file2
935
+        :param destpath: destination path of webdav move
936
+        :return: nothing
937
+        """
968
 
938
 
969
         workspace = self.content.workspace
939
         workspace = self.content.workspace
970
         parent = self.content.parent
940
         parent = self.content.parent
971
 
941
 
972
         with new_revision(self.content):
942
         with new_revision(self.content):
943
+            # INFO - G.M - 2018-03-09 - First, renaming file if needed
973
             if basename(destpath) != self.getDisplayName():
944
             if basename(destpath) != self.getDisplayName():
974
                 new_given_file_name = transform_to_bdd(basename(destpath))
945
                 new_given_file_name = transform_to_bdd(basename(destpath))
975
                 new_file_name, new_file_extension = \
946
                 new_file_name, new_file_extension = \
981
                 )
952
                 )
982
                 self.content.file_extension = new_file_extension
953
                 self.content.file_extension = new_file_extension
983
                 self.content_api.save(self.content)
954
                 self.content_api.save(self.content)
984
-            else:
985
-                workspace_api = WorkspaceApi(self.user)
986
-                content_api = ContentApi(self.user)
987
 
955
 
988
-                destination_workspace = self.provider.get_workspace_from_path(
989
-                    destpath,
990
-                    workspace_api,
991
-                )
992
-
993
-                destination_parent = self.provider.get_parent_from_path(
994
-                    destpath,
995
-                    content_api,
996
-                    destination_workspace,
997
-                )
956
+            # INFO - G.M - 2018-03-09 - Moving file if needed
957
+            workspace_api = WorkspaceApi(self.user)
958
+            content_api = ContentApi(self.user)
998
 
959
 
960
+            destination_workspace = self.provider.get_workspace_from_path(
961
+                destpath,
962
+                workspace_api,
963
+            )
964
+            destination_parent = self.provider.get_parent_from_path(
965
+                destpath,
966
+                content_api,
967
+                destination_workspace,
968
+            )
969
+            if destination_parent != parent or destination_workspace != workspace:  # nopep8
970
+                #  INFO - G.M - 12-03-2018 - Avoid moving the file "at the same place"  # nopep8
971
+                #  if the request does not result in a real move.
999
                 self.content_api.move(
972
                 self.content_api.move(
1000
                     item=self.content,
973
                     item=self.content,
1001
                     new_parent=destination_parent,
974
                     new_parent=destination_parent,
1005
 
978
 
1006
         transaction.commit()
979
         transaction.commit()
1007
 
980
 
981
+    def copyMoveSingle(self, destpath, isMove):
982
+        if isMove:
983
+            # INFO - G.M - 12-03-2018 - This case should not happen
984
+            # As far as moveRecursive method exist, all move should not go
985
+            # through this method. If such case appear, try replace this to :
986
+            ####
987
+            # self.move_file(destpath)
988
+            # return
989
+            ####
990
+
991
+            raise NotImplemented
992
+
993
+        new_file_name = None
994
+        new_file_extension = None
995
+
996
+        # Inspect destpath
997
+        if basename(destpath) != self.getDisplayName():
998
+            new_given_file_name = transform_to_bdd(basename(destpath))
999
+            new_file_name, new_file_extension = \
1000
+                os.path.splitext(new_given_file_name)
1001
+
1002
+        workspace_api = WorkspaceApi(self.user)
1003
+        content_api = ContentApi(self.user)
1004
+        destination_workspace = self.provider.get_workspace_from_path(
1005
+            destpath,
1006
+            workspace_api,
1007
+        )
1008
+        destination_parent = self.provider.get_parent_from_path(
1009
+            destpath,
1010
+            content_api,
1011
+            destination_workspace,
1012
+        )
1013
+        workspace = self.content.workspace
1014
+        parent = self.content.parent
1015
+        new_content = self.content_api.copy(
1016
+            item=self.content,
1017
+            new_label=new_file_name,
1018
+            new_parent=destination_parent,
1019
+        )
1020
+        self.content_api.copy_children(self.content, new_content)
1021
+        transaction.commit()
1022
+
1008
     def supportRecursiveMove(self, destPath):
1023
     def supportRecursiveMove(self, destPath):
1009
         return True
1024
         return True
1010
 
1025
 

+ 0 - 156
tracim/tracim/lib/webdav/tracim_http_authenticator.py View File

1
-import os
2
-import re
3
-
4
-from wsgidav.http_authenticator import HTTPAuthenticator
5
-from wsgidav import util
6
-import cherrypy
7
-
8
-from tracim.lib.user import CurrentUserGetterApi
9
-from tracim.lib.user import CURRENT_USER_WSGIDAV
10
-
11
-_logger = util.getModuleLogger(__name__, True)
12
-HOTFIX_WINXP_AcceptRootShareLogin = True
13
-
14
-
15
-class TracimHTTPAuthenticator(HTTPAuthenticator):
16
-    def __init__(self, application, config):
17
-        super(TracimHTTPAuthenticator, self).__init__(application, config)
18
-        self._headerfixparser = re.compile(r'([\w]+)=("[^"]*,[^"]*"),')
19
-
20
-    def authDigestAuthRequest(self, environ, start_response):
21
-        realmname = self._domaincontroller.getDomainRealm(environ["PATH_INFO"], environ)
22
-
23
-        isinvalidreq = False
24
-
25
-        authheaderdict = dict([])
26
-        authheaders = environ["HTTP_AUTHORIZATION"] + ","
27
-        if not authheaders.lower().strip().startswith("digest"):
28
-            isinvalidreq = True
29
-            # Hotfix for Windows file manager and OSX Finder:
30
-        # Some clients don't urlencode paths in auth header, so uri value may
31
-        # contain commas, which break the usual regex headerparser. Example:
32
-        # Digest username="user",realm="/",uri="a,b.txt",nc=00000001, ...
33
-        # -> [..., ('uri', '"a'), ('nc', '00000001'), ...]
34
-        # Override any such values with carefully extracted ones.
35
-        authheaderlist = self._headerparser.findall(authheaders)
36
-        authheaderfixlist = self._headerfixparser.findall(authheaders)
37
-        if authheaderfixlist:
38
-            _logger.info("Fixing authheader comma-parsing: extend %s with %s" \
39
-                         % (authheaderlist, authheaderfixlist))
40
-            authheaderlist += authheaderfixlist
41
-        for authheader in authheaderlist:
42
-            authheaderkey = authheader[0]
43
-            authheadervalue = authheader[1].strip().strip("\"")
44
-            authheaderdict[authheaderkey] = authheadervalue
45
-
46
-        _logger.debug("authDigestAuthRequest: %s" % environ["HTTP_AUTHORIZATION"])
47
-        _logger.debug("  -> %s" % authheaderdict)
48
-
49
-        if "username" in authheaderdict:
50
-            req_username = authheaderdict["username"]
51
-            req_username_org = req_username
52
-            # Hotfix for Windows XP:
53
-            #   net use W: http://127.0.0.1/dav /USER:DOMAIN\tester tester
54
-            # will send the name with double backslashes ('DOMAIN\\tester')
55
-            # but send the digest for the simple name ('DOMAIN\tester').
56
-            if r"\\" in req_username:
57
-                req_username = req_username.replace("\\\\", "\\")
58
-                _logger.info("Fixing Windows name with double backslash: '%s' --> '%s'" % (req_username_org, req_username))
59
-
60
-            if not self._domaincontroller.isRealmUser(realmname, req_username, environ):
61
-                isinvalidreq = True
62
-        else:
63
-            isinvalidreq = True
64
-
65
-            # TODO: Chun added this comments, but code was commented out
66
-            # Do not do realm checking - a hotfix for WinXP using some other realm's
67
-            # auth details for this realm - if user/password match
68
-
69
-        if 'realm' in authheaderdict:
70
-            if authheaderdict["realm"].upper() != realmname.upper():
71
-                if HOTFIX_WINXP_AcceptRootShareLogin:
72
-                    # Hotfix: also accept '/'
73
-                    if authheaderdict["realm"].upper() != "/":
74
-                        isinvalidreq = True
75
-                else:
76
-                    isinvalidreq = True
77
-
78
-        if "algorithm" in authheaderdict:
79
-            if authheaderdict["algorithm"].upper() != "MD5":
80
-                isinvalidreq = True  # only MD5 supported
81
-
82
-        if "uri" in authheaderdict:
83
-            req_uri = authheaderdict["uri"]
84
-
85
-        if "nonce" in authheaderdict:
86
-            req_nonce = authheaderdict["nonce"]
87
-        else:
88
-            isinvalidreq = True
89
-
90
-        req_hasqop = False
91
-        if "qop" in authheaderdict:
92
-            req_hasqop = True
93
-            req_qop = authheaderdict["qop"]
94
-            if req_qop.lower() != "auth":
95
-                isinvalidreq = True  # only auth supported, auth-int not supported
96
-        else:
97
-            req_qop = None
98
-
99
-        if "cnonce" in authheaderdict:
100
-            req_cnonce = authheaderdict["cnonce"]
101
-        else:
102
-            req_cnonce = None
103
-            if req_hasqop:
104
-                isinvalidreq = True
105
-
106
-        if "nc" in authheaderdict:  # is read but nonce-count checking not implemented
107
-            req_nc = authheaderdict["nc"]
108
-        else:
109
-            req_nc = None
110
-            if req_hasqop:
111
-                isinvalidreq = True
112
-
113
-        if "response" in authheaderdict:
114
-            req_response = authheaderdict["response"]
115
-        else:
116
-            isinvalidreq = True
117
-
118
-        if not isinvalidreq:
119
-            left_digest_response_hash = self._domaincontroller.get_left_digest_response_hash(realmname, req_username, environ)
120
-
121
-            req_method = environ["REQUEST_METHOD"]
122
-
123
-            required_digest = self.tracim_compute_digest_response(left_digest_response_hash, req_method, req_uri, req_nonce,
124
-                                                         req_cnonce, req_qop, req_nc)
125
-
126
-            if required_digest != req_response:
127
-                _logger.warning("computeDigestResponse('%s', '%s', ...): %s != %s" % (
128
-                realmname, req_username, required_digest, req_response))
129
-                isinvalidreq = True
130
-            else:
131
-                _logger.debug("digest succeeded for realm '%s', user '%s'" % (realmname, req_username))
132
-            pass
133
-
134
-        if isinvalidreq:
135
-            _logger.warning("Authentication failed for user '%s', realm '%s'" % (req_username, realmname))
136
-            return self.sendDigestAuthResponse(environ, start_response)
137
-
138
-        environ["http_authenticator.realm"] = realmname
139
-        environ["http_authenticator.username"] = req_username
140
-
141
-        # Set request current user email to be able to recognise him later
142
-        cherrypy.request.current_user_email = req_username
143
-        CurrentUserGetterApi.set_thread_local_getter(CURRENT_USER_WSGIDAV)
144
-
145
-        return self._application(environ, start_response)
146
-
147
-    def tracim_compute_digest_response(self, left_digest_response_hash, method, uri, nonce, cnonce, qop, nc):
148
-        # A1 : username:realm:password
149
-        A2 = method + ":" + uri
150
-        if qop:
151
-            digestresp = self.md5kd(left_digest_response_hash, nonce + ":" + nc + ":" + cnonce + ":" + qop + ":" + self.md5h(A2))
152
-        else:
153
-            digestresp = self.md5kd(left_digest_response_hash, nonce + ":" + self.md5h(A2))
154
-            # print(A1, A2)
155
-        # print(digestresp)
156
-        return digestresp

+ 6 - 2
tracim/tracim/model/__init__.py View File

120
 
120
 
121
 
121
 
122
 @contextmanager
122
 @contextmanager
123
-def new_revision(content: Content) -> Content:
123
+def new_revision(
124
+        content: Content,
125
+        force_create_new_revision: bool=False,
126
+) -> Content:
124
     """
127
     """
125
     Prepare context to update a Content. It will add a new updatable revision to the content.
128
     Prepare context to update a Content. It will add a new updatable revision to the content.
126
     :param content: Content instance to update
129
     :param content: Content instance to update
128
     """
131
     """
129
     with DBSession.no_autoflush:
132
     with DBSession.no_autoflush:
130
         try:
133
         try:
131
-            if inspect(content.revision).has_identity:
134
+            if force_create_new_revision \
135
+                    or inspect(content.revision).has_identity:
132
                 content.new_revision()
136
                 content.new_revision()
133
             RevisionsIntegrity.add_to_updatable(content.revision)
137
             RevisionsIntegrity.add_to_updatable(content.revision)
134
             yield content
138
             yield content

+ 0 - 25
tracim/tracim/model/auth.py View File

128
     is_active = Column(Boolean, default=True, nullable=False)
128
     is_active = Column(Boolean, default=True, nullable=False)
129
     imported_from = Column(Unicode(32), nullable=True)
129
     imported_from = Column(Unicode(32), nullable=True)
130
     timezone = Column(Unicode(255), nullable=False, server_default='')
130
     timezone = Column(Unicode(255), nullable=False, server_default='')
131
-    _webdav_left_digest_response_hash = Column('webdav_left_digest_response_hash', Unicode(128))
132
     auth_token = Column(Unicode(255))
131
     auth_token = Column(Unicode(255))
133
     auth_token_created = Column(DateTime)
132
     auth_token_created = Column(DateTime)
134
 
133
 
202
 
201
 
203
         Hash cleartext password on the fly,
202
         Hash cleartext password on the fly,
204
         Store its ciphertext version,
203
         Store its ciphertext version,
205
-        Update the WebDAV hash as well.
206
         """
204
         """
207
         self._password = self._hash_password(cleartext_password)
205
         self._password = self._hash_password(cleartext_password)
208
-        self.update_webdav_digest_auth(cleartext_password)
209
 
206
 
210
     def _get_password(self) -> str:
207
     def _get_password(self) -> str:
211
         """Return the hashed version of the password."""
208
         """Return the hashed version of the password."""
214
     password = synonym('_password', descriptor=property(_get_password,
211
     password = synonym('_password', descriptor=property(_get_password,
215
                                                         _set_password))
212
                                                         _set_password))
216
 
213
 
217
-    @classmethod
218
-    def _hash_digest(cls, digest):
219
-        return md5(bytes(digest, 'utf-8')).hexdigest()
220
-
221
-    def _set_hash_digest(self, digest):
222
-        self._webdav_left_digest_response_hash = self._hash_digest(digest)
223
-
224
-    def _get_hash_digest(self):
225
-        return self._webdav_left_digest_response_hash
226
-
227
-    webdav_left_digest_response_hash = synonym('_webdav_left_digest_response_hash',
228
-                                               descriptor=property(_get_hash_digest,
229
-                                                                   _set_hash_digest))
230
-
231
-    def update_webdav_digest_auth(self, cleartext_password: str) -> None:
232
-        self.webdav_left_digest_response_hash \
233
-            = '{username}:/:{cleartext_password}'.format(
234
-                username=self.email,
235
-                cleartext_password=cleartext_password,
236
-            )
237
 
214
 
238
     def validate_password(self, cleartext_password: str) -> bool:
215
     def validate_password(self, cleartext_password: str) -> bool:
239
         """
216
         """
252
             hash = sha256()
229
             hash = sha256()
253
             hash.update((cleartext_password + self.password[:64]).encode('utf-8'))
230
             hash.update((cleartext_password + self.password[:64]).encode('utf-8'))
254
             result = self.password[64:] == hash.hexdigest()
231
             result = self.password[64:] == hash.hexdigest()
255
-            if result and not self.webdav_left_digest_response_hash:
256
-                self.update_webdav_digest_auth(cleartext_password)
257
         return result
232
         return result
258
 
233
 
259
     def get_display_name(self, remove_email_part: bool=False) -> str:
234
     def get_display_name(self, remove_email_part: bool=False) -> str:

+ 66 - 7
tracim/tracim/model/data.py View File

204
     - closed-deprecated
204
     - closed-deprecated
205
     """
205
     """
206
 
206
 
207
+    COPY = 'copy'
207
     ARCHIVING = 'archiving'
208
     ARCHIVING = 'archiving'
208
     COMMENT = 'content-comment'
209
     COMMENT = 'content-comment'
209
     CREATION = 'creation'
210
     CREATION = 'creation'
225
         'status-update': 'fa-random',
226
         'status-update': 'fa-random',
226
         'unarchiving': 'fa-file-archive-o',
227
         'unarchiving': 'fa-file-archive-o',
227
         'undeletion': 'fa-trash-o',
228
         'undeletion': 'fa-trash-o',
228
-        'move': 'fa-arrows'
229
+        'move': 'fa-arrows',
230
+        'copy': 'fa-files-o',
229
     }
231
     }
230
 
232
 
231
     _LABELS = {
233
     _LABELS = {
238
         'status-update': l_('New status'),
240
         'status-update': l_('New status'),
239
         'unarchiving': l_('Item unarchived'),
241
         'unarchiving': l_('Item unarchived'),
240
         'undeletion': l_('Item undeleted'),
242
         'undeletion': l_('Item undeleted'),
241
-        'move': l_('Item moved')
243
+        'move': l_('Item moved'),
244
+        'copy': l_('Item copied'),
242
     }
245
     }
243
 
246
 
244
     def __init__(self, id):
247
     def __init__(self, id):
259
                 cls.STATUS_UPDATE,
262
                 cls.STATUS_UPDATE,
260
                 cls.UNARCHIVING,
263
                 cls.UNARCHIVING,
261
                 cls.UNDELETION,
264
                 cls.UNDELETION,
262
-                cls.MOVE]
265
+                cls.MOVE,
266
+                cls.COPY,
267
+                ]
263
 
268
 
264
 
269
 
265
 class ContentStatus(object):
270
 class ContentStatus(object):
514
                 return False
519
                 return False
515
             return True
520
             return True
516
 
521
 
522
+        # TODO - G.M - 15-03-2018 - Choose only correct Content-type for origin
523
+        # Only content who can be copied need this
524
+        if item.type == ContentType.Any:
525
+            if 'origin' in properties.keys():
526
+                return True
517
         raise NotImplementedError
527
         raise NotImplementedError
518
 
528
 
519
     @classmethod
529
     @classmethod
580
         'is_archived',
590
         'is_archived',
581
         'is_deleted',
591
         'is_deleted',
582
         'label',
592
         'label',
583
-        'node',
584
         'owner',
593
         'owner',
585
         'owner_id',
594
         'owner_id',
586
         'parent',
595
         'parent',
640
 
649
 
641
         return new_rev
650
         return new_rev
642
 
651
 
652
+    @classmethod
653
+    def copy(
654
+            cls,
655
+            revision: 'ContentRevisionRO',
656
+            parent: 'Content'
657
+    ) -> 'ContentRevisionRO':
658
+
659
+        copy_rev = cls()
660
+        import copy
661
+        copy_columns = cls._cloned_columns
662
+        for column_name in copy_columns:
663
+            # INFO - G-M - 15-03-2018 - set correct parent
664
+            if column_name == 'parent_id':
665
+                column_value = copy.copy(parent.id)
666
+            elif column_name == 'parent':
667
+                column_value = copy.copy(parent)
668
+            else:
669
+                column_value = copy.copy(getattr(revision, column_name))
670
+            setattr(copy_rev, column_name, column_value)
671
+
672
+        # copy attached_file
673
+        if revision.depot_file:
674
+            copy_rev.depot_file = FileIntent(
675
+                revision.depot_file.file.read(),
676
+                revision.file_name,
677
+                revision.file_mimetype,
678
+            )
679
+        return copy_rev
680
+
681
+
643
     def __setattr__(self, key: str, value: 'mixed'):
682
     def __setattr__(self, key: str, value: 'mixed'):
644
         """
683
         """
645
         ContentRevisionUpdateError is raised if tried to update column and revision own identity
684
         ContentRevisionUpdateError is raised if tried to update column and revision own identity
1077
         revisions = sorted(self.revisions, key=lambda revision: revision.revision_id)
1116
         revisions = sorted(self.revisions, key=lambda revision: revision.revision_id)
1078
         return revisions[-1]
1117
         return revisions[-1]
1079
 
1118
 
1080
-    def new_revision(self) -> None:
1119
+    def new_revision(self) -> ContentRevisionRO:
1081
         """
1120
         """
1082
         Return and assign to this content a new revision.
1121
         Return and assign to this content a new revision.
1083
         If it's a new content, revision is totally new.
1122
         If it's a new content, revision is totally new.
1234
 
1273
 
1235
         return ContentType.sorted(types)
1274
         return ContentType.sorted(types)
1236
 
1275
 
1237
-    def get_history(self) -> '[VirtualEvent]':
1276
+    def get_history(self, drop_empty_revision=False) -> '[VirtualEvent]':
1238
         events = []
1277
         events = []
1239
         for comment in self.get_comments():
1278
         for comment in self.get_comments():
1240
             events.append(VirtualEvent.create_from_content(comment))
1279
             events.append(VirtualEvent.create_from_content(comment))
1241
-        for revision in self.revisions:
1280
+
1281
+        revisions = sorted(self.revisions, key=lambda rev: rev.revision_id)
1282
+        for revision in revisions:
1283
+            # INFO - G.M - 09-03-2018 - Do not show file revision with empty
1284
+            # file to have a more clear view of revision.
1285
+            # Some webdav client create empty file before uploading, we must
1286
+            # have possibility to not show the related revision
1287
+            if drop_empty_revision:
1288
+                if revision.depot_file and revision.depot_file.file.content_length == 0:  # nopep8
1289
+                    # INFO - G.M - 12-03-2018 -Always show the last and
1290
+                    # first revision.
1291
+                    if revision != revisions[-1] and revision != revisions[0]:
1292
+                        continue
1293
+
1242
             events.append(VirtualEvent.create_from_content_revision(revision))
1294
             events.append(VirtualEvent.create_from_content_revision(revision))
1243
 
1295
 
1244
         sorted_events = sorted(events,
1296
         sorted_events = sorted(events,
1253
         cid = content.content_id
1305
         cid = content.content_id
1254
         return url_template.format(wid=wid, fid=fid, ctype=ctype, cid=cid)
1306
         return url_template.format(wid=wid, fid=fid, ctype=ctype, cid=cid)
1255
 
1307
 
1308
+    def copy(self, parent):
1309
+        cpy_content = Content()
1310
+        for rev in self.revisions:
1311
+            cpy_rev = ContentRevisionRO.copy(rev, parent)
1312
+            cpy_content.revisions.append(cpy_rev)
1313
+        return cpy_content
1314
+
1256
 
1315
 
1257
 class RevisionReadStatus(DeclarativeBase):
1316
 class RevisionReadStatus(DeclarativeBase):
1258
 
1317
 

+ 2 - 2
tracim/tracim/model/serializers.py View File

398
             links=[],
398
             links=[],
399
             revision_nb = len(content.revisions),
399
             revision_nb = len(content.revisions),
400
             selected_revision='latest' if content.revision_to_serialize<=0 else content.revision_to_serialize,
400
             selected_revision='latest' if content.revision_to_serialize<=0 else content.revision_to_serialize,
401
-            history=Context(CTX.CONTENT_HISTORY).toDict(content.get_history()),
401
+            history=Context(CTX.CONTENT_HISTORY).toDict(content.get_history(drop_empty_revision=True)),  # nopep8
402
             is_editable=content.is_editable,
402
             is_editable=content.is_editable,
403
             is_deleted=content.is_deleted,
403
             is_deleted=content.is_deleted,
404
             is_archived=content.is_archived,
404
             is_archived=content.is_archived,
475
             workspace = context.toDict(item.workspace),
475
             workspace = context.toDict(item.workspace),
476
             comments = reversed(context.toDict(item.get_comments())),
476
             comments = reversed(context.toDict(item.get_comments())),
477
             is_new=item.has_new_information_for(context.get_user()),
477
             is_new=item.has_new_information_for(context.get_user()),
478
-            history = Context(CTX.CONTENT_HISTORY).toDict(item.get_history()),
478
+            history = Context(CTX.CONTENT_HISTORY).toDict(item.get_history(drop_empty_revision=True)),  # nopep8
479
             is_editable=item.is_editable,
479
             is_editable=item.is_editable,
480
             is_deleted=item.is_deleted,
480
             is_deleted=item.is_deleted,
481
             is_archived=item.is_archived,
481
             is_archived=item.is_archived,

+ 3 - 1
tracim/tracim/public/_caldavzap/resource.js View File

489
 			newElement.addClass('r_operate');
489
 			newElement.addClass('r_operate');
490
 
490
 
491
     var uniqueIdForLabel = inputResource.displayvalue.replace('.', '') // remove the point so jquery doesn't consider it as 2 differents classes when used in a selector
491
     var uniqueIdForLabel = inputResource.displayvalue.replace('.', '') // remove the point so jquery doesn't consider it as 2 differents classes when used in a selector
492
+    // Côme - 2018/03/19 - added custom style to avoid the '...' for long workspace name that hide the entire name
493
+		var customStyle = 'display: inline-block;white-space: nowrap;text-overflow: ellipsis;overflow: hidden;vertical-align: top;width: 80%;'
492
 		newElement.html("\
494
 		newElement.html("\
493
       <div class='resourceCalDAVColor' style='background:"+inputResource.ecolor+"'></div>\
495
       <div class='resourceCalDAVColor' style='background:"+inputResource.ecolor+"'></div>\
494
       <input type='text' class='colorPicker'/>\
496
       <input type='text' class='colorPicker'/>\
495
       <input type='checkbox' name="+inputResource.uid+" />\
497
       <input type='checkbox' name="+inputResource.uid+" />\
496
-      <div class='"+uniqueIdForLabel+"' style='display:inline-block'>"+inputResource.displayvalue+"</div>");
498
+      <div class='"+uniqueIdForLabel+"' style='"+customStyle+"'>"+inputResource.displayvalue+"</div>");
497
 
499
 
498
     // +$('<div/>').text(inputResource.displayvalue).html()); // legacy code
500
     // +$('<div/>').text(inputResource.displayvalue).html()); // legacy code
499
     $.ajax({
501
     $.ajax({

+ 2 - 2
tracim/tracim/templates/workspace/getone.mak View File

129
             <div class="col-md-6">
129
             <div class="col-md-6">
130
                 <div class="input-group">
130
                 <div class="input-group">
131
                     <span class="input-group-addon" style="width: 8em;"><i class="fa fa-fw fa-windows"></i> Windows</span>
131
                     <span class="input-group-addon" style="width: 8em;"><i class="fa fa-fw fa-windows"></i> Windows</span>
132
-                    <div class="form-control webdavconfig__input">http://${webdav_url}</div>
132
+                    <div class="form-control webdavconfig__input">${website_protocol}://${webdav_url}</div>
133
                 </div>
133
                 </div>
134
                 <p></p>
134
                 <p></p>
135
                 <div class="input-group">
135
                 <div class="input-group">
136
                     <span class="input-group-addon" style="width: 8em;"><i class="fa fa-fw fa-linux"></i> Linux</span>
136
                     <span class="input-group-addon" style="width: 8em;"><i class="fa fa-fw fa-linux"></i> Linux</span>
137
-                    <div class="form-control webdavconfig__input">dav://${webdav_url}</div>
137
+                    <div class="form-control webdavconfig__input">${dav_protocol}://${webdav_url}</div>
138
                 </div>
138
                 </div>
139
             </div>
139
             </div>
140
             <div class="col-md-6">
140
             <div class="col-md-6">

+ 0 - 6
tracim/tracim/tests/command/user.py View File

14
         # Check webdav digest exist for this user
14
         # Check webdav digest exist for this user
15
         user = DBSession.query(User)\
15
         user = DBSession.query(User)\
16
             .filter(User.email == 'new-user@algoo.fr').one()
16
             .filter(User.email == 'new-user@algoo.fr').one()
17
-        ok_(user.webdav_left_digest_response_hash)
18
 
17
 
19
     def test_update_password(self):
18
     def test_update_password(self):
20
         self._create_user('new-user@algoo.fr', 'toor')
19
         self._create_user('new-user@algoo.fr', 'toor')
22
         # Grab webdav digest
21
         # Grab webdav digest
23
         user = DBSession.query(User) \
22
         user = DBSession.query(User) \
24
             .filter(User.email == 'new-user@algoo.fr').one()
23
             .filter(User.email == 'new-user@algoo.fr').one()
25
-        webdav_digest = user.webdav_left_digest_response_hash
26
 
24
 
27
         self._execute_command(
25
         self._execute_command(
28
             UpdateUserCommand,
26
             UpdateUserCommand,
35
         # Grab new webdav digest to compare it
33
         # Grab new webdav digest to compare it
36
         user = DBSession.query(User) \
34
         user = DBSession.query(User) \
37
             .filter(User.email == 'new-user@algoo.fr').one()
35
             .filter(User.email == 'new-user@algoo.fr').one()
38
-        ok_(
39
-            webdav_digest != user.webdav_left_digest_response_hash,
40
-            msg='Webdav digest should be different',
41
-        )
42
 
36
 
43
     def test_create_with_group(self):
37
     def test_create_with_group(self):
44
         more_args = ['--add-to-group', 'managers', '--add-to-group', 'administrators']
38
         more_args = ['--add-to-group', 'managers', '--add-to-group', 'administrators']

+ 0 - 8
tracim/tracim/tests/functional/test_admin.py View File

43
         ok_(user, msg="User should exist now")
43
         ok_(user, msg="User should exist now")
44
         ok_(user.validate_password('password'))
44
         ok_(user.validate_password('password'))
45
 
45
 
46
-        # User must have webdav digest
47
-        ok_(user.webdav_left_digest_response_hash)
48
-
49
     def test_update_user_password(self):
46
     def test_update_user_password(self):
50
         self._connect_user(
47
         self._connect_user(
51
             'admin@admin.admin',
48
             'admin@admin.admin',
67
 
64
 
68
         user = DBSession.query(User) \
65
         user = DBSession.query(User) \
69
             .filter(User.email == 'an-other-email@test.local').one()
66
             .filter(User.email == 'an-other-email@test.local').one()
70
-        webdav_digest = user.webdav_left_digest_response_hash
71
 
67
 
72
         self.app.post(
68
         self.app.post(
73
             '/admin/users/{user_id}/password?_method=PUT'.format(
69
             '/admin/users/{user_id}/password?_method=PUT'.format(
82
         user = DBSession.query(User) \
78
         user = DBSession.query(User) \
83
             .filter(User.email == 'an-other-email@test.local').one()
79
             .filter(User.email == 'an-other-email@test.local').one()
84
         ok_(user.validate_password('new-password'))
80
         ok_(user.validate_password('new-password'))
85
-        ok_(
86
-            webdav_digest != user.webdav_left_digest_response_hash,
87
-            msg='Webdav digest should be updated',
88
-        )

+ 0 - 5
tracim/tracim/tests/functional/test_user.py View File

22
 
22
 
23
         user = DBSession.query(User) \
23
         user = DBSession.query(User) \
24
             .filter(User.email == 'lawrence-not-real-email@fsf.local').one()
24
             .filter(User.email == 'lawrence-not-real-email@fsf.local').one()
25
-        webdav_digest = user.webdav_left_digest_response_hash
26
 
25
 
27
         try_post_user = self.app.post(
26
         try_post_user = self.app.post(
28
             '/user/{user_id}/password?_method=PUT'.format(
27
             '/user/{user_id}/password?_method=PUT'.format(
40
         user = DBSession.query(User) \
39
         user = DBSession.query(User) \
41
             .filter(User.email == 'lawrence-not-real-email@fsf.local').one()
40
             .filter(User.email == 'lawrence-not-real-email@fsf.local').one()
42
         ok_(user.validate_password('new-password'))
41
         ok_(user.validate_password('new-password'))
43
-        ok_(
44
-            webdav_digest != user.webdav_left_digest_response_hash,
45
-            msg='Webdav digest should be updated',
46
-        )

+ 262 - 0
tracim/tracim/tests/library/test_content_api.py View File

4
 from nose.tools import eq_, ok_
4
 from nose.tools import eq_, ok_
5
 from nose.tools import raises
5
 from nose.tools import raises
6
 
6
 
7
+from depot.io.utils import FileIntent
7
 import transaction
8
 import transaction
8
 
9
 
9
 from tracim.lib.content import compare_content_for_sorting_by_type_and_name
10
 from tracim.lib.content import compare_content_for_sorting_by_type_and_name
347
         eq_('', c.label)
348
         eq_('', c.label)
348
         eq_(ActionDescription.COMMENT, c.revision_type)
349
         eq_(ActionDescription.COMMENT, c.revision_type)
349
 
350
 
351
+    def test_unit_copy_file_different_label_different_parent_ok(self):
352
+        uapi = UserApi(None)
353
+        groups = [
354
+            GroupApi(None).get_one(Group.TIM_USER),
355
+            GroupApi(None).get_one(Group.TIM_MANAGER),
356
+            GroupApi(None).get_one(Group.TIM_ADMIN)
357
+        ]
358
+
359
+        user = uapi.create_user(
360
+            email='user1@user',
361
+            groups=groups,
362
+            save_now=True
363
+        )
364
+        user2 = uapi.create_user(
365
+            email='user2@user',
366
+            groups=groups,
367
+            save_now=True
368
+        )
369
+        workspace = WorkspaceApi(user).create_workspace(
370
+            'test workspace',
371
+            save_now=True
372
+        )
373
+        RoleApi(user).create_one(
374
+            user2,
375
+            workspace,
376
+            UserRoleInWorkspace.WORKSPACE_MANAGER,
377
+            with_notif=False
378
+        )
379
+        api = ContentApi(user)
380
+        foldera = api.create(
381
+            ContentType.Folder,
382
+            workspace,
383
+            None,
384
+            'folder a',
385
+            True
386
+        )
387
+        with DBSession.no_autoflush:
388
+            text_file = api.create(
389
+                content_type=ContentType.File,
390
+                workspace=workspace,
391
+                parent=foldera,
392
+                label='test_file',
393
+                do_save=False,
394
+            )
395
+            api.update_file_data(
396
+                text_file,
397
+                'test_file',
398
+                'text/plain',
399
+                b'test_content'
400
+            )
401
+
402
+        api.save(text_file, ActionDescription.CREATION)
403
+        api2 = ContentApi(user2)
404
+        workspace2 = WorkspaceApi(user2).create_workspace(
405
+            'test workspace2',
406
+            save_now=True
407
+        )
408
+        folderb = api2.create(
409
+            ContentType.Folder,
410
+            workspace2,
411
+            None,
412
+            'folder b',
413
+            True
414
+        )
415
+
416
+        api2.copy(
417
+            item=text_file,
418
+            new_parent=folderb,
419
+            new_label='test_file_copy'
420
+        )
421
+
422
+        transaction.commit()
423
+        text_file_copy = api2.get_one_by_label_and_parent(
424
+            'test_file_copy',
425
+            folderb,
426
+        )
427
+
428
+        assert text_file != text_file_copy
429
+        assert text_file_copy.content_id != text_file.content_id
430
+        assert text_file_copy.workspace_id == workspace2.workspace_id
431
+        assert text_file_copy.depot_file.file.read() == text_file.depot_file.file.read()   # nopep8
432
+        assert text_file_copy.depot_file.path != text_file.depot_file.path
433
+        assert text_file_copy.label == 'test_file_copy'
434
+        assert text_file_copy.type == text_file.type
435
+        assert text_file_copy.parent.content_id == folderb.content_id
436
+        assert text_file_copy.owner.user_id == user.user_id
437
+        assert text_file_copy.description == text_file.description
438
+        assert text_file_copy.file_extension == text_file.file_extension
439
+        assert text_file_copy.file_mimetype == text_file.file_mimetype
440
+        assert text_file_copy.revision_type == ActionDescription.COPY
441
+        assert len(text_file_copy.revisions) == len(text_file.revisions) + 1
442
+
443
+    def test_unit_copy_file__same_label_different_parent_ok(self):
444
+        uapi = UserApi(None)
445
+        groups = [GroupApi(None).get_one(Group.TIM_USER),
446
+                  GroupApi(None).get_one(Group.TIM_MANAGER),
447
+                  GroupApi(None).get_one(Group.TIM_ADMIN)]
448
+
449
+        user = uapi.create_user(
450
+            email='user1@user',
451
+            groups=groups,
452
+            save_now=True
453
+        )
454
+        user2 = uapi.create_user(
455
+            email='user2@user',
456
+            groups=groups,
457
+            save_now=True
458
+        )
459
+        workspace = WorkspaceApi(user).create_workspace(
460
+            'test workspace',
461
+            save_now=True
462
+        )
463
+        RoleApi(user).create_one(
464
+            user2,
465
+            workspace,
466
+            UserRoleInWorkspace.WORKSPACE_MANAGER,
467
+            with_notif=False
468
+        )
469
+        api = ContentApi(user)
470
+        foldera = api.create(
471
+            ContentType.Folder,
472
+            workspace,
473
+            None,
474
+            'folder a',
475
+            True
476
+        )
477
+        with DBSession.no_autoflush:
478
+            text_file = api.create(
479
+                content_type=ContentType.File,
480
+                workspace=workspace,
481
+                parent=foldera,
482
+                label='test_file',
483
+                do_save=False,
484
+            )
485
+            api.update_file_data(
486
+                text_file,
487
+                'test_file',
488
+                'text/plain',
489
+                b'test_content'
490
+            )
491
+
492
+        api.save(text_file, ActionDescription.CREATION)
493
+        api2 = ContentApi(user2)
494
+        workspace2 = WorkspaceApi(user2).create_workspace(
495
+            'test workspace2',
496
+            save_now=True
497
+        )
498
+        folderb = api2.create(
499
+            ContentType.Folder,
500
+            workspace2,
501
+            None,
502
+            'folder b',
503
+            True
504
+        )
505
+        api2.copy(
506
+            item=text_file,
507
+            new_parent=folderb,
508
+        )
509
+
510
+        transaction.commit()
511
+        text_file_copy = api2.get_one_by_label_and_parent(
512
+            'test_file',
513
+            folderb,
514
+        )
515
+
516
+        assert text_file != text_file_copy
517
+        assert text_file_copy.content_id != text_file.content_id
518
+        assert text_file_copy.workspace_id == workspace2.workspace_id
519
+        assert text_file_copy.depot_file.file.read() == text_file.depot_file.file.read()  # nopep8
520
+        assert text_file_copy.depot_file.path != text_file.depot_file.path
521
+        assert text_file_copy.label == text_file.label
522
+        assert text_file_copy.type == text_file.type
523
+        assert text_file_copy.parent.content_id == folderb.content_id
524
+        assert text_file_copy.owner.user_id == user.user_id
525
+        assert text_file_copy.description == text_file.description
526
+        assert text_file_copy.file_extension == text_file.file_extension
527
+        assert text_file_copy.file_mimetype == text_file.file_mimetype
528
+        assert text_file_copy.revision_type == ActionDescription.COPY
529
+        assert len(text_file_copy.revisions) == len(text_file.revisions) + 1
530
+
531
+    def test_unit_copy_file_different_label_same_parent_ok(self):
532
+        uapi = UserApi(None)
533
+        groups = [
534
+            GroupApi(None).get_one(Group.TIM_USER),
535
+            GroupApi(None).get_one(Group.TIM_MANAGER),
536
+            GroupApi(None).get_one(Group.TIM_ADMIN),
537
+        ]
538
+
539
+        user = uapi.create_user(
540
+            email='user1@user',
541
+            groups=groups,
542
+            save_now=True,
543
+        )
544
+        user2 = uapi.create_user(
545
+            email='user2@user',
546
+            groups=groups,
547
+            save_now=True
548
+        )
549
+        workspace = WorkspaceApi(user).create_workspace(
550
+            'test workspace',
551
+            save_now=True
552
+        )
553
+        RoleApi(user).create_one(
554
+            user2, workspace,
555
+            UserRoleInWorkspace.WORKSPACE_MANAGER,
556
+            with_notif=False
557
+        )
558
+        api = ContentApi(user)
559
+        foldera = api.create(
560
+            ContentType.Folder,
561
+            workspace,
562
+            None,
563
+            'folder a',
564
+            True
565
+        )
566
+        with DBSession.no_autoflush:
567
+            text_file = api.create(
568
+                content_type=ContentType.File,
569
+                workspace=workspace,
570
+                parent=foldera,
571
+                label='test_file',
572
+                do_save=False,
573
+            )
574
+            api.update_file_data(
575
+                text_file,
576
+                'test_file',
577
+                'text/plain',
578
+                b'test_content'
579
+            )
580
+
581
+        api.save(
582
+            text_file,
583
+            ActionDescription.CREATION
584
+        )
585
+        api2 = ContentApi(user2)
586
+
587
+        api2.copy(
588
+            item=text_file,
589
+            new_label='test_file_copy'
590
+        )
591
+
592
+        transaction.commit()
593
+        text_file_copy = api2.get_one_by_label_and_parent(
594
+            'test_file_copy',
595
+            foldera,
596
+        )
597
+
598
+        assert text_file != text_file_copy
599
+        assert text_file_copy.content_id != text_file.content_id
600
+        assert text_file_copy.workspace_id == workspace.workspace_id
601
+        assert text_file_copy.depot_file.file.read() == text_file.depot_file.file.read()  # nopep8
602
+        assert text_file_copy.depot_file.path != text_file.depot_file.path
603
+        assert text_file_copy.label == 'test_file_copy'
604
+        assert text_file_copy.type == text_file.type
605
+        assert text_file_copy.parent.content_id == foldera.content_id
606
+        assert text_file_copy.owner.user_id == user.user_id
607
+        assert text_file_copy.description == text_file.description
608
+        assert text_file_copy.file_extension == text_file.file_extension
609
+        assert text_file_copy.file_mimetype == text_file.file_mimetype
610
+        assert text_file_copy.revision_type == ActionDescription.COPY
611
+        assert len(text_file_copy.revisions) == len(text_file.revisions) + 1
350
 
612
 
351
     def test_mark_read__workspace(self):
613
     def test_mark_read__workspace(self):
352
         uapi = UserApi(None)
614
         uapi = UserApi(None)

+ 44 - 0
tracim/tracim/tests/library/test_webdav.py View File

481
             )
481
             )
482
         )
482
         )
483
 
483
 
484
+    def test_unit__move_and_rename_content__ok(self):
485
+        provider = self._get_provider()
486
+        environ = self._get_environ(
487
+            provider,
488
+            'bob@fsf.local',
489
+        )
490
+        w1f1d1 = provider.getResourceInst(
491
+            '/w1/w1f1/w1f1d1.txt',
492
+            environ,
493
+        )
494
+
495
+        content_w1f1d1 = DBSession.query(ContentRevisionRO) \
496
+            .filter(Content.label == 'w1f1d1') \
497
+            .one()  # It must exist only one revision, cf fixtures
498
+        ok_(content_w1f1d1, msg='w1f1d1 should be exist')
499
+        content_w1f1d1_id = content_w1f1d1.content_id
500
+        content_w1f1d1_parent = content_w1f1d1.parent
501
+        eq_(
502
+            content_w1f1d1_parent.label,
503
+            'w1f1',
504
+            msg='field parent should be w1f1',
505
+        )
506
+
507
+        w1f1d1.moveRecursive('/w1/w1f2/w1f1d1_RENAMED.txt')
508
+
509
+        # Database content is moved
510
+        content_w1f1d1 = DBSession.query(ContentRevisionRO) \
511
+            .filter(ContentRevisionRO.content_id == content_w1f1d1_id) \
512
+            .order_by(ContentRevisionRO.revision_id.desc()) \
513
+            .first()
514
+        ok_(
515
+            content_w1f1d1.parent.label != content_w1f1d1_parent.label,
516
+            msg='file should be moved in w1f2 but is in {0}'.format(
517
+                content_w1f1d1.parent.label
518
+            )
519
+        )
520
+        eq_(
521
+            'w1f1d1_RENAMED',
522
+            content_w1f1d1.label,
523
+            msg='File should be labeled w1f1d1_RENAMED, not {0}'.format(
524
+                content_w1f1d1.label
525
+            )
526
+        )
527
+
484
     def test_unit__move_content__ok__another_workspace(self):
528
     def test_unit__move_content__ok__another_workspace(self):
485
         provider = self._get_provider()
529
         provider = self._get_provider()
486
         environ = self._get_environ(
530
         environ = self._get_environ(

+ 7 - 1
tracim/tracim/tests/models/test_content_revision.py View File

13
 class TestContentRevision(BaseTest, TestStandard):
13
 class TestContentRevision(BaseTest, TestStandard):
14
 
14
 
15
     def _new_from(self, revision):
15
     def _new_from(self, revision):
16
-        excluded_columns = ('revision_id', '_sa_instance_state', 'depot_file')
16
+        excluded_columns = (
17
+            'revision_id',
18
+            '_sa_instance_state',
19
+            'depot_file',
20
+            'node',
21
+            'revision_read_statuses',
22
+        )
17
         revision_columns = [attr.key for attr in inspect(revision).attrs if not attr.key in excluded_columns]
23
         revision_columns = [attr.key for attr in inspect(revision).attrs if not attr.key in excluded_columns]
18
         new_revision = ContentRevisionRO()
24
         new_revision = ContentRevisionRO()
19
 
25
 

+ 4 - 0
tracim/wsgidav.conf.sample View File

25
 
25
 
26
 root_path = ''
26
 root_path = ''
27
 
27
 
28
+# Tracim doesn't support digest auth for webdav
29
+acceptbasic = True
30
+acceptdigest = False
31
+defaultdigest = False
28
 #===============================================================================
32
 #===============================================================================
29
 # Lock Manager
33
 # Lock Manager
30
 #
34
 #