Skip to content

Commit bc2de7f

Browse files
authored
Merge pull request #301 from SPARCS-UP-Mindanao/stage
Devfest Release November 9
2 parents 6d74f32 + ed933ef commit bc2de7f

33 files changed

Lines changed: 793 additions & 130 deletions

backend/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ node_modules
88
.env
99
.env.*
1010
.ruff_cache
11+
pyrightconfig.json

backend/constants/common_constants.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,10 @@ class EmailType(str, Enum):
6363
class SpecialEmails(str, Enum):
6464
DURIAN_PY = 'durianpy.davao@gmail.com'
6565
AWSUG_DAVAO = 'hello@awsugdavao.ph'
66+
GDG_DAVAO = 'davao.gdg@gmail.com'
6667

6768

6869
class SpecialSenders(str, Enum):
6970
DURIAN_PY = 'DurianPy - Davao Python User Group'
7071
AWSUG_DAVAO = 'AWS User Group Davao'
72+
GDG_DAVAO = 'Google Developer Group Davao'

backend/controller/preregistration_router.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from constants.common_constants import CommonConstants
66
from fastapi import APIRouter, Depends, Path, Query
77
from model.common import Message
8+
from model.file_uploads.file_upload import FileDownloadOut
89
from model.preregistrations.preregistration import (
910
PreRegistrationIn,
1011
PreRegistrationOut,
@@ -197,7 +198,9 @@ def update_preregistration(
197198
_ = current_user
198199
preregistrations_uc = PreRegistrationUsecase()
199200
return preregistrations_uc.update_preregistration(
200-
event_id=event_id, preregistration_id=entry_id, preregistration_in=preregistration
201+
event_id=event_id,
202+
preregistration_id=entry_id,
203+
preregistration_in=preregistration,
201204
)
202205

203206

@@ -237,3 +240,35 @@ def delete_preregistration(
237240
_ = current_user
238241
preregistrations_uc = PreRegistrationUsecase()
239242
return preregistrations_uc.delete_preregistration(preregistration_id=entry_id, event_id=event_id)
243+
244+
245+
@preregistration_router.get(
246+
'/{eventId}/csv_download',
247+
response_model=FileDownloadOut,
248+
responses={
249+
404: {'model': Message, 'description': 'Pre-registration not found'},
250+
500: {'model': Message, 'description': 'Internal server error'},
251+
},
252+
summary='Get CSV for pre-registration',
253+
)
254+
@preregistration_router.get(
255+
'/{eventId}/csv_download/',
256+
response_model=FileDownloadOut,
257+
response_model_exclude_none=True,
258+
response_model_exclude_unset=True,
259+
include_in_schema=False,
260+
)
261+
def get_preregistration_csv(
262+
event_id: str = Path(..., title='Event Id', alias=CommonConstants.EVENT_ID),
263+
):
264+
"""Get the CSV for a specific event
265+
266+
:param event_id: The event ID.
267+
:type event_id: str
268+
269+
:return: The csv for the corresponding event
270+
:rtype: FileDownloadOut
271+
272+
"""
273+
preregistrations_uc = PreRegistrationUsecase()
274+
return preregistrations_uc.get_preregistration_csv(event_id=event_id)

backend/controller/registration_router.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from constants.common_constants import CommonConstants
66
from fastapi import APIRouter, Depends, Path, Query
77
from model.common import Message
8+
from model.file_uploads.file_upload import FileDownloadOut
89
from model.registrations.registration import (
910
RegistrationIn,
1011
RegistrationOut,
@@ -183,3 +184,35 @@ def delete_registration(
183184
_ = current_user
184185
registrations_uc = RegistrationUsecase()
185186
return registrations_uc.delete_registration(registration_id=entry_id, event_id=event_id)
187+
188+
189+
@registration_router.get(
190+
'/{eventId}/csv_download',
191+
response_model=FileDownloadOut,
192+
responses={
193+
404: {'model': Message, 'description': 'Registration not found'},
194+
500: {'model': Message, 'description': 'Internal server error'},
195+
},
196+
summary='Get CSV for registration',
197+
)
198+
@registration_router.get(
199+
'/{eventId}/csv_download/',
200+
response_model=FileDownloadOut,
201+
response_model_exclude_none=True,
202+
response_model_exclude_unset=True,
203+
include_in_schema=False,
204+
)
205+
def get_registration_csv(
206+
event_id: str = Path(..., title='Event Id', alias=CommonConstants.EVENT_ID),
207+
):
208+
"""Get the CSV for a specific event
209+
210+
:param event_id: The event ID.
211+
:type event_id: str
212+
213+
:return: The csv for the corresponding event
214+
:rtype: FileDownloadOut
215+
216+
"""
217+
registrations_uc = RegistrationUsecase()
218+
return registrations_uc.get_registration_csv(event_id=event_id)

backend/model/events/event.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ class Meta:
7070
konfhubId = UnicodeAttribute(null=True)
7171
konfhubApiKey = UnicodeAttribute(null=True)
7272

73+
platformFee = NumberAttribute(null=True)
74+
7375
eventIdIndex = EventIdIndex()
7476

7577

@@ -98,6 +100,8 @@ class Config:
98100
konfhubId: Optional[str] = Field(None, title='Konfhub ID')
99101
konfhubApiKey: Optional[str] = Field(None, title='Konfhub API Key')
100102

103+
platformFee: Optional[float] = Field(None, title='Percent platform fee')
104+
101105

102106
class EventIn(EventDBIn):
103107
class Config:

backend/model/konfhub/konfhub.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
1-
from typing import Dict, List
1+
from typing import Dict, List, Optional
22

33
from pydantic import BaseModel, EmailStr, Field
44

55

66
class RegistrationDetail(BaseModel):
7-
name: str = Field(..., description='Name of the registrant')
8-
email_id: EmailStr = Field(..., description='Email ID of the registrant')
9-
quantity: int = Field(..., description='Quantity of the registrant')
10-
designation: str = Field(..., description='Designation of the registrant')
11-
organisation: str = Field(..., description='Organisation of the registrant')
12-
t_shirt_size: str = Field(..., description='T-shirt size of the registrant')
13-
phone_number: str = Field(..., description='Phone number of the registrant')
14-
dial_code: str = Field(..., description='Dial code of the registrant')
15-
country_code: str = Field(..., description='Country code of the registrant')
7+
name: Optional[str] = Field(None, description='Name of the registrant')
8+
email_id: Optional[EmailStr] = Field(None, description='Email ID of the registrant')
9+
quantity: Optional[int] = Field(None, description='Quantity of the registrant')
10+
designation: Optional[str] = Field(None, description='Designation of the registrant')
11+
organisation: Optional[str] = Field(None, description='Organisation of the registrant')
12+
phone_number: Optional[str] = Field(None, description='Phone number of the registrant')
13+
dial_code: Optional[str] = Field(None, description='Dial code of the registrant')
14+
country_code: Optional[str] = Field(None, description='Country code of the registrant')
15+
t_shirt_size: Optional[str] = Field(None, description='T-shirt size of the registrant')
1616

1717

1818
class KonfHubCaptureRegistrationIn(BaseModel):

backend/model/preregistrations/preregistration.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ class Config:
7878

7979
class PreRegistrationPatch(PreRegistrationaDataIn):
8080
class Config:
81-
extra = Extra.forbid
81+
extra = Extra.ignore
8282

8383
acceptanceStatus: Optional[AcceptanceStatus] = Field(None, title='Acceptance Status')
8484
acceptanceEmailSent: bool = Field(None, title='Acceptance Email Sent')
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
import csv
2+
import re
3+
from datetime import datetime
4+
from enum import Enum
5+
from typing import Optional, Tuple
6+
7+
import pytz
8+
import ulid
9+
from constants.common_constants import EntryStatus
10+
from model.preregistrations.preregistration import PreRegistration, PreRegistrationIn
11+
from model.preregistrations.preregistrations_constants import AcceptanceStatus
12+
from pydantic import BaseModel, EmailStr, Field, validator
13+
14+
15+
class CareerStatus(str, Enum):
16+
STUDENT = 'Student'
17+
WORKING_STUDENT = 'Working Student'
18+
WORKING_PROFESSIONAL = 'Working Professional'
19+
ENTREPRENEUR = 'Entrepreneur'
20+
21+
22+
class DeveloperStatus(str, Enum):
23+
DEVELOPER = 'Developer'
24+
NON_DEVELOPER = 'Non-developer'
25+
26+
27+
class LevelOfExperience(str, Enum):
28+
BEGINNER = 'Beginner'
29+
INTERMEDIATE = 'Intermediate'
30+
ADVANCED = 'Advanced'
31+
32+
33+
class DevFestColumns:
34+
TIMESTAMP = 'Timestamp'
35+
EMAIL = 'Username'
36+
NAME = 'Name'
37+
PHONE_NUMBER = 'Phone number'
38+
CAREER_STATUS = 'Which of the options best describe you?'
39+
DEVELOPER_STATUS = 'Are you a'
40+
JOB_TITLE = 'Job title'
41+
LEVEL_OF_EXPERIENCE = 'Level of expertise'
42+
COMPANY_AFFILIATION = 'Company or affiliation'
43+
GENDER = 'Gender'
44+
WHY_INTERESTED_IN_DEVFEST = 'Why are you interested in joining DevFest Davao 2024?'
45+
HOW_DID_YOU_LEARN_ABOUT_DEVFEST = 'How did you learn about the event?'
46+
INTEREST_IN_JOINING_AI_WORKSHOP = 'Are you interested in joining an AI/ML (Machine Learning) hands-on workshop?'
47+
PREFERRED_SOCIAL_MEDIA = (
48+
'When it comes to joining a group chat community, which social media platform/s would you prefer?'
49+
)
50+
PROCESSING_OF_PERSONAL_DATA = 'I hereby consent to the processing of the personal data that I have provided and declare my agreement with the data protection regulations in the data privacy statement.'
51+
52+
53+
class DevFestPreRegistrationSchema(BaseModel):
54+
timestamp: str = Field(title=DevFestColumns.TIMESTAMP)
55+
email: EmailStr = Field(None, title=DevFestColumns.EMAIL)
56+
name: str = Field(None, title=DevFestColumns.NAME)
57+
phone_number: str = Field(
58+
None,
59+
title=DevFestColumns.PHONE_NUMBER,
60+
regex=r'^(09)\d{9}$', # Philippine mobile number
61+
)
62+
career_status: Optional[CareerStatus] = Field(None, title=DevFestColumns.CAREER_STATUS)
63+
developer_status: Optional[DeveloperStatus] = Field(None, title=DevFestColumns.DEVELOPER_STATUS)
64+
job_title: Optional[str] = Field(None, title=DevFestColumns.JOB_TITLE)
65+
level_of_experience: Optional[LevelOfExperience] = Field(None, title=DevFestColumns.LEVEL_OF_EXPERIENCE)
66+
company_affiliation: Optional[str] = Field(None, title=DevFestColumns.COMPANY_AFFILIATION)
67+
gender: Optional[str] = Field(None, title=DevFestColumns.GENDER)
68+
why_interested_in_devfest: Optional[str] = Field(None, title=DevFestColumns.WHY_INTERESTED_IN_DEVFEST)
69+
how_did_you_learn_about_devfest: Optional[str] = Field(None, title=DevFestColumns.HOW_DID_YOU_LEARN_ABOUT_DEVFEST)
70+
interest_in_joining_ai_workshop: Optional[bool] = Field(None, title=DevFestColumns.INTEREST_IN_JOINING_AI_WORKSHOP)
71+
preferred_social_media: Optional[str] = Field(None, title=DevFestColumns.PREFERRED_SOCIAL_MEDIA)
72+
processing_of_personal_data: Optional[str] = Field(None, title=DevFestColumns.PROCESSING_OF_PERSONAL_DATA)
73+
74+
@validator('phone_number', pre=True)
75+
def strip_and_clean_phone_number(cls, value):
76+
"""Strip and clean phone number to remove non-numeric characters before validation.
77+
78+
:param value: Phone number string.
79+
:type value: str
80+
81+
:return: Cleaned phone number string.
82+
:rtype: str
83+
"""
84+
if value is None or value == '':
85+
return None
86+
87+
value = re.sub(r'\D', '', value)
88+
89+
return value
90+
91+
92+
def separate_first_and_last_name(name: str) -> Tuple[str, str]:
93+
"""Separate first and last name from a full name string, excluding any middle initials.
94+
95+
The last name is assumed to be the last word in the full name string, while the first name
96+
consists of all preceding words before the last name.
97+
98+
:param name: Full name string.
99+
:type name: str
100+
101+
:return: A tuple containing the first name and last name, in that order.
102+
:rtype: Tuple[str, str]
103+
104+
Examples:
105+
>>> separate_first_and_last_name("John Doe")
106+
('John', 'Doe')
107+
108+
>>> separate_first_and_last_name("Juan Manuel C. Cruz")
109+
('Juan Manuel', 'Cruz')
110+
"""
111+
112+
name_parts = name.split()
113+
114+
if len(name_parts) > 2:
115+
first_name = ' '.join(name_parts[:-2] + [name_parts[-2] if len(name_parts[-2]) > 2 else '']).strip()
116+
else:
117+
first_name = name_parts[0]
118+
119+
last_name = name_parts[-1] if len(name_parts) > 1 else 'N/A'
120+
121+
return first_name, last_name
122+
123+
124+
def import_pre_registration_from_csv(eventId: str, csv_file_path: str = 'input.csv') -> None:
125+
"""Import pre-registration data from a CSV file to a PreRegistration table.
126+
127+
:param eventId: Event ID.
128+
:type eventId: str
129+
130+
:param csv_file_path: Path to the CSV file containing pre-registration data.
131+
:type csv_file_path: str
132+
133+
:return: None
134+
:rtype: None
135+
"""
136+
137+
with open(csv_file_path, mode='r') as csv_file:
138+
csv_reader = csv.reader(csv_file)
139+
headers = next(csv_reader)
140+
pre_registration_data_rows: list[DevFestPreRegistrationSchema] = []
141+
142+
for line in csv_reader:
143+
# Dictionary mapping of CSV columns to DevFestPreRegistrationSchema fields
144+
pre_registration_data = {
145+
'timestamp': line[headers.index(DevFestColumns.TIMESTAMP)],
146+
'email': line[headers.index(DevFestColumns.EMAIL)],
147+
'name': line[headers.index(DevFestColumns.NAME)],
148+
'phone_number': line[headers.index(DevFestColumns.PHONE_NUMBER)].strip(),
149+
'career_status': line[headers.index(DevFestColumns.CAREER_STATUS)],
150+
'developer_status': line[headers.index(DevFestColumns.DEVELOPER_STATUS)],
151+
'job_title': line[headers.index(DevFestColumns.JOB_TITLE)],
152+
'level_of_experience': line[headers.index(DevFestColumns.LEVEL_OF_EXPERIENCE)],
153+
'company_affiliation': line[headers.index(DevFestColumns.COMPANY_AFFILIATION)],
154+
'gender': line[headers.index(DevFestColumns.GENDER)],
155+
'why_interested_in_devfest': line[headers.index(DevFestColumns.WHY_INTERESTED_IN_DEVFEST)],
156+
'how_did_you_learn_about_devfest': line[headers.index(DevFestColumns.HOW_DID_YOU_LEARN_ABOUT_DEVFEST)],
157+
'interest_in_joining_ai_workshop': bool(
158+
line[headers.index(DevFestColumns.INTEREST_IN_JOINING_AI_WORKSHOP)]
159+
),
160+
'preferred_social_media': line[headers.index(DevFestColumns.PREFERRED_SOCIAL_MEDIA)],
161+
'processing_of_personal_data': line[headers.index(DevFestColumns.PROCESSING_OF_PERSONAL_DATA)],
162+
}
163+
164+
pre_registration_data_rows.append(DevFestPreRegistrationSchema(**pre_registration_data))
165+
166+
with PreRegistration.batch_write() as batch:
167+
current_date = datetime.now(tz=pytz.timezone('Asia/Manila')).isoformat()
168+
items = []
169+
170+
for pre_registration in pre_registration_data_rows:
171+
pre_registration_id = ulid.ulid()
172+
first_name, last_name = separate_first_and_last_name(pre_registration.name)
173+
174+
preregistration_in = PreRegistrationIn(
175+
email=pre_registration.email,
176+
firstName=first_name,
177+
lastName=last_name,
178+
contactNumber=pre_registration.phone_number,
179+
careerStatus=pre_registration.career_status,
180+
yearsOfExperience=pre_registration.level_of_experience,
181+
organization=pre_registration.company_affiliation,
182+
title=pre_registration.job_title,
183+
eventId=eventId,
184+
)
185+
186+
items.append(
187+
PreRegistration(
188+
hashKey=eventId,
189+
rangeKey=pre_registration_id,
190+
createDate=current_date,
191+
updateDate=current_date,
192+
entryStatus=EntryStatus.ACTIVE.value,
193+
preRegistrationId=pre_registration_id,
194+
acceptanceStatus=AcceptanceStatus.ACCEPTED.value,
195+
**preregistration_in.dict(),
196+
)
197+
)
198+
199+
for item in items:
200+
batch.save(item)
201+
202+
203+
if __name__ == '__main__':
204+
event_id = 'testevent'
205+
csv_file_path = 'scripts/input.csv'
206+
import_pre_registration_from_csv(eventId=event_id, csv_file_path=csv_file_path)

0 commit comments

Comments
 (0)