Bläddra i källkod

Merge pull request #19 from tracim/feature/699_endpoint_to_delete_user

Bastien Sevajol 6 år sedan
förälder
incheckning
a6eabea2d8
No account linked to committer's email

+ 4 - 0
backend/README.md Visa fil

@@ -77,6 +77,10 @@ Initialize the database using [tracimcli](doc/cli.md) tool
77 77
 
78 78
     tracimcli db init
79 79
 
80
+Stamp current version of database to last (useful for migration):
81
+
82
+    alembic -c development.ini stamp head
83
+
80 84
 create wsgidav configuration file for webdav:
81 85
 
82 86
     cp wsgidav.conf.sample wsgidav.conf

+ 4 - 0
backend/doc/migration.md Visa fil

@@ -28,6 +28,10 @@ and active the Tracim virtualenv:
28 28
 
29 29
     alembic -c development.ini current
30 30
 
31
+## Set Alembic stamp to last version (first time use) ##
32
+
33
+    alembic -c development.ini stamp head
34
+
31 35
 ### Creating new schema migration ###
32 36
 
33 37
 This creates a new auto-generated python migration file 

+ 2 - 1
backend/doc/roles.md Visa fil

@@ -23,7 +23,8 @@ The other is workspace related and is called "workspace role".
23 23
 | delete workspace              |  no         | yes, if manager of a given workspace         | yes     |
24 24
 |-------------------------------|-------------|-------------|---------|
25 25
 | set user global profile rights|  no         | no          | yes     |
26
-| deactivate user               |  no         | no          | yes     |
26
+| activate/deactivate user      |  no         | no          | yes     |
27
+| delete user/ undelete user    |  no         | no          | yes     |
27 28
 |-------------------------------|-------------|-------------|---------|
28 29
 | access to all user data (/users/{user_id} endpoints) |personal-only|personal-only| yes     |
29 30
 

+ 16 - 1
backend/tracim_backend/lib/core/user.py Visa fil

@@ -34,13 +34,18 @@ class UserApi(object):
34 34
             current_user: typing.Optional[User],
35 35
             session: Session,
36 36
             config: CFG,
37
+            show_deleted: bool = False,
37 38
     ) -> None:
38 39
         self._session = session
39 40
         self._user = current_user
40 41
         self._config = config
42
+        self._show_deleted = show_deleted
41 43
 
42 44
     def _base_query(self):
43
-        return self._session.query(User)
45
+        query = self._session.query(User)
46
+        if not self._show_deleted:
47
+            query = query.filter(User.is_deleted == False)
48
+        return query
44 49
 
45 50
     def get_user_with_context(self, user: User) -> UserInContext:
46 51
         """
@@ -382,6 +387,16 @@ class UserApi(object):
382 387
         if do_save:
383 388
             self.save(user)
384 389
 
390
+    def delete(self, user: User, do_save=False):
391
+        user.is_deleted = True
392
+        if do_save:
393
+            self.save(user)
394
+
395
+    def undelete(self, user: User, do_save=False):
396
+        user.is_deleted = False
397
+        if do_save:
398
+            self.save(user)
399
+
385 400
     def save(self, user: User):
386 401
         self._session.flush()
387 402
 

+ 1 - 1
backend/tracim_backend/lib/utils/request.py Visa fil

@@ -293,7 +293,7 @@ class TracimRequest(Request):
293 293
         :return: user found from header/body
294 294
         """
295 295
         app_config = request.registry.settings['CFG']
296
-        uapi = UserApi(None, session=request.dbsession, config=app_config)
296
+        uapi = UserApi(None, show_deleted=True, session=request.dbsession, config=app_config)
297 297
         login = ''
298 298
         try:
299 299
             login = None

+ 26 - 0
backend/tracim_backend/migration/versions/78b52ca39419_add_is_deleted_to_user.py Visa fil

