Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions backend/treeckle/treeckle/common/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
IC_CONTACT_NUMBER = "ic_contact_number"
IC_EMAIL = "ic_email"
IC_NAME = "ic_name"
IC_TELEGRAM_HANDLE = "ic_telegram_handle"
ID = "id"
IS_ACTIVE = "is_active"
NAME = "name"
Expand Down
6 changes: 6 additions & 0 deletions backend/treeckle/venues/logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
IC_NAME,
IC_EMAIL,
IC_CONTACT_NUMBER,
IC_TELEGRAM_HANDLE,
FORM_FIELD_DATA,
ORGANIZATION,
EMAIL,
Expand Down Expand Up @@ -44,6 +45,7 @@ def venue_to_json(venue: Venue, full_details: bool = False) -> dict:
IC_NAME: venue.ic_name,
IC_EMAIL: venue.ic_email,
IC_CONTACT_NUMBER: venue.ic_contact_number,
IC_TELEGRAM_HANDLE: venue.ic_telegram_handle,
FORM_FIELD_DATA: venue.form_field_data,
SHORT_NOTICE_ENABLED: venue.short_notice_enabled,
SHORT_NOTICE_DAYS: venue.short_notice_days,
Expand Down Expand Up @@ -116,6 +118,7 @@ def create_venue(
ic_name: str,
ic_email: str,
ic_contact_number: str,
ic_telegram_handle: str,
form_field_data: list[dict],
short_notice_settings: Optional[dict] = None,
) -> Venue:
Expand All @@ -132,6 +135,7 @@ def create_venue(
ic_name=ic_name,
ic_email=ic_email,
ic_contact_number=ic_contact_number,
ic_telegram_handle=ic_telegram_handle,
form_field_data=form_field_data,
**(short_notice_settings or {}),
)
Expand All @@ -148,6 +152,7 @@ def update_venue(
ic_name: str,
ic_email: str,
ic_contact_number: str,
ic_telegram_handle: str,
form_field_data: list[dict],
short_notice_settings: Optional[dict] = None,
) -> Venue:
Expand All @@ -164,6 +169,7 @@ def update_venue(
"ic_name": ic_name,
"ic_email": ic_email,
"ic_contact_number": ic_contact_number,
"ic_telegram_handle": ic_telegram_handle,
"form_field_data": form_field_data,
**(short_notice_settings or {}),
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Generated by Django 4.2.20 on 2026-10-28 14:35

import django.core.validators
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("venues", "0018_venue_short_notice_settings"),
]

operations = [
migrations.AddField(
model_name="venue",
name="ic_telegram_handle",
field=models.CharField(
blank=True,
max_length=32,
validators=[
django.core.validators.RegexValidator(
"^[A-Za-z0-9_]{5,32}$",
"Telegram handle must be 5-32 letters, numbers or underscores.",
)
],
),
),
]
17 changes: 16 additions & 1 deletion backend/treeckle/venues/models.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
from django.core.validators import MaxValueValidator, MinValueValidator
from django.core.validators import (
MaxValueValidator,
MinValueValidator,
RegexValidator,
)
from django.db import models

from organizations.models import Organization
Expand Down Expand Up @@ -38,6 +42,17 @@ class Venue(TimestampedModel):
ic_name = models.CharField(max_length=255, blank=True)
ic_email = models.EmailField(blank=True)
ic_contact_number = models.CharField(max_length=50, blank=True)
# stored without the leading "@"; Telegram usernames are 5-32 letters, digits or underscores
ic_telegram_handle = models.CharField(
max_length=32,
blank=True,
validators=[
RegexValidator(
r"^[A-Za-z0-9_]{5,32}$",
"Telegram handle must be 5-32 letters, numbers or underscores.",
)
],
)
form_field_data = models.JSONField()
short_notice_enabled = models.BooleanField(default=True)
short_notice_days = models.PositiveIntegerField(
Expand Down
1 change: 1 addition & 0 deletions backend/treeckle/venues/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class Meta:
"ic_name",
"ic_email",
"ic_contact_number",
"ic_telegram_handle",
"form_field_data",
"short_notice_enabled",
"short_notice_days",
Expand Down
16 changes: 16 additions & 0 deletions backend/treeckle/venues/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ def create_test_venue(self, **short_notice_settings):
ic_name="",
ic_email="",
ic_contact_number="",
ic_telegram_handle="",
form_field_data=[],
short_notice_settings=short_notice_settings,
)
Expand All @@ -53,6 +54,7 @@ def test_update_changes_only_given_settings(self):
ic_name="",
ic_email="",
ic_contact_number="",
ic_telegram_handle="",
form_field_data=[],
short_notice_settings={"short_notice_enabled": False},
)
Expand All @@ -67,6 +69,20 @@ def test_serializer_rejects_invalid_days(self):
self.assertFalse(serializer.is_valid())
self.assertIn("short_notice_days", serializer.errors)

