Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / api / projects.py @ 85ae5a4c

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
        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)
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
        projects = projects.filter(is_memb | owned | active)
298
    return projects.select_related(
299
        "application", "application__owner", "application__applicant")
300

    
301

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

    
311

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

    
321

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

    
332

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

    
339

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

    
349

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

    
360

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

    
367

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

    
372

    
373
def valid_project_name(name):
374
    return DOMAIN_VALUE_REGEX.match(name) is not None
375

    
376

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

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

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

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

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

    
407
    start_date = _get_date(app_data, "start_date")
408
    end_date = _get_date(app_data, "end_date")
409

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

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

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

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

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

    
444

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

    
459

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

    
467

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

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

    
482

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

    
490

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

    
499

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

    
511

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

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

    
521

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

    
533

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

    
540

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

    
548

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

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

    
562
    return HttpResponse()
563

    
564

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

    
574

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

    
583

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

    
595

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

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

    
608

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

    
616

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

    
624
    response = {"id": m.id}
625
    return json_response(response)
626

    
627

    
628
MEMBERSHIPS_ACTION = {
629
    "join":   join_project,
630
    "enroll": enroll_user,
631
}
632

    
633

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

    
644

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

    
655

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

    
661

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

    
670

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