Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
8d07c04
Sync upstream (#17)
alfeng22 Nov 11, 2024
91cea9b
Create design_doc.md (#1)
anikaahmed114 Nov 18, 2024
e4b17a2
Implement user story 1 view organization roster (#24)
Alanna423 Nov 20, 2024
7ef9565
Implement user story 1 view organization roster (#24) (#25)
Alanna423 Nov 20, 2024
29a513b
Feature/join organization (#26)
axu-1 Nov 22, 2024
f3ee8ec
Merge tech specs into stage (#5) (#28)
axu-1 Nov 22, 2024
16e849f
Frontend edit api 14 (#31)
Alanna423 Dec 9, 2024
5177f2a
Feature/edit roster as admin 3 (#32)
axu-1 Dec 10, 2024
acba734
Sync upstream (#33)
axu-1 Dec 10, 2024
d148831
Fix dependencies in package-lock.json
Alanna423 Jan 21, 2025
6e7b0ce
Merge branch 'main' of https://github.com/unc-csxl/csxl.unc.edu into …
Alanna423 Jan 21, 2025
d5bd4f3
Merge branch 'main' of https://github.com/unc-csxl/csxl.unc.edu into …
Alanna423 Feb 17, 2025
650a358
Revise membership entity, adjust FastAPI and service methods
Alanna423 Feb 17, 2025
afe02c0
Revise OrganizationMembership tests to include term/title/admin
Alanna423 Feb 24, 2025
5462dbd
Make term autopopulate if not provided
Alanna423 Feb 24, 2025
080930c
Change add membership logic by org join type
Alanna423 Feb 25, 2025
f5fc65b
Update test data fixtures for term field
Alanna423 Feb 25, 2025
5f97e68
Update logic for roster action permission levels
Alanna423 Feb 26, 2025
3d47460
Bring organization test coverage to 100%
Alanna423 Feb 26, 2025
64186a3
Merge branch 'main' of https://github.com/unc-csxl/csxl.unc.edu into …
Alanna423 Feb 26, 2025
4aa6d56
Merge branch 'main' of https://github.com/unc-csxl/csxl.unc.edu into …
Alanna423 Feb 26, 2025
6ea8ec1
Merge branch 'organization-memberships' of https://github.com/unc-csx…
Alanna423 Feb 26, 2025
1bc8218
Change admin level to enum and refactor
Alanna423 Feb 28, 2025
dcc9b9b
Merge branch 'main' of https://github.com/unc-csxl/csxl.unc.edu into …
Alanna423 Feb 28, 2025
5ee6bd2
Merge branch 'organization-memberships' of https://github.com/unc-csx…
Alanna423 Feb 28, 2025
254dc7c
Merge pull request #712 from unc-csxl/organization-memberships-backend
Alanna423 Feb 28, 2025
a12dd17
Add events widget to organization details
axfng Apr 23, 2025
054f1ca
Merge branch 'main' into organization-memberships
axu-1 May 9, 2025
2e7d858
Update organization model and services to use new schema
axu-1 Jun 30, 2025
f80a22f
Add roster membership edit dialog
axu-1 Jun 30, 2025
9585dc3
Add joining, leaving and editing memberships
axu-1 Jun 30, 2025
3b0113a
Rename roster widget folder
axu-1 Jun 30, 2025
47c4b42
Add application join type to org card
axu-1 Jun 30, 2025
4c58974
Add exec board chip list to details card
axu-1 Jun 30, 2025
0bad72b
Add attribution for contributions
axu-1 Jun 30, 2025
c1eb638
Merge branch 'main' into organization-memberships
axu-1 Jun 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
167 changes: 163 additions & 4 deletions backend/api/organizations.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,31 @@

Organization routes are used to create, retrieve, and update Organizations."""

from fastapi import APIRouter, Depends
from fastapi import APIRouter, Depends, Body
from typing import Annotated

from ..services import OrganizationService, RoleService
from ..services.academics import TermService
from ..models.organization import Organization
from ..models.organization_membership import (
OrganizationMembership,
OrganizationMembershipRegistration,
OrganizationPermissionLevel,
OrganizationMembershipStatus,
)
from ..models.organization_details import OrganizationDetails
from ..api.authentication import registered_user
from ..models.user import User

__authors__ = ["Ajay Gandecha", "Jade Keegan", "Brianna Ta", "Audrey Toney"]
__copyright__ = "Copyright 2023"
from datetime import datetime

__authors__ = [
"Ajay Gandecha",
"Jade Keegan",
"Brianna Ta",
"Audrey Toney",
"Alanna Zhang",
]
__copyright__ = "Copyright 2025"
__license__ = "MIT"

api = APIRouter(prefix="/api/organizations")
Expand Down Expand Up @@ -141,3 +156,147 @@ def delete_organization(
"""

organization_service.delete(subject, slug)


@api.post(
"/{slug}/roster",
responses={404: {"model": None}},
response_model=OrganizationMembership,
tags=["Organizations"],
)
def add_membership(
slug: str,
membership_registration: Annotated[
OrganizationMembershipRegistration,
Body(
description="Details to create a new organization membership",
openapi_examples={
"default": {
"summary": "Default",
"value": {"user_id": 0, "organization_id": 0},
},
"custom": {
"summary": "Custom",
"value": {
"user_id": 0,
"organization_id": 0,
"title": "Member",
"permission_level": OrganizationPermissionLevel.MEMBER,
"status": OrganizationMembershipStatus.ACTIVE,
"term_id": "25S",
},
},
},
),
],
organization_service: OrganizationService = Depends(),
term_service: TermService = Depends(),
subject: User = Depends(registered_user),
) -> OrganizationMembership:
"""
Add membership to organization with matching slug

Parameters:
slug: a string representing a unique identifier for an Organization
membership_registration: an OrganizationMembershipRegistration object with info for a new OrganizationMembership
organization_service: a valid OrganizationService
subject: a valid User model representing the currently logged in User

Returns:
OrganizationMember: Created organization member

Raises:
HTTPException 404 if add_member() raises an Exception
"""
if membership_registration.term_id is None:
membership_registration.term_id = term_service.get_by_date(datetime.today()).id
return organization_service.add_membership(subject, slug, membership_registration)


@api.get(
"/{slug}/roster",
responses={404: {"model": None}},
response_model=list[OrganizationMembership],
tags=["Organizations"],
)
def get_roster_by_slug(
slug: str,
organization_service: OrganizationService = Depends(),
) -> list[OrganizationMembership]:
"""
Get organization roster with matching slug

Parameters:
slug: a string representing a unique identifier for an Organization
organization_service: a valid OrganizationService

Returns:
list[OrganizationMembership]: List of OrganizationMemberships of the organization with matching slug

Raises:
HTTPException 404 if get_roster() raises an Exception
"""
return organization_service.get_roster(slug)


@api.put(
"/{slug}/roster",
response_model=OrganizationMembership,
tags=["Organizations"],
)
def update_membership(
slug: str,
membership: Annotated[
OrganizationMembershipRegistration,
Body(
description="Details to modify an organization membership",
openapi_examples={
"model": {
"summary": "Default",
"value": {
"id": 0,
"user_id": 0,
"organization_id": 0,
"title": "Member",
"permission_level": OrganizationPermissionLevel.MEMBER,
"status": OrganizationMembershipStatus.ACTIVE,
"term_id": "25S",
},
},
},
),
],
subject: User = Depends(registered_user),
organization_service: OrganizationService = Depends(),
) -> OrganizationMembership:
"""
Update membership details

Parameters:
slug: a string representing a unique identifier for an Organization
membership: the OrganizationMembership to update
subject: a valid User model representing the currently logged in User
organization_service: a valid OrganizationService
"""
return organization_service.update_membership(subject, slug, membership)


@api.delete(
"/{slug}/roster/{membership_id}", response_model=None, tags=["Organizations"]
)
def delete_membership(
slug: str,
membership_id: int,
subject: User = Depends(registered_user),
organization_service: OrganizationService = Depends(),
):
"""
Delete membership based on membership_id

Parameters:
slug: a string representing a unique identifier for an Organization
membership_id: a unique OrganizationMembership id
subject: a valid User model representing the currently logged in User
organization_service: a valid OrganizationService
"""
organization_service.delete_membership(subject, slug, membership_id)
1 change: 1 addition & 0 deletions backend/entities/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from .room_entity import RoomEntity

from .organization_entity import OrganizationEntity
from .organization_membership_entity import OrganizationMembershipEntity
from .event_entity import EventEntity
from .event_registration_entity import EventRegistrationEntity

Expand Down
5 changes: 5 additions & 0 deletions backend/entities/academics/term_entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ class TermEntity(EntityBase):
back_populates="term", cascade="all, delete"
)

# NOTE: This field establishes a one-to-many relationship between the term and organization membership tables.
organization_memberships: Mapped[list["OrganizationMembershipEntity"]] = (
relationship(back_populates="term", cascade="all, delete")
)

@classmethod
def from_model(cls, model: Term) -> Self:
"""
Expand Down
38 changes: 34 additions & 4 deletions backend/entities/organization_entity.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
"""Definition of SQLAlchemy table-backed object mapping entity for Organizations."""

from sqlalchemy import Integer, String, Boolean
from sqlalchemy import Integer, String, Boolean, Enum as SQLAlchemyEnum
from sqlalchemy.orm import Mapped, mapped_column, relationship
from .entity_base import EntityBase
from typing import Self
from ..models.organization import Organization
from ..models.organization import Organization, OrganizationJoinType
from ..models.organization_details import OrganizationDetails

__authors__ = ["Ajay Gandecha", "Jade Keegan", "Brianna Ta", "Audrey Toney"]
__copyright__ = "Copyright 2023"
__authors__ = [
"Ajay Gandecha",
"Jade Keegan",
"Brianna Ta",
"Audrey Toney",
"Alanna Zhang",
]
__copyright__ = "Copyright 2025"
__license__ = "MIT"


Expand Down Expand Up @@ -48,6 +54,14 @@ class OrganizationEntity(EntityBase):
heel_life: Mapped[str] = mapped_column(String)
# Whether the organization can be joined by anyone or not
public: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
# Whether the organization is open, application-based, or closed
join_type: Mapped[OrganizationJoinType] = mapped_column(
SQLAlchemyEnum(OrganizationJoinType)
)
# Application link for an organization with APPLY join_type
application_url: Mapped[str | None] = mapped_column(
String, nullable=True, default=None
)

# NOTE: This field establishes a one-to-many relationship between the organizations and events table.
events: Mapped[list["EventEntity"]] = relationship(
Expand All @@ -59,6 +73,16 @@ class OrganizationEntity(EntityBase):
back_populates="organization", cascade="all,delete"
)

# NOTE: This field establishes a one-to-many relationship between the organizations and organization_member table.
members: Mapped[list["OrganizationMembershipEntity"]] = relationship(
back_populates="organization", cascade="all,delete"
)
users: Mapped[list["UserEntity"]] = relationship(
secondary="organization_membership",
back_populates="organizations",
viewonly=True,
)

@classmethod
def from_model(cls, model: Organization) -> Self:
"""
Expand All @@ -84,6 +108,8 @@ def from_model(cls, model: Organization) -> Self:
youtube=model.youtube,
heel_life=model.heel_life,
public=model.public,
join_type=model.join_type,
application_url=model.application_url,
)

def to_model(self) -> Organization:
Expand All @@ -108,6 +134,8 @@ def to_model(self) -> Organization:
youtube=self.youtube,
heel_life=self.heel_life,
public=self.public,
join_type=self.join_type,
application_url=self.application_url,
)

def to_details_model(self) -> OrganizationDetails:
Expand All @@ -132,5 +160,7 @@ def to_details_model(self) -> OrganizationDetails:
youtube=self.youtube,
heel_life=self.heel_life,
public=self.public,
join_type=self.join_type,
application_url=self.application_url,
events=[event.to_overview_model() for event in self.events],
)
96 changes: 96 additions & 0 deletions backend/entities/organization_membership_entity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
"""Definition of SQLAlchemy table-backed object mapping entity for Organization Members."""

from sqlalchemy import Integer, Boolean, String, ForeignKey, Enum as SQLAlchemyEnum
from sqlalchemy.orm import Mapped, mapped_column, relationship
from .entity_base import EntityBase
from typing import Self
from ..models.organization_membership import (
OrganizationMembership,
OrganizationMembershipRegistration,
OrganizationPermissionLevel,
OrganizationMembershipStatus,
)

__authors__ = [
"Alanna Zhang",
]
__copyright__ = "Copyright 2025"
__license__ = "MIT"


class OrganizationMembershipEntity(EntityBase):

__tablename__ = "organization_membership"

id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)

# The member's user information
# NOTE: This defines a one-to-many relationship between the user and organization membership tables.
user_id: Mapped[int] = mapped_column(ForeignKey("user.id"), primary_key=True)
user: Mapped["UserEntity"] = relationship(back_populates="memberships")

# Organization that the member is a part of
# NOTE: This defines a one-to-many relationship between the organization and organization membership tables.
organization_id: Mapped[int] = mapped_column(
ForeignKey("organization.id"), primary_key=True
)
organization: Mapped["OrganizationEntity"] = relationship(back_populates="members")

# Membership title, default value of "Member"
title: Mapped[str] = mapped_column(String, default="Member")

# Level of administrative privileges in organization
permission_level: Mapped["OrganizationPermissionLevel"] = mapped_column(
SQLAlchemyEnum(OrganizationPermissionLevel),
default=OrganizationPermissionLevel.MEMBER,
)

# Membership status
status: Mapped["OrganizationMembershipStatus"] = mapped_column(
SQLAlchemyEnum(OrganizationMembershipStatus)
)

# Membership term, default value of the current academic term
# NOTE: This defines a one-to-many relationship between the term and organization membership tables.
term_id: Mapped[str] = mapped_column(ForeignKey("academics__term.id"))
term: Mapped["TermEntity"] = relationship(back_populates="organization_memberships")

@classmethod
def from_model(
cls, model: OrganizationMembership | OrganizationMembershipRegistration
) -> Self:
"""Create an OrganizationMembershipEntity from an OrganizationMembership model or Registration model."""
if isinstance(model, OrganizationMembership):
return cls(
id=model.id,
user_id=model.user.id,
organization_id=model.organization_id,
title=model.title,
permission_level=model.permission_level,
status=model.status,
term_id=model.term.id,
)
else:
return cls(
id=model.id,
user_id=model.user_id,
organization_id=model.organization_id,
title=model.title,
permission_level=model.permission_level,
status=model.status,
term_id=model.term_id,
)

def to_model(self) -> OrganizationMembership:
"""Create an OrganizationMembership model from an OrganizationMembershipEntity."""
return OrganizationMembership(
id=self.id,
user=self.user.to_public_model(),
organization_id=self.organization_id,
organization_name=self.organization.name,
organization_slug=self.organization.slug,
title=self.title,
permission_level=self.permission_level,
status=self.status,
term=self.term.to_model(),
)
Loading