Skip to content

Commit 2c55fc2

Browse files
author
Hieu Lam - TMA
authored
feature-8684: Add option to tag attendees (#9072)
* feature-8684: Add option to tag attendees * feature-8684: Merge code development * feature-8684: Add option to tag attendees * feature-8684: Add option to tag attendees * feature-8684: Add option to tag attendees * feature-8684: Add option to tag attendees * feature-8684: Add option to tag attendees
1 parent 1f76855 commit 2c55fc2

File tree

8 files changed

+259
-0
lines changed

8 files changed

+259
-0
lines changed

app/api/routes.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,7 @@
241241
StripeAuthorizationListPost,
242242
StripeAuthorizationRelationship,
243243
)
244+
from app.api.tags import TagDetail, TagList, TagListPost, TagRelationship
244245
from app.api.tax import TaxDetail, TaxList, TaxRelationship
245246
from app.api.ticket_fees import TicketFeeDetail, TicketFeeList
246247
from app.api.ticket_tags import (
@@ -840,6 +841,7 @@
840841
'/speaker-invites/<int:speaker_invite_id>/event',
841842
'/stations/<int:station_id>/event',
842843
'/badge-forms/<int:badge_form_id>/event',
844+
'/tags/<int:tag_id>/event',
843845
)
844846
api.route(
845847
EventRelationship,
@@ -1021,6 +1023,12 @@
10211023
'/events/<int:id>/relationships/group',
10221024
'/events/<identifier>/relationships/group',
10231025
)
1026+
api.route(
1027+
EventRelationship,
1028+
'event_tags',
1029+
'/events/<int:id>/relationships/tags',
1030+
'/events/<identifier>/relationships/tags',
1031+
)
10241032
# Events -> roles:
10251033
api.route(
10261034
EventRelationship,
@@ -1530,6 +1538,11 @@
15301538
'attendee_user',
15311539
'/attendees/<int:id>/relationships/user',
15321540
)
1541+
api.route(
1542+
AttendeeRelationshipOptional,
1543+
'attendee_tag',
1544+
'/attendees/<int:id>/relationships/tag',
1545+
)
15331546

15341547
# event locations
15351548
api.route(EventLocationList, 'event_location_list', '/event-locations')
@@ -2198,3 +2211,13 @@
21982211
'badge_field_form_badge_form',
21992212
'/badge-field-forms/<int:id>/relationships/badge_form',
22002213
)
2214+
2215+
api.route(TagListPost, 'tag_post', '/tags')
2216+
api.route(TagList, 'tags_list', '/events/<int:event_id>/tags')
2217+
api.route(
2218+
TagDetail,
2219+
'tags_detail',
2220+
'/tags/<int:id>',
2221+
'/attendees/<int:attendee_id>/tags',
2222+
)
2223+
api.route(TagRelationship, 'tags_event', '/tags/<int:id>/relationships/event')

app/api/schema/attendees.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ def validate_json(self, data, original_data):
8282
is_consent_form_field_email = fields.Boolean(allow_none=True)
8383
is_badge_printed = fields.Boolean(allow_none=True)
8484
badge_printed_at = fields.DateTime(allow_none=True)
85+
tag_id = fields.Int(allow_none=True)
8586
event = Relationship(
8687
self_view='v1.attendee_event',
8788
self_view_kwargs={'id': '<id>'},
@@ -109,6 +110,14 @@ def validate_json(self, data, original_data):
109110
type_='ticket',
110111
dump_only=True,
111112
)
113+
tag = Relationship(
114+
self_view='v1.attendee_tag',
115+
self_view_kwargs={'id': '<id>'},
116+
related_view='v1.tags_detail',
117+
related_view_kwargs={'attendee_id': '<id>'},
118+
schema='TagSchema',
119+
type_='tag',
120+
)
112121

113122

114123
class AttendeeSchema(AttendeeSchemaPublic):

app/api/schema/events.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,16 @@ def validate_timezone(self, data, original_data):
404404
many=True,
405405
type_='badge-form',
406406
)
407+
tags = Relationship(
408+
attribute='tag',
409+
self_view='v1.event_tags',
410+
self_view_kwargs={'id': '<id>'},
411+
related_view='v1.tags_list',
412+
related_view_kwargs={'event_id': '<id>'},
413+
schema='TagSchema',
414+
many=True,
415+
type_='tag',
416+
)
407417

