Skip to content

Commit 6a2eceb

Browse files
Feature-9065: Track when attendees login and join an event virtually in CSV (#9070)
* fix issue user request check in not admin * feature-9065: Track when attendees login and join an event virtually in CSV * feature-9065: Track when attendees login and join an event virtually in CSV * fix python deepsource scan --------- Co-authored-by: Mario Behling <[email protected]>
1 parent 2c55fc2 commit 6a2eceb

File tree

6 files changed

+227
-2
lines changed

6 files changed

+227
-2
lines changed

app/api/custom/events.py

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,21 @@
22
from flask_jwt_extended import current_user
33
from sqlalchemy import asc, distinct, func, or_
44

5-
from app.api.helpers.errors import ForbiddenError, UnprocessableEntityError
5+
from app.api.helpers.errors import ForbiddenError, NotFoundError, UnprocessableEntityError
66
from app.api.helpers.mail import send_email
77
from app.api.helpers.permissions import is_coorganizer, jwt_required, to_event_id
88
from app.api.helpers.system_mails import MAILS, MailType
9+
from app.api.helpers.user import get_user_id_from_token, virtual_event_check_in
910
from app.api.helpers.utilities import group_by, strip_tags
1011
from app.api.schema.exhibitors import ExhibitorReorderSchema
1112
from app.api.schema.speakers import SpeakerReorderSchema
13+
from app.api.schema.virtual_check_in import VirtualCheckInSchema
1214
from app.models import db
1315
from app.models.discount_code import DiscountCode
1416
from app.models.event import Event
1517
from app.models.exhibitor import Exhibitor
18+
from app.models.microlocation import Microlocation
19+
from app.models.order import Order
1620
from app.models.session import Session
1721
from app.models.speaker import Speaker
1822
from app.models.ticket_holder import TicketHolder
@@ -216,6 +220,53 @@ def search_attendees(event_id):
216220
return jsonify({'attendees': attendees})
217221

218222

223+
@events_routes.route('/<string:event_identifier>/virtual/check-in', methods=['POST'])
224+
@jwt_required
225+
def virtual_check_in(event_identifier):
226+
"""Search attendees by name or email."""
227+
event = db.session.query(Event).filter_by(identifier=event_identifier).first()
228+
if event is None:
229+
raise NotFoundError({'source': ''}, 'event can not be found')
230+
data, errors = VirtualCheckInSchema().load(request.get_json())
231+
if errors:
232+
raise UnprocessableEntityError(
233+
{'pointer': '/data', 'errors': errors}, 'Data in incorrect format'
234+
)
235+
token = None
236+
if "Authorization" in request.headers:
237+
token = request.headers["Authorization"].split(" ")[1]
238+
if not token:
239+
return {
240+
"message": "Authentication Token is missing!",
241+
"data": None,
242+
"error": "Unauthorized",
243+
}, 401
244+
user_id = get_user_id_from_token(token)
245+
if user_id is None:
246+
return {"message": "Can't get user id!", "data": None}, 404
247+
248+
if data.get('microlocation_id') is not None:
249+
microlocation = Microlocation.query.filter(
250+
Microlocation.id == data.get('microlocation_id')
251+
).first()
252+
if microlocation is None:
253+
raise NotFoundError({'source': ''}, 'microlocation can not be found')
254+
255+
orders = Order.query.filter(
256+
Order.user_id == user_id, Order.event_id == event.id
257+
).all()
258+
259+
orders_id = [order.id for order in orders]
260+
261+
attendees = TicketHolder.query.filter(TicketHolder.order_id.in_(orders_id)).all()
262+
263+
attendees_ids = [attendee.id for attendee in attendees]
264+
265+
virtual_event_check_in(data, attendees_ids, event.id)
266+
267+
return jsonify({'message': 'Attendee check in/out success'})
268+
269+
219270
@events_routes.route('/<string:event_identifier>/sessions/languages', methods=['GET'])
220271
@to_event_id
221272
def get_languages(event_id):

app/api/helpers/csv_jobs_util.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from app.models.helpers.versioning import strip_tags
88
from app.models.order import OrderTicket
99
from app.models.ticket import access_codes_tickets
10+
from app.models.user_check_in import VirtualCheckIn
1011

1112

1213
def export_orders_csv(orders):
@@ -148,11 +149,28 @@ def export_attendees_csv(attendees, custom_forms, attendee_form_dict):
148149
)
149150
converted_header = field.name
150151
data[converted_header] = dict_value
152+
data['virtual_event_checkin_times'] = get_virtual_checkin_times(attendee.id)
151153
return_dict_list.append(data)
152154

