Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / api / projects.py @ edc51a21

History | View | Annotate | Download (22.4 kB)

1
# Copyright 2013 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
#
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
#
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
#
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33

    
34
import re
35
from django.utils import simplejson as json
36
from django.views.decorators.csrf import csrf_exempt
37
from django.http import HttpResponse
38
from django.db.models import Q
39
from django.db import transaction
40

    
41
from astakos.api.util import json_response
42

    
43
from snf_django.lib import api
44
from snf_django.lib.api import faults
45
from .util import user_from_token, invert_dict, read_json_body
46

    
47
from astakos.im import functions
48
from astakos.im.models import (
49
    AstakosUser, Project, ProjectApplication, ProjectMembership,
50
    ProjectResourceQuota, ProjectResourceGrant, ProjectLog,
51
    ProjectMembershipLog)
52
import synnefo.util.date as date_util
53
from synnefo.util import units
54

    
55

    
56
MEMBERSHIP_POLICY_SHOW = {
57
    functions.AUTO_ACCEPT_POLICY: "auto",
58
    functions.MODERATED_POLICY:   "moderated",
59
    functions.CLOSED_POLICY:      "closed",
60
}
61

    
62
MEMBERSHIP_POLICY = invert_dict(MEMBERSHIP_POLICY_SHOW)
63

    
64
APPLICATION_STATE_SHOW = {
65
    ProjectApplication.PENDING:   "pending",
66
    ProjectApplication.APPROVED:  "approved",
67
    ProjectApplication.REPLACED:  "replaced",
68
    ProjectApplication.DENIED:    "denied",
69
    ProjectApplication.DISMISSED: "dismissed",
70
    ProjectApplication.CANCELLED: "cancelled",
71
}
72

    
73
PROJECT_STATE_SHOW = {
74
    Project.UNINITIALIZED: "uninitialized",
75
    Project.NORMAL:        "active",
76
    Project.SUSPENDED:     "suspended",
77
    Project.TERMINATED:    "terminated",
78
    Project.DELETED:       "deleted",
79
}
80

    
81
PROJECT_STATE = invert_dict(PROJECT_STATE_SHOW)
82

    
83
MEMBERSHIP_STATE_SHOW = {
84
    ProjectMembership.REQUESTED:       "requested",
85
    ProjectMembership.ACCEPTED:        "accepted",
86
    ProjectMembership.LEAVE_REQUESTED: "leave_requested",
87
    ProjectMembership.USER_SUSPENDED:  "suspended",
88
    ProjectMembership.REJECTED:        "rejected",
89
    ProjectMembership.CANCELLED:       "cancelled",
90
    ProjectMembership.REMOVED:         "removed",
91
}
92

    
93

    
94
def _grant_details(grants):
95
    resources = {}
96
    for grant in grants:
97
        if not grant.resource.api_visible:
98
            continue
99
        resources[grant.resource.name] = {
100
            "member_capacity": grant.member_capacity,
101
            "project_capacity": grant.project_capacity,
102
        }
103
    return resources
104

    
105

    
106
def _application_details(application, all_grants):
107
    grants = all_grants.get(application.id, [])
108
    resources = _grant_details(grants)
109
    join_policy = MEMBERSHIP_POLICY_SHOW.get(application.member_join_policy)
110
    leave_policy = MEMBERSHIP_POLICY_SHOW.get(application.member_leave_policy)
111

    
112
    d = {
113
        "id": application.id,
114
        "state": APPLICATION_STATE_SHOW[application.state],
115
        "name": application.name,
116
        "owner": application.owner.uuid if application.owner else None,
117
        "applicant": application.applicant.uuid,
118
        "homepage": application.homepage,
119
        "description": application.description,
120
        "start_date": application.start_date,
121
        "end_date": application.end_date,
122
        "comments": application.comments,
123
        "join_policy": join_policy,
124
        "leave_policy": leave_policy,
125
        "max_members": application.limit_on_members_number,
126
        "private": application.private,
127
        "resources": resources,
128
    }