def test_serializer_validates_telegram_handle(self):
for handle in ("venue_ic", ""):
serializer = VenueSerializer(
data={**VENUE_DATA, "ic_telegram_handle": handle}
)
self.assertTrue(serializer.is_valid(), serializer.errors)

for handle in ("@venue_ic", "abc", "has space", "a" * 33):
serializer = VenueSerializer(
data={**VENUE_DATA, "ic_telegram_handle": handle}
)
self.assertFalse(serializer.is_valid())
self.assertIn("ic_telegram_handle", serializer.errors)

def test_serializer_omits_missing_settings(self):
serializer = VenueSerializer(data=VENUE_DATA)

Expand Down
7 changes: 7 additions & 0 deletions backend/treeckle/venues/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,7 @@ def delete(
"ic_name": "John Manager",
"ic_email": "john.manager@university.edu",
"ic_contact_number": "+65 9123 4567",
"ic_telegram_handle": "venue_ic",
"form_field_data": [
{
"field_name": "Purpose",
Expand Down Expand Up @@ -351,6 +352,7 @@ def delete(
"ic_name": "Library Staff",
"ic_email": "library@university.edu",
"ic_contact_number": "+65 9876 5432",
"ic_telegram_handle": "venue_ic",
"form_field_data": [
{
"field_name": "Number of Students",
Expand Down Expand Up @@ -424,6 +426,7 @@ def post(self, request, requester: User):
ic_name=validated_data.get("ic_name", ""),
ic_email=validated_data.get("ic_email", ""),
ic_contact_number=validated_data.get("ic_contact_number", ""),
ic_telegram_handle=validated_data.get("ic_telegram_handle", ""),
form_field_data=validated_data.get("form_field_data", []),
short_notice_settings=get_short_notice_settings(validated_data),
)
Expand Down Expand Up @@ -459,6 +462,7 @@ def post(self, request, requester: User):
"ic_name": "Events Manager",
"ic_email": "events.manager@university.edu",
"ic_contact_number": "+65 9111 2222",
"ic_telegram_handle": "venue_ic",
"form_field_data": [
{
"field_name": "Event Type",
Expand Down Expand Up @@ -515,6 +519,7 @@ def post(self, request, requester: User):
"ic_name": "New Events Manager",
"ic_email": "new.events@university.edu",
"ic_contact_number": "+65 9333 4444",
"ic_telegram_handle": "venue_ic",
"form_field_data": [
{
"field_name": "Event Type",
Expand Down Expand Up @@ -566,6 +571,7 @@ def post(self, request, requester: User):
"ic_name": "Events Manager",
"ic_email": "events@university.edu",
"ic_contact_number": "+65 9333 4444",
"ic_telegram_handle": "venue_ic",
"form_field_data": [],
"organization": {"id": 1, "name": "University College"},
},
Expand Down Expand Up @@ -616,6 +622,7 @@ def put(self, request, requester: User, venue: Venue):
ic_name=validated_data.get("ic_name", ""),
ic_email=validated_data.get("ic_email", ""),
ic_contact_number=validated_data.get("ic_contact_number", ""),
ic_telegram_handle=validated_data.get("ic_telegram_handle", ""),
form_field_data=validated_data.get("form_field_data", []),
short_notice_settings=get_short_notice_settings(validated_data),
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ function BookingCreationHelpButton() {
icName,
icEmail,
icContactNumber,
icTelegramHandle,
} = selectedVenue?.venueFormProps ?? {};

const [showModal, hideModal] = useModal(
Expand All @@ -26,7 +27,7 @@ function BookingCreationHelpButton() {
<LinkifyTextViewer>
<h3>For any queries, do contact:</h3>

{icName || icEmail || icContactNumber ? (
{icName || icEmail || icContactNumber || icTelegramHandle ? (
<>
{icName && (
<p>
Expand All @@ -43,6 +44,18 @@ function BookingCreationHelpButton() {
<strong>Contact Number:</strong> {icContactNumber}
</p>
)}
{icTelegramHandle && (
<p>
<strong>Telegram:</strong>{" "}
<a
href={`https://t.me/${icTelegramHandle}`}
target="_blank"
rel="noopener noreferrer"
>
@{icTelegramHandle}
</a>
</p>
)}
</>
) : (
<p>
Expand All @@ -53,7 +66,7 @@ function BookingCreationHelpButton() {
}
/>
),
[venueName, icName, icEmail, icContactNumber],
[venueName, icName, icEmail, icContactNumber, icTelegramHandle],
);

return (
Expand Down
19 changes: 19 additions & 0 deletions frontend/src/components/venue-details-form/venue-details-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
IC_CONTACT_NUMBER,
IC_EMAIL,
IC_NAME,
IC_TELEGRAM_HANDLE,
LABEL,
NAME,
PHONE_NUM_REGEX,
Expand All @@ -21,6 +22,7 @@ import {
SHORT_NOTICE_ENABLED,
SHORT_NOTICE_MESSAGE,
SHORT_NOTICE_TITLE,
TELEGRAM_HANDLE_REGEX,
TYPE,
} from "../../constants";
import {
Expand Down Expand Up @@ -68,6 +70,16 @@ const SCHEMA = yup.object().shape({
.trim()
.matches(PHONE_NUM_REGEX, "Input must be a valid phone number")
.notRequired(),
[IC_TELEGRAM_HANDLE]: yup
.string()
.trim()
// admins may type "@handle"; it is stored without the "@"
.transform((value?: string) => value?.replace(/^@/, ""))
.matches(TELEGRAM_HANDLE_REGEX, {
message: "Handle must be 5-32 letters, numbers or underscores",
excludeEmptyString: true,
})
.notRequired(),
[BOOKING_FORM_FIELDS]: yup
.array(
yup
Expand Down Expand Up @@ -170,6 +182,7 @@ const DEFAULT_VALUES: VenueFormProps = {
[IC_NAME]: "",
[IC_EMAIL]: "",
[IC_CONTACT_NUMBER]: "",
[IC_TELEGRAM_HANDLE]: "",
[BOOKING_FORM_FIELDS]: [],
...DEFAULT_SHORT_NOTICE_WARNING,
};
Expand Down Expand Up @@ -267,6 +280,12 @@ function VenueDetailsForm({
name={IC_CONTACT_NUMBER}
type="tel"
/>

<FormField
label="Venue IC Telegram Handle"
name={IC_TELEGRAM_HANDLE}
placeholder="@username"
/>
</Form.Group>
</Segment>

Expand Down
2 changes: 2 additions & 0 deletions frontend/src/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export const FORM_RESPONSE_DATA = "formResponseData";
export const IC_CONTACT_NUMBER = "icContactNumber";
export const IC_EMAIL = "icEmail";
export const IC_NAME = "icName";
export const IC_TELEGRAM_HANDLE = "icTelegramHandle";
export const ID = "id";
export const IDS = "ids";
export const IMAGE = "image";
Expand All @@ -47,6 +48,7 @@ export const ORGANIZED_BY = "organizedBy";
export const PHONE_NUM_REGEX =
/^(\s*|(\+?\d{0,4})?\s?-?\s?(\(?\d{3}\)?)\s?-?\s?(\(?\d{3}\)?)\s?-?\s?(\(?\d{4}\)?)?)$/;
export const PLACEHOLDER = "placeholder";
export const TELEGRAM_HANDLE_REGEX = /^[A-Za-z0-9_]{5,32}$/;
export const POSITIVE_NUM_REGEX = /^(\s*|[1-9]\d*)$/;
export const REFRESH = "refresh";
export const REQUIRED = "required";
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/custom-hooks/api/venues-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ function parseVenueFormProps(venueFormProps: VenueFormProps): VenuePostData {
icName,
icEmail,
icContactNumber,
icTelegramHandle,
bookingFormFields,
shortNoticeEnabled,
shortNoticeDays,
Expand All @@ -51,6 +52,7 @@ function parseVenueFormProps(venueFormProps: VenueFormProps): VenuePostData {
icName,
icEmail,
icContactNumber,
icTelegramHandle,
formFieldData: bookingFormFields ?? [],
shortNoticeEnabled,
shortNoticeDays,
Expand All @@ -72,6 +74,7 @@ function parseVenueData(venueData: VenueData): VenueViewProps {
icName = "",
icEmail = "",
icContactNumber = "",
icTelegramHandle = "",
formFieldData,
organization = "",
shortNoticeEnabled = DEFAULT_SHORT_NOTICE_WARNING.shortNoticeEnabled,
Expand All @@ -92,6 +95,7 @@ function parseVenueData(venueData: VenueData): VenueViewProps {
icName,
icEmail,
icContactNumber,
icTelegramHandle,
bookingFormFields: formFieldData,
shortNoticeEnabled,
shortNoticeDays,
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/types/venues.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
IC_CONTACT_NUMBER,
IC_EMAIL,
IC_NAME,
IC_TELEGRAM_HANDLE,
LABEL,
NAME,
ORGANIZATION,
Expand All @@ -34,6 +35,7 @@ export type VenuePostData = {
[IC_NAME]: string;
[IC_EMAIL]: string;
[IC_CONTACT_NUMBER]: string;
[IC_TELEGRAM_HANDLE]: string;
[FORM_FIELD_DATA]: BookingFormFieldProps[];
} & ShortNoticeWarningProps;

Expand Down Expand Up @@ -74,6 +76,7 @@ export type VenueFormProps = {
[IC_NAME]: string;
[IC_EMAIL]: string;
[IC_CONTACT_NUMBER]: string;
[IC_TELEGRAM_HANDLE]: string;
[BOOKING_FORM_FIELDS]?: BookingFormFieldProps[];
} & ShortNoticeWarningProps;

Expand Down
Loading