Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / api / projects.py @ 62d30634

History | View | Annotate | Download (21.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
    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
        if not grant.resource.api_visible:
99
            continue
100
        resources[grant.resource.name] = {
101
            "member_capacity": grant.member_capacity,
102
            "project_capacity": grant.project_capacity,
103
        }
104

    
105
    join_policy = MEMBERSHIP_POLICY_SHOW[application.member_join_policy]
106
    leave_policy = MEMBERSHIP_POLICY_SHOW[application.member_leave_policy]
107

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

    
123

    
124
def get_applications_details(applications):
125
    grants = ProjectResourceGrant.objects.grants_per_app(applications)
126

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

    
139

    
140
def get_application_details(application):
141
    return get_applications_details([application])[0]
142

    
143

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

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

    
172

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

    
176

    
177
def get_memberships_details(memberships, request_user):
178
    all_logs = ProjectMembershipLog.objects.last_logs(memberships)
179

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

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

    
200

    
201
def get_membership_details(membership, request_user):
202
    return get_memberships_details([membership], request_user)[0]
203

    
204

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

    
211

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

    
218

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

    
225

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

    
232

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

    
243

    
244
class ExceptionHandler(object):
245
    def __enter__(self):
246
        pass
247

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

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

    
263

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

    
273

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

    
286

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

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

    
302

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

    
312

    
313
@csrf_exempt
314
def project(request, project_id):
315
    method = request.method
316
    if method == "GET":
317
        return get_project(request, project_id)
318
    if method == "POST":
319
        return modify_project(request, project_id)
320
    return api.api_method_not_allowed(request, allowed_methods=['GET', 'POST'])
321

    
322

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

    
333

    
334
def _get_project(project_id, request_user=None):
335
    project = functions.get_project_by_id(project_id)
336
    functions.project_check_allowed(
337
        project, request_user, level=functions.ANY_LEVEL)
338
    return project
339

    
340

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

    
350

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

    
361

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

    
368

    
369
def _get_maybe_boolean(d, key):
370
    value = d.get(key)
371
    if value is not None and not isinstance(value, bool):
372
        raise faults.BadRequest("%s must be boolean" % key)
373
    return value
374

    
375

    
376
DOMAIN_VALUE_REGEX = re.compile(
377
    r'^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,6}$',
378
    re.IGNORECASE)
379

    
380

    
381
def valid_project_name(name):
382
    return DOMAIN_VALUE_REGEX.match(name) is not None
383

    
384

    
385
def submit_application(app_data, user, project_id=None):
386
    uuid = app_data.get("owner")
387
    if uuid is None:
388
        owner = user
389
    else:
390
        try:
391
            owner = AstakosUser.objects.accepted().get(uuid=uuid)
392
        except AstakosUser.DoesNotExist:
393
            raise faults.BadRequest("User does not exist.")
394

    
395
    try:
396
        name = app_data["name"]
397
    except KeyError:
398
        raise faults.BadRequest("Name missing.")
399

    
400
    if not valid_project_name(name):
401
        raise faults.BadRequest("Project name should be in domain format")
402

    
403
    join_policy = app_data.get("join_policy", "moderated")
404
    try:
405
        join_policy = MEMBERSHIP_POLICY[join_policy]
406
    except KeyError:
407
        raise faults.BadRequest("Invalid join policy")
408

    
409
    leave_policy = app_data.get("leave_policy", "auto")
410
    try:
411
        leave_policy = MEMBERSHIP_POLICY[leave_policy]
412
    except KeyError:
413
        raise faults.BadRequest("Invalid leave policy")
414

    
415
    start_date = _get_date(app_data, "start_date")
416
    end_date = _get_date(app_data, "end_date")
417

    
418
    if end_date is None:
419
        raise faults.BadRequest("Missing end date")
420

    
421
    max_members = app_data.get("max_members")
422
    if not isinstance(max_members, (int, long)) or max_members < 0:
423
        raise faults.BadRequest("Invalid max_members")
424

    
425
    private = bool(_get_maybe_boolean(app_data, "private"))
426
    homepage = _get_maybe_string(app_data, "homepage")
427
    description = _get_maybe_string(app_data, "description")
428
    comments = _get_maybe_string(app_data, "comments")
429
    resources = app_data.get("resources", {})
430

    
431
    submit = functions.submit_application
432
    with ExceptionHandler():
433
        application = submit(
434
            owner=owner,
435
            name=name,
436
            project_id=project_id,
437
            homepage=homepage,
438
            description=description,
439
            start_date=start_date,
440
            end_date=end_date,
441
            member_join_policy=join_policy,
442
            member_leave_policy=leave_policy,
443
            limit_on_members_number=max_members,
444
            private=private,
445
            comments=comments,
446
            resources=resources,
447
            request_user=user)
448

    
449
    result = {"application": application.id,
450
              "id": application.chain_id
451
              }
452
    return json_response(result, status_code=201)
453

    
454

    
455
def get_action(actions, input_data):
456
    action = None
457
    data = None
458
    for option in actions.keys():
459
        if option in input_data:
460
            if action:
461
                raise faults.BadRequest("Multiple actions not supported")
462
            else:
463
                action = option
464
                data = input_data[action]
465
    if not action:
466
        raise faults.BadRequest("No recognized action")
467
    return actions[action], data
468

    
469

    
470
PROJECT_ACTION = {
471
    "terminate": functions.terminate,
472
    "suspend":   functions.suspend,
473
    "unsuspend": functions.unsuspend,
474
    "reinstate": functions.reinstate,
475
}
476

    
477

    
478
@csrf_exempt
479
@api.api_method(http_method="POST", token_required=True, user_required=False)
480
@user_from_token
481
@transaction.commit_on_success
482
def project_action(request, project_id):
483
    user = request.user
484
    data = request.body
485
    input_data = json.loads(data)
486

    
487
    func, action_data = get_action(PROJECT_ACTION, input_data)
488
    with ExceptionHandler():
489
        func(project_id, request_user=user, reason=action_data)
490
    return HttpResponse()
491

    
492

    
493
@csrf_exempt
494
def applications(request):
495
    method = request.method
496
    if method == "GET":
497
        return get_applications(request)
498
    return api.api_method_not_allowed(request, allowed_methods=['GET'])
499

    
500

    
501
def make_application_query(input_data):
502
    project_id = input_data.get("project")
503
    if project_id is not None:
504
        if not isinstance(project_id, (int, long)):
505
            raise faults.BadRequest("'project' must be integer")
506
        return Q(chain=project_id)
507
    return Q()
508

    
509

    
510
@api.api_method(http_method="GET", token_required=True, user_required=False)
511
@user_from_token
512
@transaction.commit_on_success
513
def get_applications(request):
514
    user = request.user
515
    input_data = read_json_body(request, default={})
516
    query = make_application_query(input_data)
517
    apps = _get_applications(query, request_user=user)
518
    data = get_applications_details(apps)
519
    return json_response(data)
520

    
521

    
522
def _get_applications(query, request_user=None):
523
    apps = ProjectApplication.objects.filter(query)
524

    
525
    if not request_user.is_project_admin():
526
        owned = (Q(owner=request_user) |
527
                 Q(applicant=request_user))
528
        apps = apps.filter(owned)
529
    return apps.select_related()
530

    
531

    
532
@csrf_exempt
533
@api.api_method(http_method="GET", token_required=True, user_required=False)
534
@user_from_token
535
@transaction.commit_on_success
536
def application(request, app_id):
537
    user = request.user
538
    with ExceptionHandler():
539
        application = _get_application(app_id, user)
540
    data = get_application_details(application)
541
    return json_response(data)
542

    
543

    
544
def _get_application(app_id, request_user=None):
545
    application = functions.get_application(app_id)
546
    functions.app_check_allowed(
547
        application, request_user, level=functions.APPLICANT_LEVEL)
548
    return application
549

    
550

    
551
APPLICATION_ACTION = {
552
    "approve": functions.approve_application,
553
    "deny": functions.deny_application,
554
    "dismiss": functions.dismiss_application,
555
    "cancel": functions.cancel_application,
556
}
557

    
558

    
559
@csrf_exempt
560
@api.api_method(http_method="POST", token_required=True, user_required=False)
561
@user_from_token
562
@transaction.commit_on_success
563
def application_action(request, app_id):
564
    user = request.user
565
    data = request.body
566
    input_data = json.loads(data)
567

    
568
    func, action_data = get_action(APPLICATION_ACTION, input_data)
569
    with ExceptionHandler():
570
        func(app_id, request_user=user, reason=action_data)
571

    
572
    return HttpResponse()
573

    
574

    
575
@csrf_exempt
576
def memberships(request):
577
    method = request.method
578
    if method == "GET":
579
        return get_memberships(request)
580
    elif method == "POST":
581
        return post_memberships(request)
582
    return api.api_method_not_allowed(request, allowed_methods=['GET', 'POST'])
583

    
584

    
585
def make_membership_query(input_data):
586
    project_id = input_data.get("project")
587
    if project_id is not None:
588
        if not isinstance(project_id, (int, long)):
589
            raise faults.BadRequest("'project' must be integer")
590
        return Q(project=project_id)
591
    return Q()
592

    
593

    
594
@api.api_method(http_method="GET", token_required=True, user_required=False)
595
@user_from_token
596
@transaction.commit_on_success
597
def get_memberships(request):
598
    user = request.user
599
    input_data = read_json_body(request, default={})
600
    query = make_membership_query(input_data)
601
    memberships = _get_memberships(query, request_user=user)
602
    data = get_memberships_details(memberships, user)
603
    return json_response(data)
604

    
605

    
606
def _get_memberships(query, request_user=None):
607
    memberships = ProjectMembership.objects
608
    if not request_user.is_project_admin():
609
        owned = Q(project__application__owner=request_user)
610
        memb = Q(person=request_user)
611
        memberships = memberships.filter(owned | memb)
612

    
613
    return memberships.select_related(
614
        "project", "project__application",
615
        "project__application__owner", "project__application__applicant",
616
        "person").filter(query)
617

    
618

    
619
def join_project(data, request_user):
620
    project_id = data.get("project")
621
    with ExceptionHandler():
622
        membership = functions.join_project(project_id, request_user)
623
    response = {"id": membership.id}
624
    return json_response(response)
625

    
626

    
627
def enroll_user(data, request_user):
628
    project_id = data.get("project")
629
    email = data.get("user")
630
    with ExceptionHandler():
631
        m = functions.enroll_member_by_email(
632
            project_id, email, request_user)
633

    
634
    response = {"id": m.id}
635
    return json_response(response)
636

    
637

    
638
MEMBERSHIPS_ACTION = {
639
    "join":   join_project,
640
    "enroll": enroll_user,
641
}
642

    
643

    
644
@api.api_method(http_method="POST", token_required=True, user_required=False)
645
@user_from_token
646
@transaction.commit_on_success
647
def post_memberships(request):
648
    user = request.user
649
    data = request.body
650
    input_data = json.loads(data)
651
    func, action_data = get_action(MEMBERSHIPS_ACTION, input_data)
652
    return func(action_data, user)
653

    
654

    
655
@api.api_method(http_method="GET", token_required=True, user_required=False)
656
@user_from_token
657
@transaction.commit_on_success
658
def membership(request, memb_id):
659
    user = request.user
660
    with ExceptionHandler():
661
        m = _get_membership(memb_id, request_user=user)
662
    data = get_membership_details(m, user)
663
    return json_response(data)
664

    
665

    
666
def _get_membership(memb_id, request_user=None):
667
    membership = functions.get_membership_by_id(memb_id)
668
    functions.membership_check_allowed(membership, request_user)
669
    return membership
670

    
671

    
672
MEMBERSHIP_ACTION = {
673
    "leave":  functions.leave_project,
674
    "cancel": functions.cancel_membership,
675
    "accept": functions.accept_membership,
676
    "reject": functions.reject_membership,
677
    "remove": functions.remove_membership,
678
}
679

    
680

    
681
@csrf_exempt
682
@api.api_method(http_method="POST", token_required=True, user_required=False)
683
@user_from_token
684
@transaction.commit_on_success
685
def membership_action(request, memb_id):
686
    user = request.user
687
    input_data = read_json_body(request, default={})
688
    func, action_data = get_action(MEMBERSHIP_ACTION, input_data)
689
    with ExceptionHandler():
690
        func(memb_id, user, reason=action_data)
691
    return HttpResponse()