129
    return d
130

    
131

    
132
def get_projects_details(projects, request_user=None):
133
    applications = [p.last_application for p in projects if p.last_application]
134
    proj_quotas = ProjectResourceQuota.objects.quotas_per_project(projects)
135
    app_grants = ProjectResourceGrant.objects.grants_per_app(applications)
136
    deactivations = ProjectLog.objects.last_deactivations(projects)
137

    
138
    l = []
139
    for project in projects:
140
        join_policy = MEMBERSHIP_POLICY_SHOW[project.member_join_policy]
141
        leave_policy = MEMBERSHIP_POLICY_SHOW[project.member_leave_policy]
142
        quotas = proj_quotas.get(project.id, [])
143
        resources = _grant_details(quotas)
144

    
145
        d = {
146
            "id": project.uuid,
147
            "state": PROJECT_STATE_SHOW[project.state],
148
            "creation_date": project.creation_date,
149
            "name": project.realname,
150
            "owner": project.owner.uuid if project.owner else None,
151
            "homepage": project.homepage,
152
            "description": project.description,
153
            "end_date": project.end_date,
154
            "join_policy": join_policy,
155
            "leave_policy": leave_policy,
156
            "max_members": project.limit_on_members_number,
157
            "private": project.private,
158
            "base_project": project.is_base,
159
            "resources": resources,
160
            }
161

    
162
        check = functions.project_check_allowed
163
        if check(project, request_user,
164
                 level=functions.APPLICANT_LEVEL, silent=True):
165
            application = project.last_application
166
            if application:
167
                d["last_application"] = _application_details(
168
                    application, app_grants)
169
            deact = deactivations.get(project.id)
170
            if deact is not None:
171
                d["deactivation_date"] = deact.date
172
        l.append(d)
173
    return l
174

    
175

    
176
def get_project_details(project, request_user=None):
177
    return get_projects_details([project], request_user=request_user)[0]
178

    
179

    
180
def get_memberships_details(memberships, request_user):
181
    all_logs = ProjectMembershipLog.objects.last_logs(memberships)
182

    
183
    l = []
184
    for membership in memberships:
185
        logs = all_logs.get(membership.id, {})
186
        dates = {}
187
        for s, log in logs.iteritems():
188
            dates[MEMBERSHIP_STATE_SHOW[s]] = log.date
189

    
190
        allowed_actions = functions.membership_allowed_actions(
191
            membership, request_user)
192
        d = {
193
            "id": membership.id,
194
            "user": membership.person.uuid,
195
            "project": membership.project.uuid,
196
            "state": MEMBERSHIP_STATE_SHOW[membership.state],
197
            "allowed_actions": allowed_actions,
198
        }
199
        d.update(dates)
200
        l.append(d)
201
    return l
202

    
203

    
204
def get_membership_details(membership, request_user):
205
    return get_memberships_details([membership], request_user)[0]
206

    
207

    
208
def _query(attr):
209
    def inner(val):
210
        kw = attr + "__in" if isinstance(val, list) else attr
211
        return Q(**{kw: val})
212
    return inner
213

    
214

    
215
def _get_project_state(val):
216
    try:
217
        return PROJECT_STATE[val]
218
    except KeyError:
219
        raise faults.BadRequest("Unrecognized state %s" % val)
220

    
221

    
222
def _project_state_query(val):
223
    if isinstance(val, list):
224
        states = [_get_project_state(v) for v in val]
225
        return Q(state__in=states)
226
    return Q(state=_get_project_state(val))
227

    
228

    
229
PROJECT_QUERY = {
230
    "name": _query("realname"),
231
    "owner": _query("owner__uuid"),
232
    "state": _project_state_query,
233
}
234

    
235

    
236
def make_project_query(filters):
237
    qs = Q()
238
    for attr, val in filters.iteritems():
239
        try:
240
            _q = PROJECT_QUERY[attr]
241
        except KeyError:
