Skip to content
Open
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
4 changes: 4 additions & 0 deletions backend/api/profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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)
Expand Down
15 changes: 14 additions & 1 deletion backend/entities/user_entity.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand All @@ -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(
Expand All @@ -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,
)
Original file line number Diff line number Diff line change
@@ -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")
3 changes: 3 additions & 0 deletions backend/models/public_user.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from pydantic import BaseModel
from datetime import datetime
from .registration_type import RegistrationType


Expand Down Expand Up @@ -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
5 changes: 5 additions & 0 deletions backend/models/user.py
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -213,7 +213,9 @@ export class SectionEditorComponent {
github: '',
bio: '',
linkedin: '',
website: ''
website: '',
emoji_expiration: null,
profile_emoji: null
};
}) ?? [];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@
[src]="organizer.github_avatar" />
}

{{ organizer.first_name }} {{ organizer.last_name }}
{{ getDisplayName(organizer) }}
</mat-chip>
}
</mat-chip-set>
Expand Down
18 changes: 17 additions & 1 deletion frontend/src/app/event/event-details/event-details.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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;
}
}
2 changes: 2 additions & 0 deletions frontend/src/app/models.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) */
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/app/navigation/navigation.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ <h3 matSubheader>Other</h3>
} @else {
<mat-icon class="account-circle-icon" matChipAvatar>account_circle</mat-icon>
}
{{ profile.first_name !== '' ? profile.first_name + ' ' + profile.last_name : 'Profile' }}
{{ getDisplayName(profile) }}
</mat-chip>
</mat-chip-set>
<button
Expand Down
17 changes: 17 additions & 0 deletions frontend/src/app/navigation/navigation.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,4 +104,21 @@ export class NavigationComponent implements OnInit, OnDestroy {
openLink(link: string) {
window.open(link ?? '', '_blank');
}

/** Check if emoji should be displayed for a profile (not expired) */
shouldDisplayEmoji(profile: Profile): boolean {
if (!profile.profile_emoji) return false;
if (!profile.emoji_expiration) return true;
return new Date(profile.emoji_expiration) > new Date();
}

/** Get display name with emoji if applicable */
getDisplayName(profile: Profile): string {
if (profile.first_name === '') return 'Profile';
const name = `${profile.first_name} ${profile.last_name}`;
if (this.shouldDisplayEmoji(profile)) {
return `${name} ${profile.profile_emoji}`;
}
return name;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
matChipAvatar
[src]="author.github_avatar" />
}
{{ author.first_name }} {{ author.last_name }}
{{ getAuthorDisplayName(author) }}
</mat-chip-row>
}
</mat-chip-set>
Expand Down
17 changes: 17 additions & 0 deletions frontend/src/app/news/article-page/article-page.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { NewsService } from '../news.service';
import { ArticleOverview } from 'src/app/welcome/welcome.model';
import { newsResolver } from '../news.resolver';
import { NagivationAdminGearService } from 'src/app/navigation/navigation-admin-gear.service';
import { PublicProfile } from 'src/app/profile/profile.service';

@Component({
selector: 'app-article-page',
Expand Down Expand Up @@ -53,4 +54,20 @@ export class ArticlePageComponent implements OnInit {
`/article/${this.article().slug}/edit`
);
}

/** Check if emoji should be displayed for an author (not expired) */
shouldDisplayEmoji(author: PublicProfile): boolean {
if (!author.profile_emoji) return false;
if (!author.emoji_expiration) return true;
return new Date(author.emoji_expiration) > new Date();
}

/** Get display name with emoji if applicable */
getAuthorDisplayName(author: PublicProfile): string {
const name = `${author.first_name} ${author.last_name}`;
if (this.shouldDisplayEmoji(author)) {
return `${name} ${author.profile_emoji}`;
}
return name;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,29 @@
formControlName="bio"
name="bio"></textarea>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Profile Emoji</mat-label>
<input
matInput
placeholder="Enter an emoji (e.g., 🎉)"
formControlName="profile_emoji"
name="profile_emoji"
maxlength="10" />
<mat-hint
>Add an emoji to display next to your name (optional)</mat-hint
>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Emoji Expiration</mat-label>
<input
matInput
type="datetime-local"
formControlName="emoji_expiration"
name="emoji_expiration" />
<mat-hint
>Set when the emoji should stop displaying (optional)</mat-hint
>
</mat-form-field>
</mat-card-content>
<mat-card-actions>
<button mat-stroked-button routerLink="/profile">Cancel</button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ export class ProfileEditorComponent implements OnInit {
website: '',
linkedin: '',
pronouns: '',
bio: ''
bio: '',
profile_emoji: '',
emoji_expiration: ''
});

constructor(
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
<div class="header-row">
<mat-card-title>
{{ profile.first_name }} {{ profile.last_name }}
@if (shouldDisplayEmoji()) {
<span>{{ profile.profile_emoji }}</span>
}
</mat-card-title>
<mat-chip-set class="no-hover-chipset">
<mat-chip disableRipple>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
4 changes: 4 additions & 0 deletions frontend/src/app/profile/profile.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
<div class="header-row">
<mat-card-title>
{{ profile.first_name }} {{ profile.last_name }}
@if (shouldDisplayEmoji()) {
<span>{{ profile.profile_emoji }}</span>
}
</mat-card-title>
<mat-chip-set class="no-hover-chipset">
<mat-chip disableRipple>
Expand Down
Loading