@@ -0,0 +1,26 @@
1
+"""add_is_deleted_to_user
2
+
3
+Revision ID: 78b52ca39419
4
+Revises: ad79f58ec2bf
5
+Create Date: 2018-08-09 15:50:49.656925
6
+
7
+"""
8
+
9
+# revision identifiers, used by Alembic.
10
+revision = '78b52ca39419'
11
+down_revision = 'ad79f58ec2bf'
12
+
13
+from alembic import op
14
+import sqlalchemy as sa
15
+
16
+
17
+def upgrade():
18
+    # ### commands auto generated by Alembic - please adjust! ###
19
+    op.add_column('users', sa.Column('is_deleted', sa.Boolean(), server_default=sa.sql.expression.literal(False), nullable=False))
20
+    # ### end Alembic commands ###
21
+
22
+
23
+def downgrade():
24
+    # ### commands auto generated by Alembic - please adjust! ###
25
+    op.drop_column('users', 'is_deleted')
26
+    # ### end Alembic commands ###

+ 2 - 0
backend/tracim_backend/models/auth.py Visa fil

@@ -15,6 +15,7 @@ from datetime import datetime
15 15
 from hashlib import sha256
16 16
 from typing import TYPE_CHECKING
17 17
 
18
+import sqlalchemy
18 19
 from sqlalchemy import Column
19 20
 from sqlalchemy import ForeignKey
20 21
 from sqlalchemy import Sequence
@@ -135,6 +136,7 @@ class User(DeclarativeBase):
135 136
     _password = Column('password', Unicode(128))
136 137
     created = Column(DateTime, default=datetime.utcnow)
137 138
     is_active = Column(Boolean, default=True, nullable=False)
139
+    is_deleted = Column(Boolean, default=False, nullable=False, server_default=sqlalchemy.sql.expression.literal(False))
138 140
     imported_from = Column(Unicode(32), nullable=True)
139 141
     timezone = Column(Unicode(255), nullable=False, server_default='')
140 142
     # TODO - G.M - 04-04-2018 - [auth] Check if this is already needed

+ 4 - 0
backend/tracim_backend/models/context_models.py Visa fil

@@ -406,6 +406,10 @@ class UserInContext(object):
406 406
     def profile(self) -> Profile:
407 407
         return self.user.profile.name
408 408
 
409
+    @property
410
+    def is_deleted(self) -> bool:
411
+        return self.user.is_deleted
412
+
409 413
     # Context related
410 414
 
411 415
     @property

+ 48 - 0
backend/tracim_backend/tests/functional/test_user.py Visa fil

@@ -2533,6 +2533,7 @@ class TestUserEndpoint(FunctionalTest):
2533 2533
         assert res['email'] == 'test@test.test'
2534 2534
         assert res['public_name'] == 'bob'
2535 2535
         assert res['timezone'] == 'Europe/Paris'
2536
+        assert res['is_deleted'] is False
2536 2537
 
2537 2538
     def test_api__get_user__ok_200__user_itself(self):
2538 2539
         dbsession = get_tm_session(self.session_factory, transaction.manager)
@@ -2582,6 +2583,7 @@ class TestUserEndpoint(FunctionalTest):
2582 2583
         assert res['email'] == 'test@test.test'
2583 2584
         assert res['public_name'] == 'bob'
2584 2585
         assert res['timezone'] == 'Europe/Paris'
2586
+        assert res['is_deleted'] is False
2585 2587
 
2586 2588
     def test_api__get_user__err_403__other_normal_user(self):
2587 2589
         dbsession = get_tm_session(self.session_factory, transaction.manager)
@@ -2634,6 +2636,52 @@ class TestUserEndpoint(FunctionalTest):
2634 2636
             status=403
2635 2637
         )
2636 2638
 
2639
+    def test_api_delete_user__ok_200__admin(self):
2640
+        dbsession = get_tm_session(self.session_factory, transaction.manager)
2641
+        admin = dbsession.query(models.User) \
2642
+            .filter(models.User.email == 'admin@admin.admin') \
2643
+            .one()
2644
+        uapi = UserApi(
2645
+            current_user=admin,
2646
+            session=dbsession,
2647
+            config=self.app_config,
2648
+        )
2649
+        gapi = GroupApi(
2650
+            current_user=admin,
2651
+            session=dbsession,
2652
+            config=self.app_config,
2653
+        )
2654
+        groups = [gapi.get_one_with_name('users')]
2655
+        test_user = uapi.create_user(
2656
+            email='test@test.test',
2657
+            password='pass',
2658
+            name='bob',
2659
+            groups=groups,
2660
+            timezone='Europe/Paris',
2661
+            do_save=True,
2662
+            do_notify=False,
2663
+        )
2664
+        uapi.save(test_user)
2665
+        transaction.commit()
2666
+        user_id = int(test_user.user_id)
2667
+
2668
+        self.testapp.authorization = (
2669
+            'Basic',
2670
+            (
2671
+                'admin@admin.admin',
2672
+                'admin@admin.admin'
2673
+            )
2674
+        )
2675
+        self.testapp.put(
2676
+            '/api/v2/users/{}/delete'.format(user_id),
2677
+            status=204
2678
+        )
2679
+        res = self.testapp.get(
2680
+            '/api/v2/users/{}'.format(user_id),
2681
+            status=200
2682
+        ).json_body
2683
+        assert res['is_deleted'] is True
2684
+
2637 2685
 