242
            raise faults.BadRequest("Unrecognized filter %s" % attr)
243
        qs &= _q(val)
244
    return qs
245

    
246

    
247
class ExceptionHandler(object):
248
    def __enter__(self):
249
        pass
250

    
251
    EXCS = {
252
        functions.ProjectNotFound:   faults.ItemNotFound,
253
        functions.ProjectForbidden:  faults.Forbidden,
254
        functions.ProjectBadRequest: faults.BadRequest,
255
        functions.ProjectConflict:   faults.Conflict,
256
    }
257

    
258
    def __exit__(self, exc_type, value, traceback):
259
        if value is not None:  # exception
260
            try:
261
                e = self.EXCS[exc_type]
262
            except KeyError:
263
                return False  # reraise
264
            raise e(value.message)
265

    
266

    
267
@csrf_exempt
268
def projects(request):
269
    method = request.method
270
    if method == "GET":
271
        return get_projects(request)
272
    elif method == "POST":
273
        return create_project(request)
274
    return api.api_method_not_allowed(request, allowed_methods=['GET', 'POST'])
275

    
276

    
277
@api.api_method(http_method="GET", token_required=True, user_required=False)
278
@user_from_token
279
@transaction.commit_on_success
280
def get_projects(request):
281
    user = request.user
282
    input_data = read_json_body(request, default={})
283
    filters = input_data.get("filter", {})
284
    mode = input_data.get("mode", "default")
285
    query = make_project_query(filters)
286
    projects = _get_projects(query, mode=mode, request_user=user)
287
    data = get_projects_details(projects, request_user=user)
288
    return json_response(data)
289

    
290

    
291
def _get_projects(query, mode="default", request_user=None):
292
    projects = Project.objects.filter(query)
293

    
294
    if mode == "member":
295
        membs = request_user.projectmembership_set.\
296
            actually_accepted_and_active()
297
        memb_projects = membs.values_list("project", flat=True)
298
        is_memb = Q(id__in=memb_projects)
299
        projects = projects.filter(is_memb)
300
    elif mode == "default":
301
        if not request_user.is_project_admin():
302
            membs = request_user.projectmembership_set.any_accepted()
303
            memb_projects = membs.values_list("project", flat=True)
304
            is_memb = Q(id__in=memb_projects)
305
            owned = Q(owner=request_user)
306
            active = (Q(state=Project.NORMAL) &
307
                      Q(private=False))
308
            projects = projects.filter(is_memb | owned | active)
309
    else:
310
        raise faults.BadRequest("Unrecognized mode '%s'." % mode)
311
    return projects.select_related("last_application")
312

    
313

    
314
@api.api_method(http_method="POST", token_required=True, user_required=False)
315
@user_from_token
316
@transaction.commit_on_success
317
def create_project(request):
318
    user = request.user
319
    data = request.body
320
    app_data = json.loads(data)
321
    return submit_new_project(app_data, user)
322

    
323

    
324
@csrf_exempt
325
def project(request, project_id):
326
    method = request.method
327
    if method == "GET":
328
        return get_project(request, project_id)
329
    if method == "POST":
330
        return modify_project(request, project_id)
331
    return api.api_method_not_allowed(request, allowed_methods=['GET', 'POST'])
332

    
333

    
334
@api.api_method(http_method="GET", token_required=True, user_required=False)
335
@user_from_token
336
@transaction.commit_on_success
337
def get_project(request, project_id):
338
    user = request.user
339
    with ExceptionHandler():
340
        project = _get_project(project_id, request_user=user)
341
    data = get_project_details(project, user)
342
    return json_response(data)
343

    
344

    
345
def _get_project(project_id, request_user=None):
346
    project = functions.get_project_by_uuid(project_id)
347
    functions.project_check_allowed(
348
        project, request_user, level=functions.ANY_LEVEL)
349
    return project
350

    
351

    
352
@api.api_method(http_method="POST", token_required=True, user_required=False)
353
@user_from_token
354
@transaction.commit_on_success
355
def modify_project(request, project_id):
356
    user = request.user
