Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / api / projects.py @ 88f5242e

History | View | Annotate | Download (21.2 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.uuid,
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.uuid,
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.uuid,
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_uuid(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.uuid,
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
        return Q(chain__uuid=project_id)
505
    return Q()
506

    
507

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

    
519

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

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

    
529

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

    
541

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

    
548

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

    
556

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

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

    
570
    return HttpResponse()
571

    
572

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

    
582

    
583
def make_membership_query(input_data):
584
    project_id = input_data.get("project")
585
    if project_id is not None:
586
        return Q(project__uuid=project_id)
587
    return Q()
588

    
589

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

    
601

    
602
def _get_memberships(query, request_user=None):
603
    memberships = ProjectMembership.objects
604
    if not request_user.is_project_admin():
605
        owned = Q(project__application__owner=request_user)
606
        memb = Q(person=request_user)
607
        memberships = memberships.filter(owned | memb)
608

    
609
    return memberships.select_related(
610
        "project", "project__application",
611
        "project__application__owner", "project__application__applicant",
612
        "person").filter(query)
613

    
614

    
615
def join_project(data, request_user):
616
    project_id = data.get("project")
617
    with ExceptionHandler():
618
        membership = functions.join_project(project_id, request_user)
619
    response = {"id": membership.id}
620
    return json_response(response)
621

    
622

    
623
def enroll_user(data, request_user):
624
    project_id = data.get("project")
625
    email = data.get("user")
626
    with ExceptionHandler():
627
        m = functions.enroll_member_by_email(
628
            project_id, email, request_user)
629

    
630
    response = {"id": m.id}
631
    return json_response(response)
632

    
633

    
634
MEMBERSHIPS_ACTION = {
635
    "join":   join_project,
636
    "enroll": enroll_user,
637
}
638

    
639

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

    
650

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

    
661

    
662
def _get_membership(memb_id, request_user=None):
663
    membership = functions.get_membership_by_id(memb_id)
664
    functions.membership_check_allowed(membership, request_user)
665
    return membership
666

    
667

    
668
MEMBERSHIP_ACTION = {
669
    "leave":  functions.leave_project,
670
    "cancel": functions.cancel_membership,
671
    "accept": functions.accept_membership,
672
    "reject": functions.reject_membership,
673
    "remove": functions.remove_membership,
674
}
675

    
676

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