153155
return return_dict_list
154156

155157

158+
def get_virtual_checkin_times(attendee_id: int):
159+
"""
160+
get check in times of attendee
161+
@param attendee_id: attendee_id
162+
@return: time check in of attendee
163+
"""
164+
virtual_check_in = VirtualCheckIn.query.filter(
165+
VirtualCheckIn.ticket_holder_id.any(attendee_id),
166+
VirtualCheckIn.check_in_type == 'room',
167+
).all()
168+
virtual_check_in_times = [
169+
item.check_in_at.strftime("%Y-%m-%dT%H:%M:%S%z") for item in virtual_check_in
170+
]
171+
return virtual_check_in_times
172+
173+
156174
def export_sessions_csv(sessions):
157175
headers = [
158176
'Session Title',

app/api/helpers/user.py

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
1+
import datetime
2+
3+
import jwt
4+
from flask import current_app
15
from sqlalchemy.orm.exc import NoResultFound
26

37
from app.api.helpers.db import save_to_db
4-
from app.api.helpers.errors import ForbiddenError
8+
from app.api.helpers.errors import ForbiddenError, UnprocessableEntityError
59
from app.models import db
610
from app.models.user import User
11+
from app.models.user_check_in import VirtualCheckIn
712

813

914
def modify_email_for_user_to_be_deleted(user):
@@ -49,3 +54,50 @@ def modify_email_for_user_to_be_restored(user):
4954
"This email is already registered! Manually edit and then try restoring",
5055
)
5156
return user
57+
58+
59+
def get_user_id_from_token(token: str):
60+
"""
61+
Get user Id from JWT token
62+
@param token: JWT token
63+
@return: user id
64+
"""
65+
data = jwt.decode(token, current_app.config["SECRET_KEY"], algorithms=["HS256"])
66+
if not data.get('identity', False):
67+
return {"message": "Can't get user id!", "data": None}, 404
68+
69+
return data["identity"]
70+
71+
72+
def virtual_event_check_in(data, attendee_ids, event_id):
73+
"""
74+
do check in for attendee in virtual event
75+
@param data: data
76+
@param attendee_ids: attendee_ids
77+
@param event_id: event_id
78+
"""
79+
current_time = datetime.datetime.now()
80+
if data['is_check_in']:
81+
virtual_check_in = VirtualCheckIn(
82+
ticket_holder_id=attendee_ids,
83+
event_id=event_id,
84+
check_in_type=data['check_in_type'],
85+
check_in_at=current_time,
86+
microlocation_id=data.get('microlocation_id'),
87+
)
88+
else:
89+
virtual_check_in = (
90+
VirtualCheckIn.query.filter(
91+
VirtualCheckIn.ticket_holder_id == attendee_ids,
92+
VirtualCheckIn.event_id == event_id,
93+
VirtualCheckIn.microlocation_id == data.get('microlocation_id'),
94+
)
95+
.order_by(VirtualCheckIn.id.desc())
96+
.first()
97+
)
98+
if virtual_check_in is None:
99+
raise UnprocessableEntityError({'errors': 'Attendee not check in yet'})
100+
if virtual_check_in.check_out_at is not None:
101+
raise UnprocessableEntityError({'errors': 'Attendee Already checked out'})
102+
virtual_check_in.check_out_at = current_time
103+
save_to_db(virtual_check_in)

