Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions app/api/custom/attendees.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
from flask import Blueprint, abort, jsonify, make_response, request
from flask_jwt_extended import current_user
from flask_rest_jsonapi.exceptions import ObjectNotFound
from sqlalchemy.orm.exc import NoResultFound

from app.api.helpers.db import safe_query_by_id
from app.api.helpers.errors import ForbiddenError, NotFoundError, UnprocessableEntityError
from app.api.helpers.mail import send_email_to_attendees
from app.api.helpers.permission_manager import has_access
from app.api.helpers.permissions import jwt_required
from app.models import db
from app.models.order import Order
from app.models.ticket_holder import TicketHolder

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

Expand Down Expand Up @@ -45,3 +48,58 @@ def send_receipt():
return jsonify(message="receipt sent to attendees")
else:
raise UnprocessableEntityError({'source': ''}, 'Order identifier missing')


@attendee_blueprint.route('/states', methods=['GET'])
@jwt_required
def check_attendee_state():
"""
API to check attendee state is check in/registered
@return: user is registered or not
"""
from app.models.event import Event

if not request.args.get('event_id', False):
raise NotFoundError(
{'parameter': 'event_id'}, "event_id is missing from your request."
)
if not request.args.get('attendee_id', False):
raise NotFoundError(
{'parameter': 'attendee_id'}, "attendee_id is missing from your request."
)
event_id = request.args.get('event_id')
attendee_id = request.args.get('attendee_id')
if event_id is not None:
validate_param_as_id(event_id)
if attendee_id is not None:
validate_param_as_id(attendee_id)
try:
event = safe_query_by_id(Event, event_id)
except ObjectNotFound:
raise NotFoundError({'parameter': f'{event_id}'}, "Event not found.")
try:
attendee = safe_query_by_id(TicketHolder, attendee_id)
except ObjectNotFound:
raise NotFoundError({'parameter': f'{attendee_id}'}, "Attendee not found.")
if event.id != attendee.event_id:
raise UnprocessableEntityError(
{'parameter': 'Attendee'},
"Attendee not belong to this event.",
)
return jsonify(
{
'is_registered': attendee.is_registered,
'register_times': attendee.register_times,
}
)


def validate_param_as_id(param):
"""
validate id if integer or not
@param param: param to check
"""
if not (isinstance(param, int) or (isinstance(param, str) and param.isdigit())):
raise UnprocessableEntityError(
{'parameter': f'{param}'}, f'{param} is not a valid id'
)
2 changes: 2 additions & 0 deletions app/api/schema/attendees.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ def validate_json(self, data, original_data):
checkout_times = fields.Str(allow_none=True)
attendee_notes = fields.Str(allow_none=True)
is_checked_out = fields.Boolean()
is_registered = fields.Boolean()
register_times = fields.Str(allow_none=True)
pdf_url = fields.Url(dump_only=True)
complex_field_values = CustomFormValueField(allow_none=True)
is_consent_form_field = fields.Boolean(allow_none=True)
Expand Down
54 changes: 42 additions & 12 deletions app/api/user_check_in.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from flask_rest_jsonapi.exceptions import ObjectNotFound
from sqlalchemy.orm.exc import NoResultFound

from app.api.helpers.db import save_to_db
from app.api.helpers.errors import UnprocessableEntityError
from app.api.helpers.permission_manager import has_access
from app.api.helpers.permissions import jwt_required
Expand All @@ -19,6 +20,7 @@
from app.models.session import Session
from app.models.session_type import SessionType
from app.models.station import Station
from app.models.ticket_holder import TicketHolder
from app.models.track import Track
from app.models.user_check_in import UserCheckIn