357
    data = request.body
358
    app_data = json.loads(data)
359
    return submit_modification(app_data, user, project_id=project_id)
360

    
361

    
362
def _get_date(d, key):
363
    date_str = d.get(key)
364
    if date_str is not None:
365
        try:
366
            return date_util.isoparse(date_str)
367
        except:
368
            raise faults.BadRequest("Invalid %s" % key)
369
    else:
370
        return None
371

    
372

    
373
def _get_maybe_string(d, key, default=None):
374
    value = d.get(key)
375
    if value is not None and not isinstance(value, basestring):
376
        raise faults.BadRequest("%s must be string" % key)
377
    if value is None:
378
        return default
379
    return value
380

    
381

    
382
def _get_maybe_boolean(d, key, default=None):
383
    value = d.get(key)
384
    if value is not None and not isinstance(value, bool):
385
        raise faults.BadRequest("%s must be boolean" % key)
386
    if value is None:
387
        return default
388
    return value
389

    
390

    
391
DOMAIN_VALUE_REGEX = re.compile(
392
    r'^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,6}$',
393
    re.IGNORECASE)
394

    
395

    
396
def valid_project_name(name):
397
    return DOMAIN_VALUE_REGEX.match(name) is not None
398

    
399

    
400
def _parse_max_members(s):
401
    try:
402
        max_members = units.parse(s)
403
        if max_members < 0:
404
            raise faults.BadRequest("Invalid max_members")
405
        return max_members
406
    except units.ParseError:
407
        raise faults.BadRequest("Invalid max_members")
408

    
409

    
410
def submit_new_project(app_data, user):
411
    uuid = app_data.get("owner")
412
    if uuid is None:
413
        owner = user
414
    else:
415
        try:
416
            owner = AstakosUser.objects.accepted().get(uuid=uuid)
417
        except AstakosUser.DoesNotExist:
418
            raise faults.BadRequest("User does not exist.")
419

    
420
    try:
421
        name = app_data["name"]
422
    except KeyError:
423
        raise faults.BadRequest("Name missing.")
424

    
425
    if not valid_project_name(name):
426
        raise faults.BadRequest("Project name should be in domain format")
427

    
428
    join_policy = app_data.get("join_policy", "moderated")
429
    try:
430
        join_policy = MEMBERSHIP_POLICY[join_policy]
431
    except KeyError:
432
        raise faults.BadRequest("Invalid join policy")
433

    
434
    leave_policy = app_data.get("leave_policy", "auto")
435
    try:
436
        leave_policy = MEMBERSHIP_POLICY[leave_policy]
437
    except KeyError:
438
        raise faults.BadRequest("Invalid leave policy")
439

    
440
    start_date = _get_date(app_data, "start_date")
441
    end_date = _get_date(app_data, "end_date")
442

    
443
    if end_date is None:
444
        raise faults.BadRequest("Missing end date")
445

    
446
    try:
447
        max_members = _parse_max_members(app_data["max_members"])
448
    except KeyError:
449
        max_members = units.PRACTICALLY_INFINITE
450

    
451
    private = bool(_get_maybe_boolean(app_data, "private"))
452
    homepage = _get_maybe_string(app_data, "homepage", "")
453
    description = _get_maybe_string(app_data, "description", "")
454
    comments = _get_maybe_string(app_data, "comments", "")
455
    resources = app_data.get("resources", {})
456

    
457
    submit = functions.submit_application
458
    with ExceptionHandler():
459
        application = submit(
460
            owner=owner,
461
            name=name,
462
            project_id=None,
463
            homepage=homepage,
464
            description=description,
465
            start_date=start_date,
466
            end_date=end_date,
467
            member_join_policy=join_policy,
468
            member_leave_policy=leave_policy,
469
            limit_on_members_number=max_members,
470
            private=private,
471
            comments=comments,
472
            resources=resources,
473
            request_user=user)
