Skip to content

Commit 2766736

Browse files
authored
feature-9040 + feature-9042: return register state of attendee (#9051)
* fix issue user request check in not admin * format code * feature-9040 + feature-9042: update unit test * fix UT * update payload + api route * update unit test * update payload for attendee state api * fix payload * fix ci/cd * fix ci/cd * fix conflict * fix conflict * validat mising request params * re-run pipeline * re-run pipeline
1 parent 28d53c4 commit 2766736

File tree

9 files changed

+312
-27
lines changed

9 files changed

+312
-27
lines changed

app/api/custom/attendees.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
from flask import Blueprint, abort, jsonify, make_response, request
22
from flask_jwt_extended import current_user
3+
from flask_rest_jsonapi.exceptions import ObjectNotFound
34
from sqlalchemy.orm.exc import NoResultFound
45

6+
from app.api.helpers.db import safe_query_by_id
57
from app.api.helpers.errors import ForbiddenError, NotFoundError, UnprocessableEntityError
68
from app.api.helpers.mail import send_email_to_attendees
79
from app.api.helpers.permission_manager import has_access
810
from app.api.helpers.permissions import jwt_required
911
from app.models import db
1012
from app.models.order import Order
13+
from app.models.ticket_holder import TicketHolder
1114

1215
attendee_blueprint = Blueprint('attendee_blueprint', __name__, url_prefix='/v1')
1316

@@ -45,3 +48,58 @@ def send_receipt():
4548
return jsonify(message="receipt sent to attendees")
4649
else:
4750
raise UnprocessableEntityError({'source': ''}, 'Order identifier missing')
51+
52+
53+
@attendee_blueprint.route('/states', methods=['GET'])
54+
@jwt_required
55+
def check_attendee_state():
56+
"""
57+
API to check attendee state is check in/registered
58+
@return: user is registered or not
59+
"""
60+
from app.models.event import Event
61+
62+
if not request.args.get('event_id', False):
63+
raise NotFoundError(
64+
{'parameter': 'event_id'}, "event_id is missing from your request."
65+
)
66+
if not request.args.get('attendee_id', False):
67+
raise NotFoundError(
68+
{'parameter': 'attendee_id'}, "attendee_id is missing from your request."
69+
)
70+
event_id = request.args.get('event_id')
71+
attendee_id = request.args.get('attendee_id')
72+
if event_id is not None:
73+
validate_param_as_id(event_id)
74+
if attendee_id is not None:
75+
validate_param_as_id(attendee_id)
76+
try:
77+
event = safe_query_by_id(Event, event_id)
78+
except ObjectNotFound:
79+
raise NotFoundError({'parameter': f'{event_id}'}, "Event not found.")
80+
try:
81+
attendee = safe_query_by_id(TicketHolder, attendee_id)
82+
except ObjectNotFound:
83+
raise NotFoundError({'parameter': f'{attendee_id}'}, "Attendee not found.")
84+
if event.id != attendee.event_id:
85+
raise UnprocessableEntityError(
86+
{'parameter': 'Attendee'},
87+
"Attendee not belong to this event.",
88+
)
89+
return jsonify(
90+
{
91+
'is_registered': attendee.is_registered,
92+
'register_times': attendee.register_times,
93+
}
94+
)
95+
96+
97+
def validate_param_as_id(param):
98+
"""
99+
validate id if integer or not
100+
@param param: param to check
101+
"""
102+
if not (isinstance(param, int) or (isinstance(param, str) and param.isdigit())):
103+
raise UnprocessableEntityError(
104+
{'parameter': f'{param}'}, f'{param} is not a valid id'
105+
)

app/api/schema/attendees.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ def validate_json(self, data, original_data):
7070
checkout_times = fields.Str(allow_none=True)
7171
attendee_notes = fields.Str(allow_none=True)
7272
is_checked_out = fields.Boolean()
73+
is_registered = fields.Boolean()
74+
register_times = fields.Str(allow_none=True)
7375
pdf_url = fields.Url(dump_only=True)
7476
complex_field_values = CustomFormValueField(allow_none=True)
7577
is_consent_form_field = fields.Boolean(allow_none=True)

app/api/user_check_in.py

Lines changed: 42 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from flask_rest_jsonapi.exceptions import ObjectNotFound
55
from sqlalchemy.orm.exc import NoResultFound
66

7+
from app.api.helpers.db import save_to_db
78
from app.api.helpers.errors import UnprocessableEntityError
89
from app.api.helpers.permission_manager import has_access
910
from app.api.helpers.permissions import jwt_required
@@ -19,6 +20,7 @@
1920
from app.models.session import Session
2021
from app.models.session_type import SessionType
2122
from app.models.station import Station
23+
from app.models.ticket_holder import TicketHolder
2224
from app.models.track import Track
2325
from app.models.user_check_in import UserCheckIn
2426

@@ -95,28 +97,57 @@ def before_create_object(self, data, _view_kwargs):
9597
:param _view_kwargs:
9698
:return:
9799
"""
98-
station = self.session.query(Station).filter_by(id=data.get('station')).one()
100+
try:
101+
station = db.session.query(Station).filter_by(id=data.get('station')).one()
102+
except NoResultFound:
103+
raise ObjectNotFound({'parameter': data.get('station')}, "Station: not found")
104+
current_time = datetime.datetime.utcnow()
99105
if not has_access('is_coorganizer', event_id=station.event_id):
100106
raise UnprocessableEntityError(
101107
{'parameter': 'station'},
102108
"Only admin/organiser/coorganizer of event only able to check in",
103109
)
110+
try:
111+
attendee = (
112+
self.session.query(TicketHolder)
113+
.filter_by(id=data.get('ticket_holder'))
114+
.one()
115+
)
116+
except NoResultFound:
117+
raise ObjectNotFound(
118+
{'parameter': data.get('attendee')}, "Attendee: not found"
119+
)
120+
121+
if attendee.event_id != station.event_id:
122+
raise UnprocessableEntityError(
123+
{'parameter': 'Attendee'},
124+
"Attendee not belong to this event",
125+
)
126+
104127
if station.station_type != STATION_TYPE.get('registration'):
105128
# validate if microlocation_id from session matches with station
106-
session = self.session.query(Session).filter_by(id=data.get('session')).one()
129+
session = (
130+
self.session.query(Session).filter_by(id=data.get('session')).first()
131+
)
132+
if session is None:
133+
raise ObjectNotFound(
134+
{'parameter': data.get('session')}, "Session: not found"
135+
)
107136
validate_microlocation(station=station, session=session)
108137
if session.session_type_id:
109138
session_type = (
110139
self.session.query(SessionType)
111140
.filter(SessionType.id == session.session_type_id)
112-
.one()
141+
.first()
113142
)
114-
data['session_name'] = session_type.name
143+
if session_type is not None:
144+
data['session_name'] = session_type.name
115145
if session.track_id:
116146
track = (
117-
self.session.query(Track).filter(Track.id == session.track_id).one()
147+
self.session.query(Track).filter(Track.id == session.track_id).first()
118148
)
119-
data['track_name'] = track.name
149+
if track is not None:
150+
data['track_name'] = track.name
120151
data['speaker_name'] = ', '.join(
121152
[str(speaker.name) for speaker in session.speakers]
122153
)
@@ -139,6 +170,7 @@ def before_create_object(self, data, _view_kwargs):
139170
validate_check_in_out_status(
140171
station=station, attendee_data=attendee_check_in_status
141172
)
173+
data['check_in_out_at'] = current_time
142174
else:
143175
if station.station_type == STATION_TYPE.get('registration'):
144176
attendee_check_in_status = (
@@ -158,6 +190,10 @@ def before_create_object(self, data, _view_kwargs):
158190
},
159191
"Attendee already registered.",
160192
)
193+
# update register time for attendee
194+
attendee.is_registered = True
195+
attendee.register_times = current_time
196+
save_to_db(attendee)
161197
if station.station_type == STATION_TYPE.get('daily'):
162198
attendee_check_in_status = (
163199
self.session.query(UserCheckIn)
@@ -178,12 +214,6 @@ def before_create_object(self, data, _view_kwargs):
178214
"Attendee already check daily on station.",
179215
)
180216

181-
if station.station_type in (
182-
STATION_TYPE.get('check in'),
183-
STATION_TYPE.get('check out'),
184-
):
185-
data['check_in_out_at'] = datetime.datetime.utcnow()
186-
187217
schema = UserCheckInSchema
188218
methods = [
189219
'POST',

app/models/ticket_holder.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,11 @@ class TicketHolder(SoftDeletionModel):
5454
order_id: int = db.Column(db.Integer, db.ForeignKey('orders.id', ondelete='CASCADE'))
5555
is_checked_in: bool = db.Column(db.Boolean, default=False)
5656
is_checked_out: bool = db.Column(db.Boolean, default=False)
57+
is_registered: bool = db.Column(db.Boolean, default=False)
5758
device_name_checkin: str = db.Column(db.String)
5859
checkin_times: str = db.Column(db.String)
5960
checkout_times: str = db.Column(db.String)
61+
register_times: str = db.Column(db.String)
6062
attendee_notes: str = db.Column(db.String)
6163
event_id: int = db.Column(
6264
db.Integer, db.ForeignKey('events.id', ondelete='CASCADE'), nullable=False

docs/api/blueprint/attendees.apib

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ Related to ticket holders(attendees) of an event (free, paid, donation) to the e
1515
| `is-checked-out` | If the attendee has checked out | boolean | - |
1616
| `attendee-notes` | Comma separated attendee notes | string | - |
1717
| `pdf-url` | pdf url of the Attendee | url | - |
18+
| `is-registered` | If the attendee is registered | boolean | - |
19+
| `register-times` | Comma separated register times | string | - |
1820

1921
## Send order receipts [v1/attendees/send-receipt]
2022

@@ -109,13 +111,15 @@ Get a list of attendees of an order.
109111
"deleted-at": null,
110112
"work-address": null,
111113
"checkin-times": null,
114+
"register-times": null,
112115
"state": "example",
113116
"country": "IN",
114117
"lastname": "UnDoe",
115118
"city": "example",
116119
"phone": null,
117120
"company": null,
118121
"is-checked-in": false,
122+
"is-registered": false,
119123
"gender": null,
120124
"shipping-address": null,
121125
"blog": null,
@@ -146,7 +150,7 @@ Get a list of attendees of an order.
146150
"self": "/v1/orders/7201904e-c695-4251-a30a-61765a37ff24/attendees"
147151
}
148152
}
149-
153+
150154
## List Attendees under an event [/v1/events/{event_id}/attendees]
151155
+ Parameters
152156
+ event_id: 1 (integer) - Identifier of the event
@@ -206,13 +210,15 @@ Get a list of attendees of an event.
206210
"deleted-at": null,
207211
"work-address": null,
208212
"checkin-times": null,
213+
"register-times": null,
209214
"state": "example",
210215
"country": "IN",
211216
"lastname": "UnDoe",
212217
"city": "example",
213218
"phone": null,
214219
"company": null,
215220
"is-checked-in": false,
221+
"is-registered": false,
216222
"gender": null,
217223
"shipping-address": null,
218224
"blog": null,
@@ -279,6 +285,8 @@ Search a list of attendees of an event.
279285
"phone": null,
280286
"company": null,
281287
"is-checked-in": false,
288+
"is-registered": false,
289+
"register-times": null,
282290
"gender": null,
283291
"shipping-address": null,
284292
"blog": null,
@@ -364,6 +372,8 @@ Get a list of attendees of a ticket.
364372
"phone": null,
365373
"company": null,
366374
"is-checked-in": false,
375+
"is-registered": false,
376+
"register-times": null,
367377
"gender": null,
368378
"shipping-address": null,
369379
"blog": null,
@@ -445,6 +455,8 @@ Get a single attendee.
445455
"phone": null,
446456
"company": null,
447457
"is-checked-in": false,
458+
"is-registered": false,
459+
"register-times": null,
448460
"gender": null,
449461
"shipping-address": null,
450462
"blog": null,
@@ -589,4 +601,26 @@ Delete a single attendee.
589601
"version": "1.0"
590602
}
591603
}
592-
604+
605+
## Get Attendee State [/v1/states{?event_id,attendee_id}]
606+
+ Parameters
607+
+ event_id: 1 (integer) - Identifier of the event
608+
+ attendee_id: 1 (integer) - ID of the attendee in the form of an integer
609+
610+
### Get Attendee State [GET]
611+
Check attendee state if attendee is registered or not.
612+
613+
+ Request
614+
615+
+ Headers
616+
617+
Accept: application/vnd.api+json
618+
619+
Authorization: JWT <Auth Key>
620+
621+
+ Response 200 (application/json)
622+
623+
{
624+
"is_registered": true,
625+
"register_times": "2023-08-08 03:03:52.827812"
626+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
"""empty message
2+
3+
Revision ID: 3e8e18c0bebe
4+
Revises: 8b5bc48e1d4c
5+
Create Date: 2023-08-07 15:52:49.656233
6+
7+
"""
8+
9+
from alembic import op
10+
import sqlalchemy as sa
11+
12+
13+
# revision identifiers, used by Alembic.
14+
revision = '3e8e18c0bebe'
15+
down_revision = '8b5bc48e1d4c'
16+
17+
18+
def upgrade():
19+
# ### commands auto generated by Alembic - please adjust! ###
20+
op.add_column('ticket_holders', sa.Column('is_registered', sa.Boolean(), server_default='False', nullable=True))
21+
op.add_column('ticket_holders', sa.Column('register_times', sa.String(), nullable=True))
22+
# ### end Alembic commands ###
23+
24+
25+
def downgrade():
26+
# ### commands auto generated by Alembic - please adjust! ###
27+
op.drop_column('ticket_holders', 'register_times')
28+
op.drop_column('ticket_holders', 'is_registered')
29+
# ### end Alembic commands ###

0 commit comments

Comments
 (0)