diff --git a/backend/api/profile.py b/backend/api/profile.py index e74042a4a..e05c37259 100644 --- a/backend/api/profile.py +++ b/backend/api/profile.py @@ -69,6 +69,8 @@ def update_profile( bio=profile.bio, linkedin=profile.linkedin, website=profile.website, + profile_emoji=profile.profile_emoji, + emoji_expiration=profile.emoji_expiration, ) user = user_svc.create(user, user) else: @@ -81,6 +83,8 @@ def update_profile( user.bio = profile.bio user.linkedin = profile.linkedin user.website = profile.website + user.profile_emoji = profile.profile_emoji + user.emoji_expiration = profile.emoji_expiration user = user_svc.update(user, user) user_details = user_svc.get(user.pid) diff --git a/backend/entities/user_entity.py b/backend/entities/user_entity.py index daeb4ae5d..755f7cef3 100644 --- a/backend/entities/user_entity.py +++ b/backend/entities/user_entity.py @@ -1,8 +1,9 @@ """Definition of SQLAlchemy table-backed object mapping entity for Users.""" -from sqlalchemy import Boolean, Integer, String +from sqlalchemy import Boolean, Integer, String, DateTime from sqlalchemy.orm import Mapped, mapped_column, relationship from typing import Self +from datetime import datetime from backend.entities.academics.section_member_entity import SectionMemberEntity from backend.models.academics.section_member import SectionMember @@ -54,6 +55,10 @@ class UserEntity(EntityBase): linkedin: Mapped[str | None] = mapped_column(String(), nullable=True) # Website of the user website: Mapped[str | None] = mapped_column(String(), nullable=True) + # Profile emoji for the user + profile_emoji: Mapped[str | None] = mapped_column(String(10), nullable=True) + # Expiration time for the profile emoji + emoji_expiration: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) # All of the roles for the given user. # NOTE: This field establishes a many-to-many relationship between the users and roles table. @@ -124,6 +129,8 @@ def from_model(cls, model: User) -> Self: bio=model.bio, linkedin=model.linkedin, website=model.website, + profile_emoji=model.profile_emoji, + emoji_expiration=model.emoji_expiration, ) def to_model(self) -> User: @@ -148,6 +155,8 @@ def to_model(self) -> User: bio=self.bio, linkedin=self.linkedin, website=self.website, + profile_emoji=self.profile_emoji, + emoji_expiration=self.emoji_expiration, ) def update(self, model: User) -> None: @@ -171,6 +180,8 @@ def update(self, model: User) -> None: self.bio = model.bio self.linkedin = model.linkedin self.website = model.website + self.profile_emoji = model.profile_emoji + self.emoji_expiration = model.emoji_expiration def to_public_model(self) -> PublicUser: return PublicUser( @@ -185,4 +196,6 @@ def to_public_model(self) -> PublicUser: bio=self.bio, linkedin=self.linkedin, website=self.website, + profile_emoji=self.profile_emoji, + emoji_expiration=self.emoji_expiration, ) diff --git a/backend/migrations/versions/edc00c908bac_add_profile_emoji_fields.py b/backend/migrations/versions/edc00c908bac_add_profile_emoji_fields.py new file mode 100644 index 000000000..1417c386e --- /dev/null +++ b/backend/migrations/versions/edc00c908bac_add_profile_emoji_fields.py @@ -0,0 +1,26 @@ +"""add_profile_emoji_fields + +Revision ID: edc00c908bac +Revises: a9f09b49d862 +Create Date: 2025-10-04 13:30:55.791927 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'edc00c908bac' +down_revision = 'a9f09b49d862' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.add_column("user", sa.Column("profile_emoji", sa.String(10), nullable=True)) + op.add_column("user", sa.Column("emoji_expiration", sa.DateTime(), nullable=True)) + + +def downgrade() -> None: + op.drop_column("user", "emoji_expiration") + op.drop_column("user", "profile_emoji") diff --git a/backend/models/public_user.py b/backend/models/public_user.py index 2cc8bf0eb..d88d764cb 100644 --- a/backend/models/public_user.py +++ b/backend/models/public_user.py @@ -1,4 +1,5 @@ from pydantic import BaseModel +from datetime import datetime from .registration_type import RegistrationType @@ -31,3 +32,5 @@ def __hash__(self) -> int: bio: str | None = None linkedin: str | None = None website: str | None = None + profile_emoji: str | None = None + emoji_expiration: datetime | None = None diff --git a/backend/models/user.py b/backend/models/user.py index cee9c2f4d..098369d45 100644 --- a/backend/models/user.py +++ b/backend/models/user.py @@ -1,6 +1,7 @@ """User model serves as the data object for representing registered users across application layers.""" from pydantic import BaseModel +from datetime import datetime __authors__ = ["Kris Jordan"] __copyright__ = "Copyright 2023" @@ -39,6 +40,8 @@ class User(UserIdentity, BaseModel): bio: str | None = None linkedin: str | None = None website: str | None = None + profile_emoji: str | None = None + emoji_expiration: datetime | None = None class NewUser(User, BaseModel): @@ -69,3 +72,5 @@ class ProfileForm(BaseModel): bio: str | None = None linkedin: str | None = None website: str | None = None + profile_emoji: str | None = None + emoji_expiration: datetime | None = None diff --git a/frontend/src/app/academics/academics-admin/section/section-editor/section-editor.component.ts b/frontend/src/app/academics/academics-admin/section/section-editor/section-editor.component.ts index 213786613..59c001a5a 100644 --- a/frontend/src/app/academics/academics-admin/section/section-editor/section-editor.component.ts +++ b/frontend/src/app/academics/academics-admin/section/section-editor/section-editor.component.ts @@ -62,10 +62,10 @@ const canActivateEditor: CanActivateFn = ( } }; @Component({ - selector: 'app-section-editor', - templateUrl: './section-editor.component.html', - styleUrls: ['./section-editor.component.css'], - standalone: false + selector: 'app-section-editor', + templateUrl: './section-editor.component.html', + styleUrls: ['./section-editor.component.css'], + standalone: false }) export class SectionEditorComponent { /** Route information to be used in the Routing Module */ @@ -213,7 +213,9 @@ export class SectionEditorComponent { github: '', bio: '', linkedin: '', - website: '' + website: '', + emoji_expiration: null, + profile_emoji: null }; }) ?? []; } diff --git a/frontend/src/app/event/event-details/event-details.component.html b/frontend/src/app/event/event-details/event-details.component.html index 7135f3d75..628b64603 100644 --- a/frontend/src/app/event/event-details/event-details.component.html +++ b/frontend/src/app/event/event-details/event-details.component.html @@ -65,7 +65,7 @@ [src]="organizer.github_avatar" /> } - {{ organizer.first_name }} {{ organizer.last_name }} + {{ getDisplayName(organizer) }} } diff --git a/frontend/src/app/event/event-details/event-details.component.ts b/frontend/src/app/event/event-details/event-details.component.ts index 796d6772e..9ffc72137 100644 --- a/frontend/src/app/event/event-details/event-details.component.ts +++ b/frontend/src/app/event/event-details/event-details.component.ts @@ -9,7 +9,7 @@ import { Component, OnInit, WritableSignal, signal } from '@angular/core'; import { eventResolver } from '../event.resolver'; -import { Profile, ProfileService } from 'src/app/profile/profile.service'; +import { Profile, ProfileService, PublicProfile } from 'src/app/profile/profile.service'; import { ActivatedRoute, ActivatedRouteSnapshot, @@ -178,4 +178,20 @@ export class EventDetailsComponent implements OnInit { .getRegisteredUsersForEvent(this.event(), paginationParams) .subscribe((page) => this.eventRegistrationsPage.set(page)); } + + /** Check if emoji should be displayed for a user (not expired) */ + shouldDisplayEmoji(user: PublicProfile): boolean { + if (!user.profile_emoji) return false; + if (!user.emoji_expiration) return true; + return new Date(user.emoji_expiration) > new Date(); + } + + /** Get display name with emoji if applicable */ + getDisplayName(user: PublicProfile): string { + const name = `${user.first_name} ${user.last_name}`; + if (this.shouldDisplayEmoji(user)) { + return `${name} ${user.profile_emoji}`; + } + return name; + } } diff --git a/frontend/src/app/models.module.ts b/frontend/src/app/models.module.ts index 2452d89a7..9cac3e1f3 100644 --- a/frontend/src/app/models.module.ts +++ b/frontend/src/app/models.module.ts @@ -24,6 +24,8 @@ export interface Profile { bio: string | null; linkedin: string | null; website: string | null; + profile_emoji: string | null; + emoji_expiration: Date | null; } /** Interface for UserSummary Type (used on frontend for user requests) */ diff --git a/frontend/src/app/navigation/navigation.component.html b/frontend/src/app/navigation/navigation.component.html index de8a06589..cd32b93fa 100644 --- a/frontend/src/app/navigation/navigation.component.html +++ b/frontend/src/app/navigation/navigation.component.html @@ -154,7 +154,7 @@

Other

} @else { account_circle } - {{ profile.first_name !== '' ? profile.first_name + ' ' + profile.last_name : 'Profile' }} + {{ getDisplayName(profile) }} diff --git a/frontend/src/app/profile/profile-editor/profile-editor.component.ts b/frontend/src/app/profile/profile-editor/profile-editor.component.ts index 2be3b2cb8..b19a7d0a4 100644 --- a/frontend/src/app/profile/profile-editor/profile-editor.component.ts +++ b/frontend/src/app/profile/profile-editor/profile-editor.component.ts @@ -34,7 +34,9 @@ export class ProfileEditorComponent implements OnInit { website: '', linkedin: '', pronouns: '', - bio: '' + bio: '', + profile_emoji: '', + emoji_expiration: '' }); constructor( @@ -73,13 +75,25 @@ export class ProfileEditorComponent implements OnInit { pronouns: profile.pronouns, bio: profile.bio, linkedin: profile.linkedin, - website: profile.website + website: profile.website, + profile_emoji: profile.profile_emoji, + emoji_expiration: profile.emoji_expiration + ? new Date(profile.emoji_expiration).toISOString().slice(0, 16) + : '' }); } onSubmit(): void { if (this.profileForm.valid) { Object.assign(this.profile, this.profileForm.value); + // Convert emoji_expiration string to Date if provided + if (this.profileForm.value.emoji_expiration) { + this.profile.emoji_expiration = new Date( + this.profileForm.value.emoji_expiration + ); + } else { + this.profile.emoji_expiration = null; + } if (!this.profile.accepted_community_agreement) { const dialogRef = this.dialog.open(CommunityAgreement, { disableClose: true, diff --git a/frontend/src/app/profile/profile-page/profile-page.component.html b/frontend/src/app/profile/profile-page/profile-page.component.html index 398cfb877..e515bbb1b 100644 --- a/frontend/src/app/profile/profile-page/profile-page.component.html +++ b/frontend/src/app/profile/profile-page/profile-page.component.html @@ -12,6 +12,9 @@
{{ profile.first_name }} {{ profile.last_name }} + @if (shouldDisplayEmoji()) { + {{ profile.profile_emoji }} + } diff --git a/frontend/src/app/profile/profile-page/profile-page.component.ts b/frontend/src/app/profile/profile-page/profile-page.component.ts index e74902ea9..5ea8c6b11 100644 --- a/frontend/src/app/profile/profile-page/profile-page.component.ts +++ b/frontend/src/app/profile/profile-page/profile-page.component.ts @@ -98,4 +98,11 @@ export class ProfilePageComponent { this.profileService.profile$.subscribe(); dialogRef.afterClosed().subscribe(); } + + /** Check if emoji should be displayed (not expired) */ + shouldDisplayEmoji(): boolean { + if (!this.profile.profile_emoji) return false; + if (!this.profile.emoji_expiration) return true; + return new Date(this.profile.emoji_expiration) > new Date(); + } } diff --git a/frontend/src/app/profile/profile.service.ts b/frontend/src/app/profile/profile.service.ts index ad2fdc46c..65a2d132a 100644 --- a/frontend/src/app/profile/profile.service.ts +++ b/frontend/src/app/profile/profile.service.ts @@ -27,6 +27,8 @@ export interface Profile { bio: string | null; linkedin: string | null; website: string | null; + profile_emoji: string | null; + emoji_expiration: Date | null; } export interface PublicProfile { @@ -41,6 +43,8 @@ export interface PublicProfile { bio: string | null; linkedin: string | null; website: string | null; + profile_emoji: string | null; + emoji_expiration: Date | null; } @Injectable({ diff --git a/frontend/src/app/profile/public-profile-page/public-profile-page.component.html b/frontend/src/app/profile/public-profile-page/public-profile-page.component.html index 32e004244..8421aa0d7 100644 --- a/frontend/src/app/profile/public-profile-page/public-profile-page.component.html +++ b/frontend/src/app/profile/public-profile-page/public-profile-page.component.html @@ -11,6 +11,9 @@
{{ profile.first_name }} {{ profile.last_name }} + @if (shouldDisplayEmoji()) { + {{ profile.profile_emoji }} + } diff --git a/frontend/src/app/profile/public-profile-page/public-profile-page.component.ts b/frontend/src/app/profile/public-profile-page/public-profile-page.component.ts index f6c0cb17b..4bb5bec21 100644 --- a/frontend/src/app/profile/public-profile-page/public-profile-page.component.ts +++ b/frontend/src/app/profile/public-profile-page/public-profile-page.component.ts @@ -34,4 +34,11 @@ export class PublicProfilePageComponent { }; this.profile = data.profile; } + + /** Check if emoji should be displayed (not expired) */ + shouldDisplayEmoji(): boolean { + if (!this.profile.profile_emoji) return false; + if (!this.profile.emoji_expiration) return true; + return new Date(this.profile.emoji_expiration) > new Date(); + } } diff --git a/frontend/src/app/shared/user-chip-list/user-chip-list.widget.html b/frontend/src/app/shared/user-chip-list/user-chip-list.widget.html index f6328c4a1..1674222f0 100644 --- a/frontend/src/app/shared/user-chip-list/user-chip-list.widget.html +++ b/frontend/src/app/shared/user-chip-list/user-chip-list.widget.html @@ -9,7 +9,7 @@ *ngIf="user.github_avatar" matChipAvatar [src]="user.github_avatar" /> - {{ user.first_name + ' ' + user.last_name }} + {{ getDisplayName(user) }} } @else { - {{ user.first_name + ' ' + user.last_name + nameSuffix }} + {{ getDisplayName(user) }} - {{ user.first_name + ' ' + user.last_name + nameSuffix }} + {{ getDisplayName(user) }} } } @else { @@ -43,10 +43,10 @@ *ngIf="enableMailTo; else disableMailTo" class="user-email-link" href="mailto:{{ user.email }}"> - {{ user.first_name + ' ' + user.last_name + nameSuffix }} + {{ getDisplayName(user) }} - {{ user.first_name + ' ' + user.last_name + nameSuffix }} + {{ getDisplayName(user) }}
diff --git a/frontend/src/app/shared/user-chip-list/user-chip-list.widget.ts b/frontend/src/app/shared/user-chip-list/user-chip-list.widget.ts index ba5d6ed62..1dda6c860 100644 --- a/frontend/src/app/shared/user-chip-list/user-chip-list.widget.ts +++ b/frontend/src/app/shared/user-chip-list/user-chip-list.widget.ts @@ -26,4 +26,20 @@ export class UserChipList { emailRedirect(user: PublicProfile) { window.location.href = `mailto:${user.email}`; } + + /** Check if emoji should be displayed for a user (not expired) */ + shouldDisplayEmoji(user: PublicProfile): boolean { + if (!user.profile_emoji) return false; + if (!user.emoji_expiration) return true; + return new Date(user.emoji_expiration) > new Date(); + } + + /** Get display name with emoji if applicable */ + getDisplayName(user: PublicProfile): string { + const name = `${user.first_name} ${user.last_name}${this.nameSuffix}`; + if (this.shouldDisplayEmoji(user)) { + return `${name} ${user.profile_emoji}`; + } + return name; + } } diff --git a/frontend/src/app/shared/user-lookup/user-lookup.widget.html b/frontend/src/app/shared/user-lookup/user-lookup.widget.html index 0aea84068..876bb94b8 100644 --- a/frontend/src/app/shared/user-lookup/user-lookup.widget.html +++ b/frontend/src/app/shared/user-lookup/user-lookup.widget.html @@ -10,10 +10,10 @@ *ngIf="user.github_avatar" matChipAvatar [src]="user.github_avatar" /> - {{ user.first_name + ' ' + user.last_name }} + {{ getDisplayName(user) }} @@ -35,7 +35,7 @@ - {{ option.first_name }} {{ option.last_name }} + {{ getDisplayName(option) }} diff --git a/frontend/src/app/shared/user-lookup/user-lookup.widget.ts b/frontend/src/app/shared/user-lookup/user-lookup.widget.ts index 59d6aa4b2..93c8791db 100644 --- a/frontend/src/app/shared/user-lookup/user-lookup.widget.ts +++ b/frontend/src/app/shared/user-lookup/user-lookup.widget.ts @@ -30,10 +30,10 @@ import { Profile } from 'src/app/models.module'; import { ProfileService, PublicProfile } from 'src/app/profile/profile.service'; @Component({ - selector: 'user-lookup', - templateUrl: './user-lookup.widget.html', - styleUrls: ['./user-lookup.widget.css'], - standalone: false + selector: 'user-lookup', + templateUrl: './user-lookup.widget.html', + styleUrls: ['./user-lookup.widget.css'], + standalone: false }) export class UserLookup implements OnInit { @Input() label: string = 'Users'; @@ -85,7 +85,9 @@ export class UserLookup implements OnInit { github: user.github, bio: user.bio, linkedin: user.linkedin, - website: user.website + website: user.website, + profile_emoji: user.profile_emoji, + emoji_expiration: user.emoji_expiration }; this.users.push(organizer); } @@ -100,4 +102,20 @@ export class UserLookup implements OnInit { this.userLookup.setValue(''); this.usersChanged.emit(this.users); } + + /** Check if emoji should be displayed for a user (not expired) */ + shouldDisplayEmoji(user: Profile | PublicProfile): boolean { + if (!user.profile_emoji) return false; + if (!user.emoji_expiration) return true; + return new Date(user.emoji_expiration) > new Date(); + } + + /** Get display name with emoji if applicable */ + getDisplayName(user: Profile | PublicProfile): string { + const name = `${user.first_name} ${user.last_name}`; + if (this.shouldDisplayEmoji(user)) { + return `${name} ${user.profile_emoji}`; + } + return name; + } }