474

    
475
    result = {"application": application.id,
476
              "id": application.chain.uuid,
477
              }
478
    return json_response(result, status_code=201)
479

    
480

    
481
def submit_modification(app_data, user, project_id):
482
    owner = app_data.get("owner")
483
    if owner is not None:
484
        try:
485
            owner = AstakosUser.objects.accepted().get(uuid=owner)
486
        except AstakosUser.DoesNotExist:
487
            raise faults.BadRequest("User does not exist.")
488

    
489
    name = app_data.get("name")
490

    
491
    if name is not None and not valid_project_name(name):
492
        raise faults.BadRequest("Project name should be in domain format")
493

    
494
    join_policy = app_data.get("join_policy")
495
    if join_policy is not None:
496
        try:
497
            join_policy = MEMBERSHIP_POLICY[join_policy]
498
        except KeyError:
499
            raise faults.BadRequest("Invalid join policy")
500

    
501
    leave_policy = app_data.get("leave_policy")
502
    if leave_policy is not None:
503
        try:
504
            leave_policy = MEMBERSHIP_POLICY[leave_policy]
505
        except KeyError:
506
            raise faults.BadRequest("Invalid leave policy")
507

    
508
    start_date = _get_date(app_data, "start_date")
509
    end_date = _get_date(app_data, "end_date")
510

    
511
    max_members = app_data.get("max_members")
512
    if max_members is not None:
513
        max_members = _parse_max_members(max_members)
514

    
515
    private = _get_maybe_boolean(app_data, "private")
516
    homepage = _get_maybe_string(app_data, "homepage")
517
    description = _get_maybe_string(app_data, "description")
518
    comments = _get_maybe_string(app_data, "comments")
519
    resources = app_data.get("resources", {})
520

    
521
    submit = functions.submit_application
522
    with ExceptionHandler():
523
        application = submit(
524
            owner=owner,
525
            name=name,
526
            project_id=project_id,
527
            homepage=homepage,
528
            description=description,
529
            start_date=start_date,
530
            end_date=end_date,
531
            member_join_policy=join_policy,
532
            member_leave_policy=leave_policy,
533
            limit_on_members_number=max_members,
534
            private=private,
535
            comments=comments,
536
            resources=resources,
537
            request_user=user)
538

    
539
    result = {"application": application.id,
540
              "id": application.chain.uuid,
541
              }
542
    return json_response(result, status_code=201)
543

    
544

    
545
def get_action(actions, input_data):
546
    action = None
547
    data = None
548
    for option in actions.keys():
549
        if option in input_data:
550
            if action:
551
                raise faults.BadRequest("Multiple actions not supported")
552
            else:
553
                action = option
554
                data = input_data[action]
555
    if not action:
556
        raise faults.BadRequest("No recognized action")
557
    return actions[action], data
558

    
559

    
560
PROJECT_ACTION = {
561
    "terminate": functions.terminate,
562
    "suspend":   functions.suspend,
563
    "unsuspend": functions.unsuspend,
564
    "reinstate": functions.reinstate,
565
}
566

    
567

    
568
APPLICATION_ACTION = {
569
    "approve": functions.approve_application,
570
    "deny":    functions.deny_application,
571
    "dismiss": functions.dismiss_application,
572
    "cancel":  functions.cancel_application,
573
}
574

    
575

    
576
PROJECT_ACTION.update(APPLICATION_ACTION)
577
APP_ACTION_FUNCS = APPLICATION_ACTION.values()
578

    
579

    
580
@csrf_exempt
581
@api.api_method(http_method="POST", token_required=True, user_required=False)
582
@user_from_token
583
@transaction.commit_on_success
584
def project_action(request, project_id):
585
    user = request.user
586
    data = request.body
587
    input_data = json.loads(data)
588

    
589
    func, action_data = get_action(PROJECT_ACTION, input_data)
590
    with ExceptionHandler():
591
        kwargs = {"request_user": user,
592
                  "reason": action_data.get("reason", ""),
593
                  }
