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 @@ +
+ + + + {{ user.first_name + ' ' + user.last_name }} + + + {{ user.first_name + ' ' + user.last_name }} + + +
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(''); + } +}