Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (20.9 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
    ProjectResourceGrant, ProjectLog, ProjectMembershipLog)
51
import synnefo.util.date as date_util
52

    
53

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

    
60
MEMBERSHIP_POLICY = invert_dict(MEMBERSHIP_POLICY_SHOW)
61

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

    
71
PROJECT_STATE_SHOW = {
72
    Project.O_PENDING:    "pending",
73
    Project.O_ACTIVE:     "active",
74
    Project.O_DENIED:     "denied",
75
    Project.O_DISMISSED:  "dismissed",
76
    Project.O_CANCELLED:  "cancelled",
77
    Project.O_SUSPENDED:  "suspended",
78
    Project.O_TERMINATED: "terminated",
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 _application_details(application, all_grants):
95
    grants = all_grants.get(application.id, [])
96
    resources = {}
97
    for grant in grants:
98
        resources[grant.resource.name] = {
99
            "member_capacity": grant.member_capacity,
100
            "project_capacity": grant.project_capacity,
101
        }
102

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

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

    
121

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

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

    
137

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

    
141

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

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

    
170

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

    
174

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

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

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

    
198

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

    
202

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

    
209

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

    
216

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

    
223

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

    
230

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

    
241

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

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

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

    
261

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

    
271

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

    
284

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

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

    
299

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

    
309

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

    
319

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

    
330

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

    
337

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

    
347

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

    
358

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

    
365

    
366
DOMAIN_VALUE_REGEX = re.compile(
367
    r'^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,6}$',
368
    re.IGNORECASE)
369

    
370

    
371
def valid_project_name(name):
372
    return DOMAIN_VALUE_REGEX.match(name) is not None
373

    
374

    
375
def submit_application(app_data, user, project_id=None):
376
    uuid = app_data.get("owner")
377
    if uuid is None:
378
        owner = user
379
    else:
380
        try:
381
            owner = AstakosUser.objects.get(uuid=uuid, email_verified=True)
382
        except AstakosUser.DoesNotExist:
383
            raise faults.BadRequest("User does not exist.")
384

    
385
    try:
386
        name = app_data["name"]
387
    except KeyError:
388
        raise faults.BadRequest("Name missing.")
389

    
390
    if not valid_project_name(name):
391
        raise faults.BadRequest("Project name should be in domain format")
392

    
393
    join_policy = app_data.get("join_policy", "moderated")
394
    try:
395
        join_policy = MEMBERSHIP_POLICY[join_policy]
396
    except KeyError:
397
        raise faults.BadRequest("Invalid join policy")
398

    
399
    leave_policy = app_data.get("leave_policy", "auto")
400
    try:
401
        leave_policy = MEMBERSHIP_POLICY[leave_policy]
402
    except KeyError:
403
        raise faults.BadRequest("Invalid leave policy")
404

    
405
    start_date = _get_date(app_data, "start_date")
406
    end_date = _get_date(app_data, "end_date")
407

    
408
    if end_date is None:
409
        raise faults.BadRequest("Missing end date")
410

    
411
    max_members = app_data.get("max_members")
412
    if not isinstance(max_members, (int, long)) or max_members < 0:
413
        raise faults.BadRequest("Invalid max_members")
414

    
415
    homepage = _get_maybe_string(app_data, "homepage")
416
    description = _get_maybe_string(app_data, "description")
417
    comments = _get_maybe_string(app_data, "comments")
418
    resources = app_data.get("resources", {})
419

    
420
    submit = functions.submit_application
421
    with ExceptionHandler():
422
        application = submit(
423
            owner=owner,
424
            name=name,
425
            project_id=project_id,
426
            homepage=homepage,
427
            description=description,
428
            start_date=start_date,
429
            end_date=end_date,
430
            member_join_policy=join_policy,
431
            member_leave_policy=leave_policy,
432
            limit_on_members_number=max_members,
433
            comments=comments,
434
            resources=resources,
435
            request_user=user)
436

    
437
    result = {"application": application.id,
438
              "id": application.chain_id
439
              }
440
    return json_response(result, status_code=201)
441

    
442

    
443
def get_action(actions, input_data):
444
    action = None
445
    data = None
446
    for option in actions.keys():
447
        if option in input_data:
448
            if action:
449
                raise faults.BadRequest("Multiple actions not supported")
450
            else:
451
                action = option
452
                data = input_data[action]
453
    if not action:
454
        raise faults.BadRequest("No recognized action")
455
    return actions[action], data
456

    
457

    
458
PROJECT_ACTION = {
459
    "terminate": functions.terminate,
460
    "suspend":   functions.suspend,
461
    "unsuspend": functions.unsuspend,
462
    "reinstate": functions.reinstate,
463
}
464

    
465

    
466
@csrf_exempt
467
@api.api_method(http_method="POST", token_required=True, user_required=False)
468
@user_from_token
469
@transaction.commit_on_success
470
def project_action(request, project_id):
471
    user = request.user
472
    data = request.body
473
    input_data = json.loads(data)
474

    
475
    func, action_data = get_action(PROJECT_ACTION, input_data)
476
    with ExceptionHandler():
477
        func(project_id, request_user=user, reason=action_data)
478
    return HttpResponse()
479

    
480

    
481
@csrf_exempt
482
def applications(request):
483
    method = request.method
484
    if method == "GET":
485
        return get_applications(request)
486
    return api.api_method_not_allowed(request)
487

    
488

    
489
def make_application_query(input_data):
490
    project_id = input_data.get("project")
491
    if project_id is not None:
492
        if not isinstance(project_id, (int, long)):
493
            raise faults.BadRequest("'project' must be integer")
494
        return Q(chain=project_id)
495
    return Q()
496

    
497

    
498
@api.api_method(http_method="GET", token_required=True, user_required=False)
499
@user_from_token
500
@transaction.commit_on_success
501
def get_applications(request):
502
    user = request.user
503
    input_data = read_json_body(request, default={})
504
    query = make_application_query(input_data)
505
    apps = _get_applications(query, request_user=user)
506
    data = get_applications_details(apps)
507
    return json_response(data)
508

    
509

    
510
def _get_applications(query, request_user=None):
511
    apps = ProjectApplication.objects.filter(query)
512

    
513
    if not request_user.is_project_admin():
514
        owned = (Q(owner=request_user) |
515
                 Q(applicant=request_user))
516
        apps = apps.filter(owned)
517
    return apps.select_related()
518

    
519

    
520
@csrf_exempt
521
@api.api_method(http_method="GET", token_required=True, user_required=False)
522
@user_from_token
523
@transaction.commit_on_success
524
def application(request, app_id):
525
    user = request.user
526
    with ExceptionHandler():
527
        application = _get_application(app_id, user)
528
    data = get_application_details(application)
529
    return json_response(data)
530

    
531

    
532
def _get_application(app_id, request_user=None):
533
    application = functions.get_application(app_id)
534
    functions.app_check_allowed(
535
        application, request_user, level=functions.APPLICANT_LEVEL)
536
    return application
537

    
538

    
539
APPLICATION_ACTION = {
540
    "approve": functions.approve_application,
541
    "deny": functions.deny_application,
542
    "dismiss": functions.dismiss_application,
543
    "cancel": functions.cancel_application,
544
}
545

    
546

    
547
@csrf_exempt
548
@api.api_method(http_method="POST", token_required=True, user_required=False)
549
@user_from_token
550
@transaction.commit_on_success
551
def application_action(request, app_id):
552
    user = request.user
553
    data = request.body
554
    input_data = json.loads(data)
555

    
556
    func, action_data = get_action(APPLICATION_ACTION, input_data)
557
    with ExceptionHandler():
558
        func(app_id, request_user=user, reason=action_data)
559

    
560
    return HttpResponse()
561

    
562

    
563
@csrf_exempt
564
def memberships(request):
565
    method = request.method
566
    if method == "GET":
567
        return get_memberships(request)
568
    elif method == "POST":
569
        return post_memberships(request)
570
    return api.api_method_not_allowed(request)
571

    
572

    
573
def make_membership_query(input_data):
574
    project_id = input_data.get("project")
575
    if project_id is not None:
576
        if not isinstance(project_id, (int, long)):
577
            raise faults.BadRequest("'project' must be integer")
578
        return Q(project=project_id)
579
    return Q()
580

    
581

    
582
@api.api_method(http_method="GET", token_required=True, user_required=False)
583
@user_from_token
584
@transaction.commit_on_success
585
def get_memberships(request):
586
    user = request.user
587
    input_data = read_json_body(request, default={})
588
    query = make_membership_query(input_data)
589
    memberships = _get_memberships(query, request_user=user)
590
    data = get_memberships_details(memberships, user)
591
    return json_response(data)
592

    
593

    
594
def _get_memberships(query, request_user=None):
595
    memberships = ProjectMembership.objects
596
    if not request_user.is_project_admin():
597
        owned = Q(project__application__owner=request_user)
598
        memb = Q(person=request_user)
599
        memberships = memberships.filter(owned | memb)
600

    
601
    return memberships.select_related(
602
        "project", "project__application",
603
        "project__application__owner", "project__application__applicant",
604
        "person").filter(query)
605

    
606

    
607
def join_project(data, request_user):
608
    project_id = data.get("project")
609
    with ExceptionHandler():
610
        membership = functions.join_project(project_id, request_user)
611
    response = {"id": membership.id}
612
    return json_response(response)
613

    
614

    
615
def enroll_user(data, request_user):
616
    project_id = data.get("project")
617
    email = data.get("user")
618
    with ExceptionHandler():
619
        m = functions.enroll_member_by_email(
620
            project_id, email, request_user)
621

    
622
    response = {"id": m.id}
623
    return json_response(response)
624

    
625

    
626
MEMBERSHIPS_ACTION = {
627
    "join":   join_project,
628
    "enroll": enroll_user,
629
}
630

    
631

    
632
@api.api_method(http_method="POST", token_required=True, user_required=False)
633
@user_from_token
634
@transaction.commit_on_success
635
def post_memberships(request):
636
    user = request.user
637
    data = request.body
638
    input_data = json.loads(data)
639
    func, action_data = get_action(MEMBERSHIPS_ACTION, input_data)
640
    return func(action_data, user)
641

    
642

    
643
@api.api_method(http_method="GET", token_required=True, user_required=False)
644
@user_from_token
645
@transaction.commit_on_success
646
def membership(request, memb_id):
647
    user = request.user
648
    with ExceptionHandler():
649
        m = _get_membership(memb_id, request_user=user)
650
    data = get_membership_details(m, user)
651
    return json_response(data)
652

    
653

    
654
def _get_membership(memb_id, request_user=None):
655
    membership = functions.get_membership_by_id(memb_id)
656
    functions.membership_check_allowed(membership, request_user)
657
    return membership
658

    
659

    
660
MEMBERSHIP_ACTION = {
661
    "leave":  functions.leave_project,
662
    "cancel": functions.cancel_membership,
663
    "accept": functions.accept_membership,
664
    "reject": functions.reject_membership,
665
    "remove": functions.remove_membership,
666
}
667

    
668

    
669
@csrf_exempt
670
@api.api_method(http_method="POST", token_required=True, user_required=False)
671
@user_from_token
672
@transaction.commit_on_success
673
def membership_action(request, memb_id):
674
    user = request.user
675
    input_data = read_json_body(request, default={})
676
    func, action_data = get_action(MEMBERSHIP_ACTION, input_data)
677
    with ExceptionHandler():
678
        func(memb_id, user, reason=action_data)
679
    return HttpResponse()