Expand Down Expand Up @@ -95,28 +97,57 @@ def before_create_object(self, data, _view_kwargs):
:param _view_kwargs:
:return:
"""
station = self.session.query(Station).filter_by(id=data.get('station')).one()
try:
station = db.session.query(Station).filter_by(id=data.get('station')).one()
except NoResultFound:
raise ObjectNotFound({'parameter': data.get('station')}, "Station: not found")
current_time = datetime.datetime.utcnow()
if not has_access('is_coorganizer', event_id=station.event_id):
raise UnprocessableEntityError(
{'parameter': 'station'},
"Only admin/organiser/coorganizer of event only able to check in",
)
try:
attendee = (
self.session.query(TicketHolder)
.filter_by(id=data.get('ticket_holder'))
.one()
)
except NoResultFound:
raise ObjectNotFound(
{'parameter': data.get('attendee')}, "Attendee: not found"
)

if attendee.event_id != station.event_id:
raise UnprocessableEntityError(
{'parameter': 'Attendee'},
"Attendee not belong to this event",
)

if station.station_type != STATION_TYPE.get('registration'):
# validate if microlocation_id from session matches with station
session = self.session.query(Session).filter_by(id=data.get('session')).one()
session = (
self.session.query(Session).filter_by(id=data.get('session')).first()
)
if session is None:
raise ObjectNotFound(
{'parameter': data.get('session')}, "Session: not found"
)
validate_microlocation(station=station, session=session)
if session.session_type_id:
session_type = (
self.session.query(SessionType)
.filter(SessionType.id == session.session_type_id)
.one()
.first()
)
data['session_name'] = session_type.name
if session_type is not None:
data['session_name'] = session_type.name
if session.track_id:
track = (
self.session.query(Track).filter(Track.id == session.track_id).one()
self.session.query(Track).filter(Track.id == session.track_id).first()
)
data['track_name'] = track.name
if track is not None:
data['track_name'] = track.name
data['speaker_name'] = ', '.join(
[str(speaker.name) for speaker in session.speakers]
)
Expand All @@ -139,6 +170,7 @@ def before_create_object(self, data, _view_kwargs):
validate_check_in_out_status(
station=station, attendee_data=attendee_check_in_status
)
data['check_in_out_at'] = current_time
else:
if station.station_type == STATION_TYPE.get('registration'):
attendee_check_in_status = (
Expand All @@ -158,6 +190,10 @@ def before_create_object(self, data, _view_kwargs):
},
"Attendee already registered.",
)
# update register time for attendee
attendee.is_registered = True
attendee.register_times = current_time
save_to_db(attendee)
if station.station_type == STATION_TYPE.get('daily'):
attendee_check_in_status = (
self.session.query(UserCheckIn)
Expand All @@ -178,12 +214,6 @@ def before_create_object(self, data, _view_kwargs):
"Attendee already check daily on station.",
)

if station.station_type in (
STATION_TYPE.get('check in'),
STATION_TYPE.get('check out'),
):
data['check_in_out_at'] = datetime.datetime.utcnow()

schema = UserCheckInSchema
methods = [
'POST',
Expand Down
2 changes: 2 additions & 0 deletions app/models/ticket_holder.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,11 @@ class TicketHolder(SoftDeletionModel):
order_id: int = db.Column(db.Integer, db.ForeignKey('orders.id', ondelete='CASCADE'))
is_checked_in: bool = db.Column(db.Boolean, default=False)
is_checked_out: bool = db.Column(db.Boolean, default=False)
is_registered: bool = db.Column(db.Boolean, default=False)
device_name_checkin: str = db.Column(db.String)
checkin_times: str = db.Column(db.String)
checkout_times: str = db.Column(db.String)
register_times: str = db.Column(db.String)
attendee_notes: str = db.Column(db.String)
event_id: int = db.Column(
db.Integer, db.ForeignKey('events.id', ondelete='CASCADE'), nullable=False
Expand Down
38 changes: 36 additions & 2 deletions docs/api/blueprint/attendees.apib
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ Related to ticket holders(attendees) of an event (free, paid, donation) to the e
| `is-checked-out` | If the attendee has checked out | boolean | - |
| `attendee-notes` | Comma separated attendee notes | string | - |
| `pdf-url` | pdf url of the Attendee | url | - |
| `is-registered` | If the attendee is registered | boolean | - |
| `register-times` | Comma separated register times | string | - |

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

Expand Down Expand Up @@ -109,13 +111,15 @@ Get a list of attendees of an order.
"deleted-at": null,
"work-address": null,
"checkin-times": null,
"register-times": null,
"state": "example",
"country": "IN",
"lastname": "UnDoe",
"city": "example",
"phone": null,
"company": null,
"is-checked-in": false,
"is-registered": false,
"gender": null,
"shipping-address": null,
"blog": null,
Expand Down Expand Up @@ -146,7 +150,7 @@ Get a list of attendees of an order.
"self": "/v1/orders/7201904e-c695-4251-a30a-61765a37ff24/attendees"
}
}

## List Attendees under an event [/v1/events/{event_id}/attendees]
+ Parameters
+ event_id: 1 (integer) - Identifier of the event
Expand Down Expand Up @@ -206,13 +210,15 @@ Get a list of attendees of an event.
"deleted-at": null,
"work-address": null,
"checkin-times": null,
"register-times": null,
"state": "example",
"country": "IN",
"lastname": "UnDoe",
"city": "example",
"phone": null,
"company": null,
"is-checked-in": false,
"is-registered": false,
"gender": null,
"shipping-address": null,
"blog": null,
Expand Down Expand Up @@ -279,6 +285,8 @@ Search a list of attendees of an event.
"phone": null,
"company": null,
"is-checked-in": false,
"is-registered": false,
"register-times": null,
"gender": null,
"shipping-address": null,
"blog": null,
Expand Down Expand Up @@ -364,6 +372,8 @@ Get a list of attendees of a ticket.
"phone": null,
"company": null,
"is-checked-in": false,
"is-registered": false,
"register-times": null,
"gender": null,
"shipping-address": null,
"blog": null,
Expand Down Expand Up @@ -445,6 +455,8 @@ Get a single attendee.
"phone": null,
"company": null,
"is-checked-in": false,
"is-registered": false,
"register-times": null,
"gender": null,
"shipping-address": null,
"blog": null,
Expand Down Expand Up @@ -589,4 +601,26 @@ Delete a single attendee.
"version": "1.0"
}
}


## Get Attendee State [/v1/states{?event_id,attendee_id}]
+ Parameters
+ event_id: 1 (integer) - Identifier of the event
+ attendee_id: 1 (integer) - ID of the attendee in the form of an integer

### Get Attendee State [GET]
Check attendee state if attendee is registered or not.

+ Request

+ Headers

Accept: application/vnd.api+json

Authorization: JWT <Auth Key>

+ Response 200 (application/json)

{
"is_registered": true,
"register_times": "2023-08-08 03:03:52.827812"
}
29 changes: 29 additions & 0 deletions migrations/versions/rev-2023-08-07-15:52:49-3e8e18c0bebe_.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"""empty message

Revision ID: 3e8e18c0bebe
Revises: 8b5bc48e1d4c
Create Date: 2023-08-07 15:52:49.656233

"""

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = '3e8e18c0bebe'
down_revision = '8b5bc48e1d4c'


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('ticket_holders', sa.Column('is_registered', sa.Boolean(), server_default='False', nullable=True))
op.add_column('ticket_holders', sa.Column('register_times', sa.String(), nullable=True))
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('ticket_holders', 'register_times')
op.drop_column('ticket_holders', 'is_registered')
# ### end Alembic commands ###
Loading