594
        if func in APP_ACTION_FUNCS:
595
            kwargs["application_id"] = action_data["app_id"]
596
        func(project_id=project_id, **kwargs)
597
    return HttpResponse()
598

    
599

    
600
@csrf_exempt
601
def memberships(request):
602
    method = request.method
603
    if method == "GET":
604
        return get_memberships(request)
605
    elif method == "POST":
606
        return post_memberships(request)
607
    return api.api_method_not_allowed(request, allowed_methods=['GET', 'POST'])
608

    
609

    
610
def make_membership_query(input_data):
611
    project_id = input_data.get("project")
612
    if project_id is not None:
613
        return Q(project__uuid=project_id)
614
    return Q()
615

    
616

    
617
@api.api_method(http_method="GET", token_required=True, user_required=False)
618
@user_from_token
619
@transaction.commit_on_success
620
def get_memberships(request):
621
    user = request.user
622
    input_data = read_json_body(request, default={})
623
    query = make_membership_query(input_data)
624
    memberships = _get_memberships(query, request_user=user)
625
    data = get_memberships_details(memberships, user)
626
    return json_response(data)
627

    
628

    
629
def _get_memberships(query, request_user=None):
630
    memberships = ProjectMembership.objects
631
    if not request_user.is_project_admin():
632
        owned = Q(project__owner=request_user)
633
        memb = Q(person=request_user)
634
        memberships = memberships.filter(owned | memb)
635

    
636
    return memberships.select_related(
637
        "project", "project__owner", "person").filter(query)
638

    
639

    
640
def join_project(data, request_user):
641
    project_id = data.get("project")
642
    with ExceptionHandler():
643
        membership = functions.join_project(project_id, request_user)
644
    response = {"id": membership.id}
645
    return json_response(response)
646

    
647

    
648
def enroll_user(data, request_user):
649
    project_id = data.get("project")
650
    email = data.get("user")
651
    with ExceptionHandler():
652
        m = functions.enroll_member_by_email(
653
            project_id, email, request_user)
654

    
655
    response = {"id": m.id}
656
    return json_response(response)
657

    
658

    
659
MEMBERSHIPS_ACTION = {
660
    "join":   join_project,
661
    "enroll": enroll_user,
662
}
663

    
664

    
665
@api.api_method(http_method="POST", token_required=True, user_required=False)
666
@user_from_token
667
@transaction.commit_on_success
668
def post_memberships(request):
669
    user = request.user
670
    data = request.body
671
    input_data = json.loads(data)
672
    func, action_data = get_action(MEMBERSHIPS_ACTION, input_data)
673
    return func(action_data, user)
674

    
675

    
676
@api.api_method(http_method="GET", token_required=True, user_required=False)
677
@user_from_token
678
@transaction.commit_on_success
679
def membership(request, memb_id):
680
    user = request.user
681
    with ExceptionHandler():
682
        m = _get_membership(memb_id, request_user=user)
683
    data = get_membership_details(m, user)
684
    return json_response(data)
685

    
686

    
687
def _get_membership(memb_id, request_user=None):
688
    membership = functions.get_membership_by_id(memb_id)
689
    functions.membership_check_allowed(membership, request_user)
690
    return membership
691

    
692

    
693
MEMBERSHIP_ACTION = {
694
    "leave":  functions.leave_project,
695
    "cancel": functions.cancel_membership,
696
    "accept": functions.accept_membership,
697
    "reject": functions.reject_membership,
698
    "remove": functions.remove_membership,
699
}
700

    
701

    
702
@csrf_exempt
703
@api.api_method(http_method="POST", token_required=True, user_required=False)
704
@user_from_token
705
@transaction.commit_on_success
706
def membership_action(request, memb_id):
707
    user = request.user
708
    input_data = read_json_body(request, default={})
709
    func, action_data = get_action(MEMBERSHIP_ACTION, input_data)
710
    with ExceptionHandler():
711
        func(memb_id, user, reason=action_data)
712
    return HttpResponse()