app/api/schema/virtual_check_in.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from marshmallow import Schema, ValidationError, fields, validate, validates_schema
2+
3+
CHECK_IN_TYPE = ['event', 'virtual-room', 'room']
4+
5+
6+
class VirtualCheckInSchema(Schema):
7+
id = fields.Integer(dump_only=True)
8+
check_in_type = fields.Str(
9+
nullable=False, required=True, validate=validate.OneOf(choices=CHECK_IN_TYPE)
10+
)
11+
microlocation_id = fields.Integer(nullable=True)
12+
is_check_in = fields.Boolean(required=True, nullable=False)
13+
check_in_at = fields.DateTime(dump_only=True)
14+
check_out_at = fields.DateTime(dump_only=True)
15+
16+
@validates_schema
17+
def validate_microlocation_requires_a(self, data):
18+
"""
19+
validate when check_in_type is room, microlocation id is required
20+
@param data: data
21+
"""
22+
if (
23+
'check_in_type' in data
24+
and data['check_in_type'] == 'room'
25+
and 'microlocation_id' not in data
26+
):
27+
raise ValidationError('microlocation_id is required for room check in.')

app/models/user_check_in.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import datetime
22

3+
from sqlalchemy import ARRAY, Integer, func
4+
35
from app.models import db
6+
from app.models.base import SoftDeletionModel
47

58

69
class UserCheckIn(db.Model):
@@ -35,3 +38,35 @@ class UserCheckIn(db.Model):
3538

3639
def __repr__(self):
3740
return f'<User Check In {self.id}>'
41+
42+
43+
class VirtualCheckIn(SoftDeletionModel):
44+
"""Virtual check in database model"""
45+
46+
__tablename__ = 'virtual_check_in'
47+
48+
id = db.Column(db.Integer, primary_key=True)
49+
50+
ticket_holder_id = db.Column(ARRAY(Integer), nullable=True)
51+
52+
event_id = db.Column(db.Integer, db.ForeignKey('events.id', ondelete='SET NULL'))
53+
event = db.relationship('Event', backref='virtual_check_ins')
54+
55+
microlocation_id = db.Column(
56+
db.Integer,
57+
db.ForeignKey('microlocations.id', ondelete='SET NULL'),
58+
nullable=True,
59+
default=None,
60+
)
61+
microlocation = db.relationship('Microlocation', backref='virtual_check_ins')
62+
63+
check_in_type = db.Column(db.String, nullable=False)
64+
check_in_at = db.Column(db.DateTime(timezone=True))
65+
check_out_at = db.Column(db.DateTime(timezone=True))
66+
67+
created_at = db.Column(db.DateTime(timezone=True), default=func.now())
68+
updated_at = db.Column(db.DateTime(timezone=True))
69+
is_deleted = db.Column(db.Boolean, default=False)
70+
71+
def __repr__(self):
72+
return f'<Virtual Check In {self.id}>'
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
"""empty message
2+
3+
Revision ID: bce7acfe5a4f
4+
Revises: 24271525a263
5+
Create Date: 2023-08-17 15:38:43.387065
6+
7+
"""
8+
9+
from alembic import op
10+
import sqlalchemy as sa
11+
12+
13+
# revision identifiers, used by Alembic.
14+
revision = 'bce7acfe5a4f'
15+
down_revision = '24271525a263'
16+
17+
18+
def upgrade():
19+
# ### commands auto generated by Alembic - please adjust! ###
20+
op.create_table('virtual_check_in',
21+
sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True),
22+
sa.Column('id', sa.Integer(), nullable=False),
23+
sa.Column('ticket_holder_id', sa.ARRAY(sa.Integer()), nullable=True),
24+
sa.Column('event_id', sa.Integer(), nullable=True),
25+
sa.Column('microlocation_id', sa.Integer(), nullable=True),
26+
sa.Column('check_in_type', sa.String(), nullable=False),
27+
sa.Column('check_in_at', sa.DateTime(timezone=True), nullable=True),
28+
sa.Column('check_out_at', sa.DateTime(timezone=True), nullable=True),
29+
sa.Column('created_at', sa.DateTime(timezone=True), nullable=True),
30+
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
31+
sa.Column('is_deleted', sa.Boolean(), nullable=True),
32+
sa.ForeignKeyConstraint(['event_id'], ['events.id'], ondelete='SET NULL'),
33+
sa.ForeignKeyConstraint(['microlocation_id'], ['microlocations.id'], ondelete='SET NULL'),
34+
sa.PrimaryKeyConstraint('id')
35+
)
36+
# ### end Alembic commands ###
37+
38+
39+
def downgrade():
40+
# ### commands auto generated by Alembic - please adjust! ###
41+
op.drop_table('virtual_check_in')
42+
# ### end Alembic commands ###

0 commit comments

Comments
 (0)