Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / api / projects.py @ 2556cf45

History | View | Annotate | Download (20.6 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
from django.utils import simplejson as json
35
from django.views.decorators.csrf import csrf_exempt
36
from django.http import HttpResponse
37
from django.db.models import Q
38

    
39
from snf_django.lib.db.transaction import commit_on_success_strict
40
from astakos.api.util import json_response
41

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

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

    
52

    
53
MEMBERSHIP_POLICY_SHOW = {
54
    functions.AUTO_ACCEPT_POLICY: "auto",
55
    functions.MODERATED_POLICY:   "moderated",
56
    functions.CLOSED_POLICY:      "closed",
57
}
58

    
59
MEMBERSHIP_POLICY = invert_dict(MEMBERSHIP_POLICY_SHOW)
60

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

    
70
PROJECT_STATE_SHOW = {
71
    Project.O_PENDING:    "pending",
72
    Project.O_ACTIVE:     "active",
73
    Project.O_DENIED:     "denied",
74
    Project.O_DISMISSED:  "dismissed",
75
    Project.O_CANCELLED:  "cancelled",
76
    Project.O_SUSPENDED:  "suspended",
77
    Project.O_TERMINATED: "terminated",
78
}
79

    
80
PROJECT_STATE = invert_dict(PROJECT_STATE_SHOW)
81

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

    
92

    
93
def _application_details(application, all_grants):
94
    grants = all_grants.get(application.id, [])
95
    resources = {}
96
    for grant in grants:
97
        resources[grant.resource.name] = {
98
            "member_capacity": grant.member_capacity,
99
            "project_capacity": grant.project_capacity,
100
        }
101

    
102
    join_policy = MEMBERSHIP_POLICY_SHOW[application.member_join_policy]
103
    leave_policy = MEMBERSHIP_POLICY_SHOW[application.member_leave_policy]
104

    
105
    d = {
106
        "name": application.name,
107
        "owner": application.owner.uuid,
108
        "applicant": application.applicant.uuid,
109
        "homepage": application.homepage,
110
        "description": application.description,
111
        "start_date": application.start_date,
112
        "end_date": application.end_date,
113
        "join_policy": join_policy,
114
        "leave_policy": leave_policy,
115
        "max_members": application.limit_on_members_number,
116
        "resources": resources,
117
    }
118
    return d
119

    
120

    
121
def get_applications_details(applications):
122
    grants = ProjectResourceGrant.objects.grants_per_app(applications)
123

    
124
    l = []
125
    for application in applications:
126
        d = {
127
            "id": application.id,
128
            "project": application.chain_id,
129
            "state": APPLICATION_STATE_SHOW[application.state],
130
            "comments": application.comments,
131
        }
132
        d.update(_application_details(application, grants))
133
        l.append(d)
134
    return l
135

    
136

    
137
def get_application_details(application):
138
    return get_applications_details([application])[0]
139

    
140

    
141
def get_projects_details(projects, request_user=None):
142
    pendings = ProjectApplication.objects.pending_per_project(projects)
143
    applications = [p.application for p in projects]
144
    grants = ProjectResourceGrant.objects.grants_per_app(applications)
145
    deactivations = ProjectLog.objects.last_deactivations(projects)
146

    
147
    l = []
148
    for project in projects:
149
        application = project.application
150
        d = {
151
            "id": project.id,
152
            "application": application.id,
153
            "state": PROJECT_STATE_SHOW[project.overall_state()],
154
            "creation_date": project.creation_date,
155
        }
156
        check = functions.project_check_allowed
157
        if check(project, request_user,
158
                 level=functions.APPLICANT_LEVEL, silent=True):
159
            d["comments"] = application.comments
160
            pending = pendings.get(project.id)
161
            d["pending_application"] = pending.id if pending else None
162
            deact = deactivations.get(project.id)
163
            if deact is not None:
164
                d["deactivation_date"] = deact.date
165
        d.update(_application_details(application, grants))
166
        l.append(d)
167
    return l
168

    
169

    
170
def get_project_details(project, request_user=None):
171
    return get_projects_details([project], request_user=request_user)[0]
172

    
173

    
174
def get_memberships_details(memberships, request_user):
175
    all_logs = ProjectMembershipLog.objects.last_logs(memberships)
176

    
177
    l = []
178
    for membership in memberships:
179
        logs = all_logs.get(membership.id, {})
180
        dates = {}
181
        for s, log in logs.iteritems():
182
            dates[MEMBERSHIP_STATE_SHOW[s]] = log.date
183

    
184
        allowed_actions = functions.membership_allowed_actions(
185
            membership, request_user)
186
        d = {
187
            "id": membership.id,
188
            "user": membership.person.uuid,
189
            "project": membership.project_id,
190
            "state": MEMBERSHIP_STATE_SHOW[membership.state],
191
            "allowed_actions": allowed_actions,
192
        }
193
        d.update(dates)
194
        l.append(d)
195
    return l
196

    
197

    
198
def get_membership_details(membership, request_user):
199
    return get_memberships_details([membership], request_user)[0]
200

    
201

    
202
def _query(attr):
203
    def inner(val):
204
        kw = attr + "__in" if isinstance(val, list) else attr
205
        return Q(**{kw: val})
206
    return inner
207

    
208

    
209
def _get_project_state(val):
210
    try:
211
        return PROJECT_STATE[val]
212
    except KeyError:
213
        raise faults.BadRequest("Unrecognized state %s" % val)
214

    
215

    
216
def _project_state_query(val):
217
    if isinstance(val, list):
218
        states = [_get_project_state(v) for v in val]
219
        return Project.o_states_q(states)
220
    return Project.o_state_q(_get_project_state(val))
221

    
222

    
223
PROJECT_QUERY = {
224
    "name": _query("application__name"),
225
    "owner": _query("application__owner__uuid"),
226
    "state": _project_state_query,
227
}
228

    
229

    
230
def make_project_query(filters):
231
    qs = Q()
232
    for attr, val in filters.iteritems():
233
        try:
234
            _q = PROJECT_QUERY[attr]
235
        except KeyError:
236
            raise faults.BadRequest("Unrecognized filter %s" % attr)
237
        qs &= _q(val)
238
    return qs
239

    
240

    
241
class ExceptionHandler(object):
242
    def __enter__(self):
243
        pass
244

    
245
    EXCS = {
246
        functions.ProjectNotFound:   faults.ItemNotFound,
247
        functions.ProjectForbidden:  faults.Forbidden,
248
        functions.ProjectBadRequest: faults.BadRequest,
249
        functions.ProjectConflict:   faults.Conflict,
250
    }
251

    
252
    def __exit__(self, exc_type, value, traceback):
253
        if value is not None:  # exception
254
            try:
255
                e = self.EXCS[exc_type]
256
            except KeyError:
257
                return False  # reraise
258
            raise e(value.message)
259

    
260

    
261
@csrf_exempt
262
def projects(request):
263
    method = request.method
264
    if method == "GET":
265
        return get_projects(request)
266
    elif method == "POST":
267
        return create_project(request)
268
    return api.api_method_not_allowed(request)
269

    
270

    
271
@api.api_method(http_method="GET", token_required=True, user_required=False)
272
@user_from_token
273
@commit_on_success_strict()
274
def get_projects(request):
275
    user = request.user
276
    input_data = read_json_body(request, default={})
277
    filters = input_data.get("filter", {})
278
    query = make_project_query(filters)
279
    projects = _get_projects(query, request_user=user)
280
    data = get_projects_details(projects, request_user=user)
281
    return json_response(data)
282

    
283

    
284
def _get_projects(query, request_user=None):
285
    projects = Project.objects.filter(query)
286

    
287
    if not request_user.is_project_admin():
288
        membs = request_user.projectmembership_set.any_accepted()
289
        memb_projects = membs.values_list("project", flat=True)
290
        is_memb = Q(id__in=memb_projects)
291
        owned = (Q(application__owner=request_user) |
292
                 Q(application__applicant=request_user))
293
        active = Project.o_state_q(Project.O_ACTIVE)
294
        projects = projects.filter(is_memb | owned | active)
295
    return projects.select_related(
296
        "application", "application__owner", "application__applicant")
297

    
298

    
299
@api.api_method(http_method="POST", token_required=True, user_required=False)
300
@user_from_token
301
@commit_on_success_strict()
302
def create_project(request):
303
    user = request.user
304
    data = request.raw_post_data
305
    app_data = json.loads(data)
306
    return submit_application(app_data, user, project_id=None)
307

    
308

    
309
@csrf_exempt
310
def project(request, project_id):
311
    method = request.method
312
    if method == "GET":
313
        return get_project(request, project_id)
314
    if method == "POST":
315
        return modify_project(request, project_id)
316
    return api.api_method_not_allowed(request)
317

    
318

    
319
@api.api_method(http_method="GET", token_required=True, user_required=False)
320
@user_from_token
321
@commit_on_success_strict()
322
def get_project(request, project_id):
323
    user = request.user
324
    with ExceptionHandler():
325
        project = _get_project(project_id, request_user=user)
326
    data = get_project_details(project, user)
327
    return json_response(data)
328

    
329

    
330
def _get_project(project_id, request_user=None):
331
    project = functions.get_project_by_id(project_id)
332
    functions.project_check_allowed(
333
        project, request_user, level=functions.ANY_LEVEL)
334
    return project
335

    
336

    
337
@api.api_method(http_method="POST", token_required=True, user_required=False)
338
@user_from_token
339
@commit_on_success_strict()
340
def modify_project(request, project_id):
341
    user = request.user
342
    data = request.raw_post_data
343
    app_data = json.loads(data)
344
    return submit_application(app_data, user, project_id=project_id)
345

    
346

    
347
def _get_date(d, key):
348
    date_str = d.get(key)
349
    if date_str is not None:
350
        try:
351
            return date_util.isoparse(date_str)
352
        except:
353
            raise faults.BadRequest("Invalid %s" % key)
354
    else:
355
        return None
356

    
357

    
358
def _get_maybe_string(d, key):
359
    value = d.get(key)
360
    if value is not None and not isinstance(value, basestring):
361
        raise faults.BadRequest("%s must be string" % key)
362
    return value
363

    
364

    
365
def submit_application(app_data, user, project_id=None):
366
    uuid = app_data.get("owner")
367
    if uuid is None:
368
        owner = user
369
    else:
370
        try:
371
            owner = AstakosUser.objects.get(uuid=uuid, email_verified=True)
372
        except AstakosUser.DoesNotExist:
373
            raise faults.BadRequest("User does not exist.")
374

    
375
    try:
376
        name = app_data["name"]
377
    except KeyError:
378
        raise faults.BadRequest("Name missing.")
379

    
380
    join_policy = app_data.get("join_policy", "moderated")
381
    try:
382
        join_policy = MEMBERSHIP_POLICY[join_policy]
383
    except KeyError:
384
        raise faults.BadRequest("Invalid join policy")
385

    
386
    leave_policy = app_data.get("leave_policy", "auto")
387
    try:
388
        leave_policy = MEMBERSHIP_POLICY[leave_policy]
389
    except KeyError:
390
        raise faults.BadRequest("Invalid leave policy")
391

    
392
    start_date = _get_date(app_data, "start_date")
393
    end_date = _get_date(app_data, "end_date")
394

    
395
    if end_date is None:
396
        raise faults.BadRequest("Missing end date")
397

    
398
    max_members = app_data.get("max_members")
399
    if max_members is not None and \
400
            (not isinstance(max_members, (int, long)) or max_members < 0):
401
        raise faults.BadRequest("Invalid max_members")
402

    
403
    homepage = _get_maybe_string(app_data, "homepage")
404
    description = _get_maybe_string(app_data, "description")
405
    comments = _get_maybe_string(app_data, "comments")
406
    resources = app_data.get("resources", {})
407

    
408
    submit = functions.submit_application
409
    with ExceptionHandler():
410
        application = submit(
411
            owner=owner,
412
            name=name,
413
            project_id=project_id,
414
            homepage=homepage,
415
            description=description,
416
            start_date=start_date,
417
            end_date=end_date,
418
            member_join_policy=join_policy,
419
            member_leave_policy=leave_policy,
420
            limit_on_members_number=max_members,
421
            comments=comments,
422
            resources=resources,
423
            request_user=user)
424

    
425
    result = {"application": application.id,
426
              "id": application.chain_id
427
              }
428
    return json_response(result, status_code=201)
429

    
430

    
431
def get_action(actions, input_data):
432
    action = None
433
    data = None
434
    for option in actions.keys():
435
        if option in input_data:
436
            if action:
437
                raise faults.BadRequest("Multiple actions not supported")
438
            else:
439
                action = option
440
                data = input_data[action]
441
    if not action:
442
        raise faults.BadRequest("No recognized action")
443
    return actions[action], data
444

    
445

    
446
PROJECT_ACTION = {
447
    "terminate": functions.terminate,
448
    "suspend":   functions.suspend,
449
    "unsuspend": functions.unsuspend,
450
    "reinstate": functions.reinstate,
451
}
452

    
453

    
454
@csrf_exempt
455
@api.api_method(http_method="POST", token_required=True, user_required=False)
456
@user_from_token
457
@commit_on_success_strict()
458
def project_action(request, project_id):
459
    user = request.user
460
    data = request.raw_post_data
461
    input_data = json.loads(data)
462

    
463
    func, action_data = get_action(PROJECT_ACTION, input_data)
464
    with ExceptionHandler():
465
        func(project_id, request_user=user, reason=action_data)
466
    return HttpResponse()
467

    
468

    
469
@csrf_exempt
470
def applications(request):
471
    method = request.method
472
    if method == "GET":
473
        return get_applications(request)
474
    return api.api_method_not_allowed(request)
475

    
476

    
477
def make_application_query(input_data):
478
    project_id = input_data.get("project")
479
    if project_id is not None:
480
        if not isinstance(project_id, (int, long)):
481
            raise faults.BadRequest("'project' must be integer")
482
        return Q(chain=project_id)
483
    return Q()
484

    
485

    
486
@api.api_method(http_method="GET", token_required=True, user_required=False)
487
@user_from_token
488
@commit_on_success_strict()
489
def get_applications(request):
490
    user = request.user
491
    input_data = read_json_body(request, default={})
492
    query = make_application_query(input_data)
493
    apps = _get_applications(query, request_user=user)
494
    data = get_applications_details(apps)
495
    return json_response(data)
496

    
497

    
498
def _get_applications(query, request_user=None):
499
    apps = ProjectApplication.objects.filter(query)
500

    
501
    if not request_user.is_project_admin():
502
        owned = (Q(owner=request_user) |
503
                 Q(applicant=request_user))
504
        apps = apps.filter(owned)
505
    return apps.select_related()
506

    
507

    
508
@csrf_exempt
509
@api.api_method(http_method="GET", token_required=True, user_required=False)
510
@user_from_token
511
@commit_on_success_strict()
512
def application(request, app_id):
513
    user = request.user
514
    with ExceptionHandler():
515
        application = _get_application(app_id, user)
516
    data = get_application_details(application)
517
    return json_response(data)
518

    
519

    
520
def _get_application(app_id, request_user=None):
521
    application = functions.get_application(app_id)
522
    functions.app_check_allowed(
523
        application, request_user, level=functions.APPLICANT_LEVEL)
524
    return application
525

    
526

    
527
APPLICATION_ACTION = {
528
    "approve": functions.approve_application,
529
    "deny": functions.deny_application,
530
    "dismiss": functions.dismiss_application,
531
    "cancel": functions.cancel_application,
532
}
533

    
534

    
535
@csrf_exempt
536
@api.api_method(http_method="POST", token_required=True, user_required=False)
537
@user_from_token
538
@commit_on_success_strict()
539
def application_action(request, app_id):
540
    user = request.user
541
    data = request.raw_post_data
542
    input_data = json.loads(data)
543

    
544
    func, action_data = get_action(APPLICATION_ACTION, input_data)
545
    with ExceptionHandler():
546
        func(app_id, request_user=user, reason=action_data)
547

    
548
    return HttpResponse()
549

    
550

    
551
@csrf_exempt
552
def memberships(request):
553
    method = request.method
554
    if method == "GET":
555
        return get_memberships(request)
556
    elif method == "POST":
557
        return post_memberships(request)
558
    return api.api_method_not_allowed(request)
559

    
560

    
561
def make_membership_query(input_data):
562
    project_id = input_data.get("project")
563
    if project_id is not None:
564
        if not isinstance(project_id, (int, long)):
565
            raise faults.BadRequest("'project' must be integer")
566
        return Q(project=project_id)
567
    return Q()
568

    
569

    
570
@api.api_method(http_method="GET", token_required=True, user_required=False)
571
@user_from_token
572
@commit_on_success_strict()
573
def get_memberships(request):
574
    user = request.user
575
    input_data = read_json_body(request, default={})
576
    query = make_membership_query(input_data)
577
    memberships = _get_memberships(query, request_user=user)
578
    data = get_memberships_details(memberships, user)
579
    return json_response(data)
580

    
581

    
582
def _get_memberships(query, request_user=None):
583
    memberships = ProjectMembership.objects
584
    if not request_user.is_project_admin():
585
        owned = Q(project__application__owner=request_user)
586
        memb = Q(person=request_user)
587
        memberships = memberships.filter(owned | memb)
588

    
589
    return memberships.select_related(
590
        "project", "project__application",
591
        "project__application__owner", "project__application__applicant",
592
        "person").filter(query)
593

    
594

    
595
def join_project(data, request_user):
596
    project_id = data.get("project")
597
    with ExceptionHandler():
598
        membership = functions.join_project(project_id, request_user)
599
    response = {"id": membership.id}
600
    return json_response(response)
601

    
602

    
603
def enroll_user(data, request_user):
604
    project_id = data.get("project")
605
    email = data.get("user")
606
    with ExceptionHandler():
607
        m = functions.enroll_member_by_email(
608
            project_id, email, request_user)
609

    
610
    response = {"id": m.id}
611
    return json_response(response)
612

    
613

    
614
MEMBERSHIPS_ACTION = {
615
    "join":   join_project,
616
    "enroll": enroll_user,
617
}
618

    
619

    
620
@api.api_method(http_method="POST", token_required=True, user_required=False)
621
@user_from_token
622
@commit_on_success_strict()
623
def post_memberships(request):
624
    user = request.user
625
    data = request.raw_post_data
626
    input_data = json.loads(data)
627
    func, action_data = get_action(MEMBERSHIPS_ACTION, input_data)
628
    return func(action_data, user)
629

    
630

    
631
@api.api_method(http_method="GET", token_required=True, user_required=False)
632
@user_from_token
633
@commit_on_success_strict()
634
def membership(request, memb_id):
635
    user = request.user
636
    with ExceptionHandler():
637
        m = _get_membership(memb_id, request_user=user)
638
    data = get_membership_details(m, user)
639
    return json_response(data)
640

    
641

    
642
def _get_membership(memb_id, request_user=None):
643
    membership = functions.get_membership_by_id(memb_id)
644
    functions.membership_check_allowed(membership, request_user)
645
    return membership
646

    
647

    
648
MEMBERSHIP_ACTION = {
649
    "leave":  functions.leave_project,
650
    "cancel": functions.cancel_membership,
651
    "accept": functions.accept_membership,
652
    "reject": functions.reject_membership,
653
    "remove": functions.remove_membership,
654
}
655

    
656

    
657
@csrf_exempt
658
@api.api_method(http_method="POST", token_required=True, user_required=False)
659
@user_from_token
660
@commit_on_success_strict()
661
def membership_action(request, memb_id):
662
    user = request.user
663
    input_data = read_json_body(request, default={})
664
    func, action_data = get_action(MEMBERSHIP_ACTION, input_data)
665
    with ExceptionHandler():
666
        func(memb_id, user, reason=action_data)
667
    return HttpResponse()