2638 2686
 class TestUsersEndpoint(FunctionalTest):
2639 2687
     # -*- coding: utf-8 -*-

+ 4 - 0
backend/tracim_backend/views/core_api/schemas.py Visa fil

@@ -75,6 +75,10 @@ class UserSchema(UserDigestSchema):
75 75
         example=True,
76 76
         description='Is user account activated ?'
77 77
     )
78
+    is_deleted = marshmallow.fields.Bool(
79
+        example=False,
80
+        description='Is user account deleted ?'
81
+    )
78 82
     # TODO - G.M - 17-04-2018 - Restrict timezone values
79 83
     timezone = marshmallow.fields.String(
80 84
         example="Europe/Paris",

+ 43 - 0
backend/tracim_backend/views/core_api/user_controller.py Visa fil

@@ -244,6 +244,41 @@ class UserController(Controller):
244 244
     @require_profile(Group.TIM_ADMIN)
245 245
     @hapic.input_path(UserIdPathSchema())
246 246
     @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT)  # nopep8
247
+    def delete_user(self, context, request: TracimRequest, hapic_data=None):
248
+        """
249
+        delete user
250
+        """
251
+        app_config = request.registry.settings['CFG']
252
+        uapi = UserApi(
253
+            current_user=request.current_user,  # User
254
+            session=request.dbsession,
255
+            config=app_config,
256
+        )
257
+        uapi.delete(user=request.candidate_user, do_save=True)
258
+        return
259
+
260
+    @hapic.with_api_doc(tags=[SWAGGER_TAG__USER_ENDPOINTS])
261
+    @require_profile(Group.TIM_ADMIN)
262
+    @hapic.input_path(UserIdPathSchema())
263
+    @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT)  # nopep8
264
+    def undelete_user(self, context, request: TracimRequest, hapic_data=None):
265
+        """
266
+        undelete user
267
+        """
268
+        app_config = request.registry.settings['CFG']
269
+        uapi = UserApi(
270
+            current_user=request.current_user,  # User
271
+            session=request.dbsession,
272
+            config=app_config,
273
+            show_deleted=True,
274
+        )
275
+        uapi.undelete(user=request.candidate_user, do_save=True)
276
+        return
277
+
278
+    @hapic.with_api_doc(tags=[SWAGGER_TAG__USER_ENDPOINTS])
279
+    @require_profile(Group.TIM_ADMIN)
280
+    @hapic.input_path(UserIdPathSchema())
281
+    @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT)  # nopep8
247 282
     def disable_user(self, context, request: TracimRequest, hapic_data=None):
248 283
         """
249 284
         disable user
@@ -464,6 +499,14 @@ class UserController(Controller):
464 499
         configurator.add_route('disable_user', '/users/{user_id}/disable', request_method='PUT')  # nopep8
465 500
         configurator.add_view(self.disable_user, route_name='disable_user')
466 501
 
502
+        # delete user
503
+        configurator.add_route('delete_user', '/users/{user_id}/delete', request_method='PUT')  # nopep8
504
+        configurator.add_view(self.delete_user, route_name='delete_user')
505
+
506
+        # undelete user
507
+        configurator.add_route('undelete_user', '/users/{user_id}/undelete', request_method='PUT')  # nopep8
508
+        configurator.add_view(self.undelete_user, route_name='undelete_user')
509
+
467 510
         # set user profile
468 511
         configurator.add_route('set_user_profile', '/users/{user_id}/profile', request_method='PUT')  # nopep8
469 512
         configurator.add_view(self.set_profile, route_name='set_user_profile')