diff --git a/backend/api/events/events.py b/backend/api/events/events.py
index a525c1669..cc7aae234 100644
--- a/backend/api/events/events.py
+++ b/backend/api/events/events.py
@@ -5,7 +5,7 @@
from fastapi import APIRouter, Depends, HTTPException
from datetime import datetime, timedelta
from typing import Sequence
-from backend.models.event_member import EventMember
+from backend.models.public_user import PublicUser
from backend.models.pagination import Paginated, PaginationParams
from backend.services.organization import OrganizationService
@@ -284,7 +284,7 @@ def register_for_event(
subject: User = Depends(registered_user),
event_service: EventService = Depends(),
user_service: UserService = Depends(),
-) -> EventMember:
+) -> PublicUser:
"""
Register a user event based on the event ID.
@@ -315,7 +315,7 @@ def get_event_registration_of_user(
event_id: int,
subject: User = Depends(registered_user),
event_service: EventService = Depends(),
-) -> EventMember:
+) -> PublicUser:
"""
Check the registration status of a user for an event, raise ResourceNotFound if unregistered.
@@ -337,7 +337,7 @@ def get_event_registrations(
event_id: int,
subject: User = Depends(registered_user),
event_service: EventService = Depends(),
-) -> Sequence[EventMember]:
+) -> Sequence[PublicUser]:
"""
Get the registrations of an event.
diff --git a/backend/entities/event_entity.py b/backend/entities/event_entity.py
index 945a2edca..74914e720 100644
--- a/backend/entities/event_entity.py
+++ b/backend/entities/event_entity.py
@@ -111,7 +111,7 @@ def to_model(self, subject: User | None = None) -> Event:
# Hide organizer info for unauthenticated users
organizers = [
- registration.to_flat_organizer_model()
+ registration.to_flat_model()
for registration in self.registrations
if registration.registration_type == RegistrationType.ORGANIZER
]
@@ -134,7 +134,6 @@ def to_model(self, subject: User | None = None) -> Event:
organization_id=self.organization_id,
registration_count=len(attendees),
is_attendee=is_attendee,
- attendees=attendees,
is_organizer=is_organizer,
organizers=organizers,
)
@@ -160,7 +159,6 @@ def to_details_model(self, subject: User | None = None) -> EventDetails:
organization_id=self.organization_id,
organization=self.organization.to_model(),
is_attendee=event.is_attendee,
- attendees=event.attendees,
is_organizer=event.is_organizer,
organizers=event.organizers,
)
diff --git a/backend/entities/event_registration_entity.py b/backend/entities/event_registration_entity.py
index 50e402c9b..47af8978b 100644
--- a/backend/entities/event_registration_entity.py
+++ b/backend/entities/event_registration_entity.py
@@ -3,9 +3,11 @@
from sqlalchemy import ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship
-from backend.models.event_member import EventOrganizer
+from backend.entities.event_entity import EventEntity
+from backend.entities.user_entity import UserEntity
-from ..models import RegistrationType, EventMember
+from ..models import RegistrationType
+from ..models.public_user import PublicUser
from .entity_base import EntityBase
from typing import Self
from ..models.event_registration import EventRegistration, NewEventRegistration
@@ -58,8 +60,8 @@ def from_model(cls, model: EventRegistration) -> Self:
return cls(
event_id=model.event_id,
user_id=model.user_id,
- event=model.event,
- user=model.user,
+ event=EventEntity.from_model(model.event),
+ user=UserEntity.from_model(model.user),
registration_type=model.registration_type,
)
@@ -79,7 +81,7 @@ def from_new_model(cls, model: NewEventRegistration) -> Self:
registration_type=model.registration_type,
)
- def to__model(self) -> EventRegistration:
+ def to_model(self) -> EventRegistration:
"""
Converts an `EventRegistrationEntity` into an `EventRegistration` model object
to store registration information.
@@ -89,36 +91,25 @@ def to__model(self) -> EventRegistration:
"""
return EventRegistration(
event_id=self.event_id,
- event=self.event,
+ event=self.event.to_model(),
user_id=self.user_id,
- user=self.user,
+ user=self.user.to_model(),
registration_type=self.registration_type,
)
- def to_flat_model(self) -> EventMember:
+ def to_flat_model(self) -> PublicUser:
"""
- Converts an `EventRegistrationEntity` into an `EventMember` model object
- to store user ID.
+ Converts an `EventRegistrationEntity` into an `PublicUser` model object
+ to store public user information.
Returns:
- EventMember: `EventMember` object from the entity
+ PublicUser: `PublicUser` object from the entity
"""
- return EventMember(id=self.user_id, registration_type=self.registration_type)
-
- def to_flat_organizer_model(self) -> EventMember:
- """
- Converts an `EventRegistrationEntity` into an `EventMember` model object
- to store user ID.
-
- Returns:
- EventMember: `EventMember` object from the entity
- """
- return EventOrganizer(
+ return PublicUser(
id=self.user_id,
- registration_type=self.registration_type,
first_name=self.user.first_name,
last_name=self.user.last_name,
pronouns=self.user.pronouns,
email=self.user.email,
- github_avatar=self.user.github_avatar
+ github_avatar=self.user.github_avatar,
)
diff --git a/backend/models/__init__.py b/backend/models/__init__.py
index def1f76fe..856f34839 100644
--- a/backend/models/__init__.py
+++ b/backend/models/__init__.py
@@ -9,7 +9,7 @@
from .role_details import RoleDetails
from .organization import Organization
from .event import Event
-from .event_member import EventMember
+from .public_user import PublicUser
from .event_details import EventDetails
from .room import Room
from .room_details import RoomDetails
diff --git a/backend/models/event.py b/backend/models/event.py
index a4a153371..662f8f44c 100644
--- a/backend/models/event.py
+++ b/backend/models/event.py
@@ -1,7 +1,7 @@
from pydantic import BaseModel
from datetime import datetime
-from .event_member import EventMember, EventOrganizer
+from .public_user import PublicUser
__authors__ = ["Ajay Gandecha", "Jade Keegan", "Brianna Ta", "Audrey Toney"]
__copyright__ = "Copyright 2023"
@@ -23,7 +23,7 @@ class DraftEvent(BaseModel):
public: bool
registration_limit: int
organization_id: int
- organizers: list[EventOrganizer] = []
+ organizers: list[PublicUser] = []
class Event(DraftEvent):
@@ -37,5 +37,4 @@ class Event(DraftEvent):
id: int
registration_count: int = 0
is_attendee: bool = False
- attendees: list[EventMember] = []
is_organizer: bool = False
diff --git a/backend/models/event_member.py b/backend/models/event_member.py
deleted file mode 100644
index 102180623..000000000
--- a/backend/models/event_member.py
+++ /dev/null
@@ -1,36 +0,0 @@
-from pydantic import BaseModel
-from .registration_type import RegistrationType
-
-
-__authors__ = ["Ajay Gandecha"]
-__copyright__ = "Copyright 2023"
-__license__ = "MIT"
-
-
-class EventMember(BaseModel):
- """
- Pydantic model to represent the information about a user who is
- registered for an event.
-
- This model is based on the `UserEntity` model, which defines the shape
- of the `User` database in the PostgreSQL database
- """
-
- id: int | None
- registration_type: RegistrationType
-
-
-class EventOrganizer(EventMember):
- """
- Pydantic model to represent the information about a user who is
- registered for an event.
-
- This model is based on the `UserEntity` model, which defines the shape
- of the `User` database in the PostgreSQL database
- """
-
- first_name: str
- last_name: str
- pronouns: str
- email: str
- github_avatar: str | None = None
diff --git a/backend/models/public_user.py b/backend/models/public_user.py
new file mode 100644
index 000000000..22ad25850
--- /dev/null
+++ b/backend/models/public_user.py
@@ -0,0 +1,24 @@
+from pydantic import BaseModel
+from .registration_type import RegistrationType
+
+
+__authors__ = ["Ajay Gandecha"]
+__copyright__ = "Copyright 2023"
+__license__ = "MIT"
+
+
+class PublicUser(BaseModel):
+ """
+ Pydantic model to represent public information about users to avoid
+ exposing sensitive information about them.
+
+ This model is based on the `UserEntity` model, which defines the shape
+ of the `User` database in the PostgreSQL database
+ """
+
+ id: int | None
+ first_name: str
+ last_name: str
+ pronouns: str
+ email: str
+ github_avatar: str | None = None
diff --git a/backend/services/event.py b/backend/services/event.py
index 9247bc959..e3f46590c 100644
--- a/backend/services/event.py
+++ b/backend/services/event.py
@@ -8,7 +8,8 @@
from sqlalchemy import func, select, or_
from sqlalchemy.orm import Session, aliased
from backend.entities.user_entity import UserEntity
-from backend.models.event_member import EventMember
+from backend.models.event_registration import EventRegistration
+from ..models.public_user import PublicUser
from backend.models.organization_details import OrganizationDetails
from backend.models.pagination import Paginated, PaginationParams
from backend.models.registration_type import RegistrationType
@@ -296,7 +297,7 @@ def delete(self, subject: User, id: int) -> None:
def get_registration(
self, subject: User, attendee: User, event: EventDetails
- ) -> EventMember | None:
+ ) -> EventRegistration | None:
"""
Get a registration of an attendee for an Event.
@@ -306,7 +307,7 @@ def get_registration(
event: EventDetails of the event seeking registration for
Returns:
- EventMember or None if no registration found
+ PublicUser or None if no registration found
Raises:
UserPermissionException if subject does not have permission
@@ -329,13 +330,13 @@ def get_registration(
# Return EventRegistration model or None
if event_registration_entity is not None:
- return event_registration_entity.to_flat_model()
+ return event_registration_entity.to_model()
else:
return None
def get_registrations_of_event(
self, subject: User, event: EventDetails
- ) -> list[EventMember]:
+ ) -> list[PublicUser]:
"""
List the registrations of an event.
@@ -348,7 +349,7 @@ def get_registrations_of_event(
event: The event whose registrations are being queried.
Returns:
- list[EventMember]
+ list[PublicUser]
Raises:
UserPermissionException if user is not an event organizer or admin.
@@ -370,7 +371,7 @@ def get_registrations_of_event(
def set_event_organizer(
self, subject: User, user_id: int, event: EventDetails
- ) -> EventMember:
+ ) -> PublicUser:
"""
Set the organizer of an event.
@@ -379,7 +380,7 @@ def set_event_organizer(
event: The EventDetails being registered for
Returns:
- EventMember
+ PublicUser
"""
@@ -400,11 +401,11 @@ def set_event_organizer(
self._session.commit()
# Return registration
- return event_registration_entity.to_flat_organizer_model()
+ return event_registration_entity.to_flat_model()
def register(
self, subject: User, attendee: User, event: EventDetails
- ) -> EventMember:
+ ) -> PublicUser:
"""
Register a user for an event.
@@ -414,7 +415,7 @@ def register(
event: The EventDetails being registered for
Returns:
- EventMember
+ PublicUser
Raises:
UserPermissionException if subject does not have permission to register user
@@ -440,7 +441,9 @@ def register(
# Permission to manage / read registration is enforced in EventService#get_registration
existing_registration = self.get_registration(subject, attendee, event)
if existing_registration:
- return existing_registration
+ return EventRegistrationEntity.from_model(
+ existing_registration
+ ).to_flat_model()
# Add new object to table and commit changes
event_registration_entity = EventRegistrationEntity(
@@ -492,7 +495,7 @@ def unregister(self, subject: User, attendee: User, event: EventDetails) -> None
def get_registrations_of_user(
self, subject: User, user: User, time_range: TimeRange
- ) -> Sequence[EventMember]:
+ ) -> Sequence[PublicUser]:
"""
Get a user's registrations to events falling within a given time range.
@@ -502,7 +505,7 @@ def get_registrations_of_user(
time_range: The period over which to search for event registrations.
Returns:
- Sequence[EventMember] event registrations
+ Sequence[PublicUser] event registrations
Raises:
UserPermissionException when the user is requesting the registrations
diff --git a/backend/test/services/event/event_test.py b/backend/test/services/event/event_test.py
index 379a89c5a..4846ae6e0 100644
--- a/backend/test/services/event/event_test.py
+++ b/backend/test/services/event/event_test.py
@@ -4,7 +4,6 @@
import pytest
from unittest.mock import create_autospec
from backend.models.pagination import PaginationParams
-from backend.models.registration_type import RegistrationType
from backend.services.exceptions import (
EventRegistrationException,
@@ -114,7 +113,6 @@ def test_create_event_as_root(event_svc_integration: EventService):
assert created_event.organizers[0].id == root.id
assert created_event.is_organizer == True
- assert len(created_event.attendees) == 0
assert created_event.is_attendee == False
@@ -270,7 +268,6 @@ def test_register_for_event_as_user(event_svc_integration: EventService):
event_details = event_svc_integration.get_by_id(event_one.id, root) # type: ignore
created_registration = event_svc_integration.register(root, root, event_details) # type: ignore
assert created_registration is not None
- assert created_registration.registration_type == RegistrationType.ATTENDEE
def test_register_for_event_as_user_twice(event_svc_integration: EventService):
diff --git a/backend/test/services/event/event_test_data.py b/backend/test/services/event/event_test_data.py
index 2be0080ee..043d46b43 100644
--- a/backend/test/services/event/event_test_data.py
+++ b/backend/test/services/event/event_test_data.py
@@ -3,9 +3,9 @@
import pytest
from sqlalchemy.orm import Session
-from backend.models.event_member import EventMember, EventOrganizer
+from ....models.public_user import PublicUser
from ....models.event import DraftEvent, Event
-from ....models.event_registration import EventRegistration, NewEventRegistration
+from ....models.event_registration import NewEventRegistration
from ....models.registration_type import RegistrationType
from ....entities.event_entity import EventEntity
from ....entities.event_registration_entity import EventRegistrationEntity
@@ -65,13 +65,12 @@
registration_limit=50,
organization_id=cads.id | 0,
organizers=[
- EventOrganizer(
+ PublicUser(
id=root.id,
first_name=root.first_name,
last_name=root.last_name,
pronouns=root.pronouns,
email=root.email,
- registration_type=RegistrationType.ORGANIZER,
)
],
)
@@ -97,13 +96,12 @@
registration_limit=50,
organization_id=cssg.id | 0,
organizers=[
- EventOrganizer(
+ PublicUser(
id=user.id,
first_name=user.first_name,
last_name=user.last_name,
pronouns=user.pronouns,
email=user.email,
- registration_type=RegistrationType.ORGANIZER,
),
],
)
@@ -118,21 +116,19 @@
registration_limit=50,
organization_id=cssg.id | 0,
organizers=[
- EventOrganizer(
+ PublicUser(
id=user.id,
first_name=user.first_name,
last_name=user.last_name,
pronouns=user.pronouns,
email=user.email,
- registration_type=RegistrationType.ORGANIZER,
),
- EventOrganizer(
+ PublicUser(
id=ambassador.id,
first_name=ambassador.first_name,
last_name=ambassador.last_name,
pronouns=ambassador.pronouns,
email=ambassador.email,
- registration_type=RegistrationType.ORGANIZER,
),
],
)
@@ -158,29 +154,26 @@
registration_limit=1,
organization_id=cssg.id | 0,
organizers=[
- EventOrganizer(
+ PublicUser(
id=user.id,
first_name=user.first_name,
last_name=user.last_name,
pronouns=user.pronouns,
email=user.email,
- registration_type=RegistrationType.ORGANIZER,
),
- EventOrganizer(
+ PublicUser(
id=ambassador.id,
first_name=ambassador.first_name,
last_name=ambassador.last_name,
pronouns=ambassador.pronouns,
email=ambassador.email,
- registration_type=RegistrationType.ORGANIZER,
),
- EventOrganizer(
+ PublicUser(
id=root.id,
first_name=root.first_name,
last_name=root.last_name,
pronouns=root.pronouns,
email=root.email,
- registration_type=RegistrationType.ORGANIZER,
),
],
)
@@ -195,13 +188,12 @@
registration_limit=1,
organization_id=cssg.id | 0,
organizers=[
- EventOrganizer(
+ PublicUser(
id=user.id,
first_name=user.first_name,
last_name=user.last_name,
pronouns=user.pronouns,
email=user.email,
- registration_type=RegistrationType.ORGANIZER,
),
],
)
diff --git a/frontend/src/app/event/event-editor/event-editor.component.html b/frontend/src/app/event/event-editor/event-editor.component.html
index 94fda3a9d..d3892dd54 100644
--- a/frontend/src/app/event/event-editor/event-editor.component.html
+++ b/frontend/src/app/event/event-editor/event-editor.component.html
@@ -69,48 +69,10 @@
-
- Organizers
-
-
-
- {{ organizer.first_name + ' ' + organizer.last_name }}
-
-
-
-
-
- {{ option.first_name }} {{ option.last_name }}
-
-
-
-
+
diff --git a/frontend/src/app/event/event-editor/event-editor.component.ts b/frontend/src/app/event/event-editor/event-editor.component.ts
index b2d759273..c9966de42 100644
--- a/frontend/src/app/event/event-editor/event-editor.component.ts
+++ b/frontend/src/app/event/event-editor/event-editor.component.ts
@@ -7,29 +7,21 @@
* @license MIT
*/
-import { Component, ElementRef, ViewChild } from '@angular/core';
+import { Component } from '@angular/core';
import { ActivatedRoute, Route, Router } from '@angular/router';
import { FormBuilder, FormControl, Validators } from '@angular/forms';
import { MatSnackBar } from '@angular/material/snack-bar';
import { EventService } from '../event.service';
import { profileResolver } from '../../profile/profile.resolver';
-import { Profile, ProfileService } from '../../profile/profile.service';
+import { Profile, PublicProfile } from '../../profile/profile.service';
import { OrganizationService } from '../../organization/organization.service';
-import {
- Observable,
- ReplaySubject,
- debounceTime,
- filter,
- mergeMap,
- startWith
-} from 'rxjs';
+import { Observable } from 'rxjs';
import { eventDetailResolver } from '../event.resolver';
import { PermissionService } from 'src/app/permission.service';
import { organizationDetailResolver } from 'src/app/organization/organization.resolver';
import { Organization } from 'src/app/organization/organization.model';
-import { Event, EventOrganizer, RegistrationType } from '../event.model';
+import { Event, RegistrationType } from '../event.model';
import { DatePipe } from '@angular/common';
-import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
@Component({
selector: 'app-event-editor',
@@ -59,7 +51,7 @@ export class EventEditorComponent {
public adminPermission$: Observable;
/** Store organizers */
- public organizers: EventOrganizer[] = [];
+ public organizers: PublicProfile[] = [];
/** Add validators to the form */
name = new FormControl('', [Validators.required]);
@@ -74,12 +66,6 @@ export class EventEditorComponent {
Validators.required,
Validators.min(0)
]);
- userLookup = new FormControl();
-
- @ViewChild('organizersInput') organizersInput!: ElementRef;
- private filteredUsers: ReplaySubject = new ReplaySubject();
- public filteredUsers$: Observable =
- this.filteredUsers.asObservable();
/** Create a form group */
public eventForm = this.formBuilder.group({
@@ -100,8 +86,7 @@ export class EventEditorComponent {
protected snackBar: MatSnackBar,
private eventService: EventService,
private permission: PermissionService,
- private datePipe: DatePipe,
- private profileService: ProfileService
+ private datePipe: DatePipe
) {
// Get currently-logged-in user
const data = route.snapshot.data as {
@@ -142,26 +127,11 @@ export class EventEditorComponent {
`organization/${this.organization!.id}`
);
- this.adminPermission$.subscribe((perm) => {
- if (!perm) {
- this.userLookup.disable();
- }
- });
-
- // Configure the filtered users list based on the form
- this.filteredUsers$ = this.userLookup.valueChanges.pipe(
- startWith(''),
- filter((search: string) => search.length > 2),
- debounceTime(100),
- mergeMap((search) => this.profileService.search(search))
- );
-
// Set the organizers
// If no organizers already, set current user as organizer
if (this.event.id == null) {
- let organizer = {
+ let organizer: PublicProfile = {
id: this.profile.id!,
- registration_type: RegistrationType.ORGANIZER,
first_name: this.profile.first_name!,
last_name: this.profile.last_name!,
pronouns: this.profile.pronouns!,
@@ -175,35 +145,10 @@ export class EventEditorComponent {
}
}
- /** Handler for selecting an option in the who chip grid. */
- public onOptionSelected = (event: MatAutocompleteSelectedEvent) => {
- let user = event.option.value as Profile;
- if (this.organizers.filter((e) => e.id === user.id).length == 0) {
- let organizer: EventOrganizer = {
- id: user.id!,
- registration_type: RegistrationType.ORGANIZER,
- first_name: user.first_name!,
- last_name: user.last_name!,
- pronouns: user.pronouns!,
- email: user.email!,
- github_avatar: user.github_avatar
- };
- this.organizers.push(organizer);
- }
- this.organizersInput.nativeElement.value = '';
- this.userLookup.setValue('');
- };
-
- /** Handler for selecting an option in the who chip grid. */
- public onOptionDeselected = (person: EventOrganizer) => {
- this.organizers.splice(this.organizers.indexOf(person), 1);
- this.userLookup.setValue('');
- };
-
/** Event handler to handle submitting the Create Event Form.
* @returns {void}
*/
- onSubmit = () => {
+ onSubmit() {
if (this.eventForm.valid) {
Object.assign(this.event, this.eventForm.value);
@@ -223,7 +168,7 @@ export class EventEditorComponent {
}
this.router.navigate(['/organizations/', this.organization_slug]);
}
- };
+ }
/** Opens a confirmation snackbar when an event is successfully created.
* @returns {void}
diff --git a/frontend/src/app/event/event.model.ts b/frontend/src/app/event/event.model.ts
index 05d4b53cd..9ada9851c 100644
--- a/frontend/src/app/event/event.model.ts
+++ b/frontend/src/app/event/event.model.ts
@@ -9,6 +9,7 @@
import { Profile } from '../models.module';
import { Organization } from '../organization/organization.model';
+import { PublicProfile } from '../profile/profile.service';
/** Interface for Event Type (used on frontend for event detail) */
export interface Event {
@@ -24,7 +25,7 @@ export interface Event {
registration_count: number;
is_attendee: boolean;
is_organizer: boolean;
- organizers: EventOrganizer[];
+ organizers: PublicProfile[];
}
/** Interface for the Event JSON Response model
@@ -45,7 +46,7 @@ export interface EventJson {
registration_count: number;
is_attendee: boolean;
is_organizer: boolean;
- organizers: EventOrganizer[];
+ organizers: PublicProfile[];
}
/** Function that converts an EventJSON response model to an Event model.
@@ -70,16 +71,3 @@ export interface EventRegistration {
user: Profile | null;
is_organizer: boolean | null;
}
-
-export interface EventMember {
- id: number;
- registration_type: RegistrationType;
-}
-
-export interface EventOrganizer extends EventMember {
- first_name: string;
- last_name: string;
- pronouns: string;
- email: string;
- github_avatar: string | null;
-}
diff --git a/frontend/src/app/event/widgets/event-detail-card/event-detail-card.widget.css b/frontend/src/app/event/widgets/event-detail-card/event-detail-card.widget.css
index 5914598e0..ad3a0349d 100644
--- a/frontend/src/app/event/widgets/event-detail-card/event-detail-card.widget.css
+++ b/frontend/src/app/event/widgets/event-detail-card/event-detail-card.widget.css
@@ -93,20 +93,3 @@
.organizers-text {
margin: 0;
}
-
-.organizer-chips {
- margin-bottom: 16px;
-}
-
-.organizer-chip {
- margin: 8px 8px 0px 0px;
-}
-
-.organizer-email-link {
- text-decoration: none;
- color: white;
- display: flex;
- justify-content: flex-start;
- margin: 0;
- padding: 0;
-}
diff --git a/frontend/src/app/event/widgets/event-detail-card/event-detail-card.widget.html b/frontend/src/app/event/widgets/event-detail-card/event-detail-card.widget.html
index 294d7906c..7ccaa0152 100644
--- a/frontend/src/app/event/widgets/event-detail-card/event-detail-card.widget.html
+++ b/frontend/src/app/event/widgets/event-detail-card/event-detail-card.widget.html
@@ -84,19 +84,9 @@
-
+
diff --git a/frontend/src/app/event/widgets/event-detail-card/event-detail-card.widget.ts b/frontend/src/app/event/widgets/event-detail-card/event-detail-card.widget.ts
index 3a22fb020..e8ca2e576 100644
--- a/frontend/src/app/event/widgets/event-detail-card/event-detail-card.widget.ts
+++ b/frontend/src/app/event/widgets/event-detail-card/event-detail-card.widget.ts
@@ -8,10 +8,10 @@
*/
import { Component, Input, OnInit } from '@angular/core';
-import { Event, EventOrganizer, EventRegistration } from '../../event.model';
+import { Event, EventRegistration } from '../../event.model';
import { MatSnackBar } from '@angular/material/snack-bar';
import { EventService } from '../../event.service';
-import { Observable, of } from 'rxjs';
+import { Observable } from 'rxjs';
import { PermissionService } from 'src/app/permission.service';
import { Profile } from 'src/app/models.module';
import { Router } from '@angular/router';
diff --git a/frontend/src/app/profile/profile.service.ts b/frontend/src/app/profile/profile.service.ts
index d54ee8e38..ada477db0 100644
--- a/frontend/src/app/profile/profile.service.ts
+++ b/frontend/src/app/profile/profile.service.ts
@@ -25,6 +25,15 @@ export interface Profile {
permissions: Permission[];
}
+export interface PublicProfile {
+ id: number;
+ first_name: string;
+ last_name: string;
+ pronouns: string;
+ email: string;
+ github_avatar: string | null;
+}
+
@Injectable({
providedIn: 'root'
})
diff --git a/frontend/src/app/shared/shared.module.ts b/frontend/src/app/shared/shared.module.ts
index 00c318769..493512316 100644
--- a/frontend/src/app/shared/shared.module.ts
+++ b/frontend/src/app/shared/shared.module.ts
@@ -17,6 +17,7 @@ import { FormsModule } from '@angular/forms';
import { ReactiveFormsModule } from '@angular/forms';
import { MatIconModule } from '@angular/material/icon';
import { MatTooltipModule } from '@angular/material/tooltip';
+import { MatChipsModule } from '@angular/material/chips';
/* UI Widgets */
import { SocialMediaIcon } from '../shared/social-media-icon/social-media-icon.widget';
@@ -24,13 +25,23 @@ import { SearchBar } from './search-bar/search-bar.widget';
import { EventCard } from './event-card/event-card.widget';
import { RouterModule } from '@angular/router';
import { EventList } from './event-list/event-list.widget';
-import { EventFilterPipe } from '../event/event-filter/event-filter.pipe';
+import { UserLookup } from './user-lookup/user-lookup.widget';
+
+import { UserChipList } from './user-chip-list/user-chip-list.widget';
@NgModule({
- declarations: [SocialMediaIcon, SearchBar, EventCard, EventList],
+ declarations: [
+ SocialMediaIcon,
+ SearchBar,
+ EventCard,
+ EventList,
+ UserLookup,
+ UserChipList
+ ],
imports: [
CommonModule,
MatTabsModule,
+ MatChipsModule,
MatTableModule,
MatCardModule,
MatDialogModule,
@@ -47,6 +58,13 @@ import { EventFilterPipe } from '../event/event-filter/event-filter.pipe';
MatTooltipModule,
RouterModule
],
- exports: [SocialMediaIcon, SearchBar, EventCard, EventList]
+ exports: [
+ SocialMediaIcon,
+ SearchBar,
+ EventCard,
+ EventList,
+ UserLookup,
+ UserChipList
+ ]
})
export class SharedModule {}
diff --git a/frontend/src/app/shared/user-chip-list/user-chip-list.widget.css b/frontend/src/app/shared/user-chip-list/user-chip-list.widget.css
new file mode 100644
index 000000000..bdd5e08ef
--- /dev/null
+++ b/frontend/src/app/shared/user-chip-list/user-chip-list.widget.css
@@ -0,0 +1,17 @@
+.user-chips {
+ margin-bottom: 16px;
+}
+
+.user-chip {
+ margin: 8px 8px 0px 0px;
+ vertical-align: middle;
+}
+
+.user-email-link {
+ text-decoration: none;
+ display: flex;
+ color: inherit;
+ justify-content: flex-start;
+ margin: 0;
+ padding: 0;
+}
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
new file mode 100644
index 000000000..7ba93cf17
--- /dev/null
+++ b/frontend/src/app/shared/user-chip-list/user-chip-list.widget.html
@@ -0,0 +1,14 @@
+
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
new file mode 100644
index 000000000..cf3fa235b
--- /dev/null
+++ b/frontend/src/app/shared/user-chip-list/user-chip-list.widget.ts
@@ -0,0 +1,23 @@
+/**
+ * The User Chip List Widget displays user names as MatChips with
+ * an optional "click to contact" feature.
+ *
+ * @author Jade Keegan
+ * @copyright 2024
+ * @license MIT
+ */
+
+import { Component, Input } from '@angular/core';
+import { PublicProfile } from 'src/app/profile/profile.service';
+
+@Component({
+ selector: 'user-chip-list',
+ templateUrl: './user-chip-list.widget.html',
+ styleUrls: ['./user-chip-list.widget.css']
+})
+export class UserChipList {
+ @Input() users!: PublicProfile[];
+ @Input() enableMailTo!: boolean;
+
+ constructor() {}
+}
diff --git a/frontend/src/app/shared/user-lookup/user-lookup.widget.css b/frontend/src/app/shared/user-lookup/user-lookup.widget.css
new file mode 100644
index 000000000..8aff6f5a5
--- /dev/null
+++ b/frontend/src/app/shared/user-lookup/user-lookup.widget.css
@@ -0,0 +1,3 @@
+.form-field {
+ width: 100%;
+}
diff --git a/frontend/src/app/shared/user-lookup/user-lookup.widget.html b/frontend/src/app/shared/user-lookup/user-lookup.widget.html
new file mode 100644
index 000000000..e965205d8
--- /dev/null
+++ b/frontend/src/app/shared/user-lookup/user-lookup.widget.html
@@ -0,0 +1,38 @@
+
+ Organizers
+
+
+
+ {{ user.first_name + ' ' + user.last_name }}
+
+
+
+
+
+ {{ option.first_name }} {{ option.last_name }}
+
+
+
+
diff --git a/frontend/src/app/shared/user-lookup/user-lookup.widget.ts b/frontend/src/app/shared/user-lookup/user-lookup.widget.ts
new file mode 100644
index 000000000..0c3c92104
--- /dev/null
+++ b/frontend/src/app/shared/user-lookup/user-lookup.widget.ts
@@ -0,0 +1,81 @@
+/**
+ * The User Lookup Widget allows users to search for users
+ * in the XL.
+ *
+ * @author Ajay Gandecha, Jade Keegan, Brianna Ta, Audrey Toney
+ * @copyright 2023
+ * @license MIT
+ */
+
+import { Component, ElementRef, Input, OnInit, ViewChild } from '@angular/core';
+import { FormControl } from '@angular/forms';
+import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
+import {
+ Observable,
+ ReplaySubject,
+ debounceTime,
+ filter,
+ mergeMap,
+ startWith
+} from 'rxjs';
+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']
+})
+export class UserLookup implements OnInit {
+ @Input() profile!: Profile | null;
+ @Input() users!: PublicProfile[];
+ @Input() adminPermission!: boolean | null;
+
+ userLookup = new FormControl();
+
+ @ViewChild('usersInput') usersInput!: ElementRef;
+ private filteredUsers: ReplaySubject = new ReplaySubject();
+ public filteredUsers$: Observable =
+ this.filteredUsers.asObservable();
+
+ constructor(private profileService: ProfileService) {
+ // Configure the filtered users list based on the form
+ this.filteredUsers$ = this.userLookup.valueChanges.pipe(
+ startWith(''),
+ filter((search: string) => search.length > 2),
+ debounceTime(100),
+ mergeMap((search) => this.profileService.search(search))
+ );
+ }
+
+ ngOnInit() {
+ if (!this.adminPermission) {
+ this.userLookup.disable();
+ }
+ }
+
+ /** Handler for selecting an option in the who chip grid. */
+ public onUserAdded(event: MatAutocompleteSelectedEvent) {
+ let user = event.option.value as Profile;
+ if (this.users.filter((e) => e.id === user.id).length == 0) {
+ let organizer: PublicProfile = {
+ id: user.id!,
+ first_name: user.first_name!,
+ last_name: user.last_name!,
+ pronouns: user.pronouns!,
+ email: user.email!,
+ github_avatar: user.github_avatar
+ };
+ this.users.push(organizer);
+ }
+
+ this.usersInput.nativeElement.value = '';
+ this.userLookup.setValue('');
+ }
+
+ /** Handler for selecting an option in the who chip grid. */
+ public onUserRemoved(person: PublicProfile) {
+ this.users.splice(this.users.indexOf(person), 1);
+ this.userLookup.setValue('');
+ }
+}