408418

409419
class EventSchema(EventSchemaPublic):

app/api/schema/tag.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
from marshmallow_jsonapi import fields
2+
from marshmallow_jsonapi.flask import Relationship
3+
4+
from app.api.helpers.utilities import dasherize
5+
from app.api.schema.base import SoftDeletionSchema
6+
7+
8+
class TagSchema(SoftDeletionSchema):
9+
"""API Schema for user tag Model"""
10+
11+
class Meta:
12+
"""Meta class for user tag API schema"""
13+
14+
type_ = 'tag'
15+
self_view = 'v1.tags_detail'
16+
self_view_kwargs = {'id': '<id>'}
17+
inflect = dasherize
18+
19+
id = fields.Str(dump_only=True)
20+
name = fields.Str(required=True)
21+
color = fields.Str(allow_none=True)
22+
is_read_only = fields.Boolean(required=False)
23+
event = Relationship(
24+
self_view='v1.tags_event',
25+
self_view_kwargs={'id': '<id>'},
26+
related_view='v1.event_detail',
27+
related_view_kwargs={'tag_id': '<id>'},
28+
schema='EventSchema',
29+
type_='event',
30+
)

app/api/tags.py

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
from flask_rest_jsonapi import ResourceDetail, ResourceList, ResourceRelationship
2+
3+
from app.api.helpers.db import get_count, safe_query_kwargs
4+
from app.api.helpers.errors import ConflictError
5+
from app.api.helpers.utilities import require_relationship
6+
from app.api.schema.tag import TagSchema
7+
from app.models import db
8+
from app.models.event import Event
9+
from app.models.tag import Tag
10+
from app.models.ticket_holder import TicketHolder
11+
12+
13+
class TagList(ResourceList):
14+
"""List User Emails for a user"""
15+
16+
def query(self, view_kwargs):
17+
"""
18+
query method for Notifications list
19+
:param view_kwargs:
20+
:return:
21+
"""
22+
query_ = self.session.query(Tag)
23+
if view_kwargs.get('event_id'):
24+
event = safe_query_kwargs(Event, view_kwargs, 'event_id')
25+
query_ = (
26+
query_.join(Event).filter(Event.id == event.id).order_by(Tag.id.asc())
27+
)
28+
return query_
29+
30+
view_kwargs = True
31+
methods = [
32+
"GET",
33+
]
34+
schema = TagSchema
35+
data_layer = {'session': db.session, 'model': Tag, 'methods': {'query': query}}
36+
37+
38+
class TagListPost(ResourceList):
39+
"""Create new alternate email for a user"""
40+
41+
@staticmethod
42+
def before_post(_args, _kwargs, data):
43+
"""
44+
before post method to check for required relationship and proper permission
45+
:param args:
46+
:param kwargs:
47+
:param data:
48+
:return:
49+
"""
50+
require_relationship(['event'], data)
51+
if (
52+
get_count(
53+
db.session.query(Tag.id).filter_by(
54+
name=data.get('name'),
55+
event_id=int(data['event']),
56+
deleted_at=None,
57+
)
58+
)
59+
> 0
60+
):
61+
raise ConflictError(
62+
{'pointer': '/data/attributes/name'}, "Name already exists"
63+
)
64+
65+
schema = TagSchema
66+
methods = [
67+
'POST',
68+
]
69+
data_layer = {
70+
'session': db.session,
71+
'model': Tag,
72+
'methods': {'before_post': before_post},
73+
}
74+
75+
76+
class TagDetail(ResourceDetail):
77+
"""User Email detail by id"""
78+
79+
@staticmethod
80+
def before_patch(_obj, _kwargs, data):
81+
"""
82+
before patch method to check for required relationship and proper permission
83+
:param args:
84+
:param kwargs:
85+
:param data:
86+
:return:
87+
"""
88+
require_relationship(['event'], data)
89+
tag = (
90+
db.session.query(Tag)
91+
.filter_by(
92+
name=data.get('name'), event_id=int(data['event']), deleted_at=None
93+
)
94+
.first()
95+
)
96+
if tag and tag.is_read_only and tag.name != data.get('name'):
97+
raise ConflictError(
98+
{'pointer': '/data/attributes/is_read_only'},
99+
"Cannot update read-only tag",
100+
)
101+
102+
@staticmethod
103+
def before_delete(_obj, kwargs):
104+
"""
105+
before delete method to check for required relationship and proper permission
106+
:param args:
107+
:param kwargs:
108+
:param data:
109+
:return:
110+
"""
111+
ticketHolders = TicketHolder.query.filter_by(tag_id=kwargs['id']).all()
112+
for item in ticketHolders:
113+
item.tag_id = None
114+
db.session.add(item)
115+
116+
schema = TagSchema
117+
data_layer = {
118+
'session': db.session,
119+
'model': Tag,
120+
'methods': {'before_delete': before_delete, before_patch: 'before_patch'},
121+
}
122+
123+
124+
class TagRelationship(ResourceRelationship):
125+
"""User Email Relationship"""
126+
127+
schema = TagSchema
128+
data_layer = {'session': db.session, 'model': Tag}

