From debb6a1de173e3df5eb05842c14b2f33b638237b Mon Sep 17 00:00:00 2001
From: Jade Keegan <97476936+jadekeegan@users.noreply.github.com>
Date: Fri, 5 Jan 2024 14:05:09 -0500
Subject: [PATCH] Convert Reusable Code to Widgets & Refactor Event/Organizer
Models (#237)
* Convert event editor user lookup to a reusable widget
This converts the event editor user lookup form field to a widget so it
can be used throughout the application.
Additional Changes to Be Made: Create a new model to represent public
users/info so the form field can be used for ANY user.
* Create new `PublicUser` model
This commit creates a PublicUser model to represent public information
about a user. This is surfaced on the frontend as "PublicProfile" to
match referencing the User as "Profile".
This PublicUser model replaces the EventOrganizer and EventMember models.
* Remove attendees from Event model
This commit removes the list of attendees from the Event model to avoid
exposing the attendee information when retrieving events. This list is
never actually used, so it does not impact the code.
* Create user chip list widget
This commit turns the user chip list on the event details card into a
widget so it can be reused to display user information. It also adds
an "enableMailTo" field so users may be listed without allowing other
users to email them.
---
backend/api/events/events.py | 8 +-
backend/entities/event_entity.py | 4 +-
backend/entities/event_registration_entity.py | 39 ++++-----
backend/models/__init__.py | 2 +-
backend/models/event.py | 5 +-
backend/models/event_member.py | 36 ---------
backend/models/public_user.py | 24 ++++++
backend/services/event.py | 31 +++----
backend/test/services/event/event_test.py | 3 -
.../test/services/event/event_test_data.py | 28 +++----
.../event-editor/event-editor.component.html | 46 +----------
.../event-editor/event-editor.component.ts | 73 +++--------------
frontend/src/app/event/event.model.ts | 18 +----
.../event-detail-card.widget.css | 17 ----
.../event-detail-card.widget.html | 16 +---
.../event-detail-card.widget.ts | 4 +-
frontend/src/app/profile/profile.service.ts | 9 +++
frontend/src/app/shared/shared.module.ts | 24 +++++-
.../user-chip-list/user-chip-list.widget.css | 17 ++++
.../user-chip-list/user-chip-list.widget.html | 14 ++++
.../user-chip-list/user-chip-list.widget.ts | 23 ++++++
.../shared/user-lookup/user-lookup.widget.css | 3 +
.../user-lookup/user-lookup.widget.html | 38 +++++++++
.../shared/user-lookup/user-lookup.widget.ts | 81 +++++++++++++++++++
24 files changed, 301 insertions(+), 262 deletions(-)
delete mode 100644 backend/models/event_member.py
create mode 100644 backend/models/public_user.py
create mode 100644 frontend/src/app/shared/user-chip-list/user-chip-list.widget.css
create mode 100644 frontend/src/app/shared/user-chip-list/user-chip-list.widget.html
create mode 100644 frontend/src/app/shared/user-chip-list/user-chip-list.widget.ts
create mode 100644 frontend/src/app/shared/user-lookup/user-lookup.widget.css
create mode 100644 frontend/src/app/shared/user-lookup/user-lookup.widget.html
create mode 100644 frontend/src/app/shared/user-lookup/user-lookup.widget.ts
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('');
+ }
+}