To receive notifications about scheduled maintenance, please subscribe to the mailing-list gitlab-operations@sympa.ethz.ch. You can subscribe to the mailing-list at https://sympa.ethz.ch

signups.py 5.24 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
"""Signup processing (Waiting list and payments).

So far only dummy functionality, i.e. if a payment is posted, all courses
are set to accepted.

As soon as we have payment service provider, the definite functionality needs
to be implemented.

TODO: Send notification mails
"""

import json
from functools import wraps
from itertools import chain

16
from bson import ObjectId
17
18
19
20
from flask import current_app, abort
from eve.methods.patch import patch_internal


21
def wrap_response(function):
22
    """Wrapper to modify payload for successful requests (status 2xx)."""
23
24
    @wraps(function)
    def _wrapped(_, response):
25
26
27
28
        if response.status_code // 100 == 2:
            payload = json.loads(response.get_data(as_text=True))

            if '_items' in payload:
29
                function(payload['_items'])
30
            else:
31
                function([payload])
32
33

            response.set_data(json.dumps(payload))
34
    return _wrapped
35
36
37
38
39


@wrap_response
def new_signups(signups):
    """Update the status for all signups to a course."""
40
    def get_id(course):
41
42
43
44
45
        """Return the course id, necessary to cope with embedding.

        If the client requests `course` to be embedded, it will be a dict
        with the id as key. Otherwise `course` will be just the id.
        """
46
47
        return course['_id'] if isinstance(course, dict) else course

48
    # Remove duplicates by using a set
49
    courses = set(get_id(item['course']) for item in signups)
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
    # Re-format signups into a dict so we can update them easier later
    signups_by_id = {str(item['_id']): item for item in signups}

    modified = chain.from_iterable(update_signups(course)
                                   for course in courses)

    for _id in modified:
        # Update response payload if needed
        try:
            signups_by_id[_id]['status'] = 'reserved'
        except AttributeError:
            pass  # Not in response, nothing to do


def deleted_signup(signup):
    """Update status of course the signup belonged to."""
    update_signups(signup['course'])


69
70
71
def patched_signup(update, original):
    """Update status of all signups of the original and new course."""
    # Only need to do something if course is changed
72
    if 'course' in update:
73
74
        update_signups(update['course'])
        update_signups(original['course'])
75
76
77
78
79


def patched_course(update, original):
    """If the number of spots changed, update signups of course."""
    if 'spots' in update:
80
        update_signups(original['_id'])
81
82
83
84
85


def block_course_deletion(course):
    """If a course has signups, it can't be deleted."""
    count = current_app.data.driver.db['signups'].count({
86
        'course': course['_id']
87
88
89
90
91
92
    })

    if count > 0:
        abort(409, "Course cannot be deleted as long as it has signups.")


93
94
95
96
97
98
def update_signups(course_id):
    """Update waiting list for a course.

    The course id can be given as string or ObjectId.

    We can assume that the course exists, otherwise Eve stops earlier.
99

100
    Return list of ids of all reserved signups.
101
    """
102
103
104
105
106
107
    course_id = ObjectId(course_id)  # Ensure we are working with ObjectId

    # Determine how many spots the course has
    total_spots = (current_app.data.driver.db['courses']
                   .find_one({'_id': course_id}, projection={'spots': 1})
                   .get('spots', 0))
108
109

    # Next, count current signups not on waiting list
110
111
112
    signups = current_app.data.driver.db['signups']
    taken_spots = signups.count({'course': course_id,
                                 'status': {'$ne': 'waiting'}})
113
114
115
116
117
118
119
120

    available_spots = total_spots - taken_spots

    if available_spots <= 0:
        return []

    # Finally, get as many signups on the waiting list as spots available
    # sort by _updated, use nethz as tie breaker
121
122
123
124
125
    chosen_signups = signups.find({'course': course_id,
                                   'status': 'waiting'},
                                  projection=['_id'],
                                  sort=[('_updated', 1), ('nethz', 1)],
                                  limit=available_spots)
126

127
    signup_ids = [item['_id'] for item in chosen_signups]
128

129
130
    signups.update_many({'_id': {'$in': signup_ids}},
                        {'$set': {'status': 'reserved'}})
131

132
    return [str(item) for item in signup_ids]
133
134
135
136


def mark_as_paid(payments):
    """After successful payment, set status to `accepted`."""
137
138
139
140
    # Check if payments is not a list
    if not isinstance(payments, list):
        payments = [payments]

141
142
143
144
145
146
147
148
149
    for payment in payments:

        for signup in payment['signups']:
            data = {'status': 'accepted'}
            patch_internal('signups',
                           _id=str(signup),
                           payload=data,
                           concurrency_check=False,
                           skip_validation=True)
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166


def mark_as_unpaid(payments):
    """Before a payment is deleted, set status to `reserved`."""
    # Check if payments is not a list
    if not isinstance(payments, list):
        payments = [payments]

    for payment in payments:

        for signup in payment['signups']:
            data = {'status': 'reserved'}
            patch_internal('signups',
                           _id=str(signup),
                           payload=data,
                           concurrency_check=False,
                           skip_validation=True)