app/models/tag.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from app.models import db
2+
from app.models.base import SoftDeletionModel
3+
4+
5+
class Tag(SoftDeletionModel):
6+
"""Tag model class"""
7+
8+
__tablename__ = 'tags'
9+
id = db.Column(db.Integer, primary_key=True)
10+
name = db.Column(db.String, nullable=False)
11+
color = db.Column(db.String)
12+
is_read_only = db.Column(db.Boolean, nullable=False, default=False)
13+
event_id = db.Column(db.Integer, db.ForeignKey('events.id', ondelete='CASCADE'))
14+
event = db.relationship("Event", backref="tags_", foreign_keys=[event_id])
15+
16+
def __repr__(self):
17+
return f'<Tag {self.id!r}>'

app/models/ticket_holder.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ class TicketHolder(SoftDeletionModel):
8686
is_consent_form_field_email: bool = db.Column(db.Boolean, default=False)
8787
is_badge_printed: bool = db.Column(db.Boolean, default=False)
8888
badge_printed_at: datetime = db.Column(db.DateTime(timezone=True))
89+
tag_id: int = db.Column(db.Integer, db.ForeignKey('tags.id', ondelete='CASCADE'))
90+
tag = db.relationship('Tag', backref='ticket_holders')
8991

9092
@property
9193
def name(self):
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
"""empty message
2+
3+
Revision ID: 1af4cc4f7cd5
4+
Revises: 24271525a263
5+
Create Date: 2023-08-11 15:26:07.373388
6+
7+
"""
8+
9+
from alembic import op
10+
import sqlalchemy as sa
11+
12+
13+
# revision identifiers, used by Alembic.
14+
revision = '1af4cc4f7cd5'
15+
down_revision = '24271525a263'
16+
17+
18+
def upgrade():
19+
# ### commands auto generated by Alembic - please adjust! ###
20+
op.create_table('tags',
21+
sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True),
22+
sa.Column('id', sa.Integer(), nullable=False),
23+
sa.Column('name', sa.String(), nullable=False),
24+
sa.Column('color', sa.String(), nullable=True),
25+
sa.Column('is_read_only', sa.Boolean(), nullable=False),
26+
sa.Column('event_id', sa.Integer(), nullable=True),
27+
sa.ForeignKeyConstraint(['event_id'], ['events.id'], ondelete='CASCADE'),
28+
sa.PrimaryKeyConstraint('id')
29+
)
30+
op.add_column('ticket_holders', sa.Column('tag_id', sa.Integer(), nullable=True))
31+
op.create_foreign_key(u'ticket_holders_tag_id_fkey', 'ticket_holders', 'tags', ['tag_id'], ['id'], ondelete='CASCADE')
32+
# ### end Alembic commands ###
33+
34+
35+
def downgrade():
36+
# ### commands auto generated by Alembic - please adjust! ###
37+
op.drop_constraint(u'ticket_holders_tag_id_fkey', 'ticket_holders', type_='foreignkey')
38+
op.drop_column('ticket_holders', 'tag_id')
39+
op.drop_table('tags')
40+
# ### end Alembic commands ###

0 commit comments

Comments
 (0)