Skip to content

Commit

Permalink
Merge branch 'cc2785/team-alumni' of github.com:cornell-dti/idol into…
Browse files Browse the repository at this point in the history
… cc2785/team-alumni
  • Loading branch information
cchrischen committed Aug 27, 2024
2 parents c149add + 0522fcd commit e36dd7a
Show file tree
Hide file tree
Showing 35 changed files with 821 additions and 54 deletions.
2 changes: 2 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
node_modules
build
dti-website
dti-website-redesign
6 changes: 3 additions & 3 deletions .github/workflows/backup-prod-db.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
with:
token: ${{ secrets.BOT_TOKEN }}
- uses: actions/setup-node@v2
- uses: actions/setup-node@v4
with:
node-version-file: .node-version
- name: Use Yarn Cache
uses: actions/cache@v1
uses: actions/cache@v4
with:
path: ~/.cache/yarn
key: yarn-${{ hashFiles(format('{0}{1}', github.workspace, '/yarn.lock')) }}
Expand Down
10 changes: 5 additions & 5 deletions .github/workflows/ci-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: .node-version
- name: Use Yarn Cache
uses: actions/cache@v1
uses: actions/cache@v4
with:
path: ~/.cache/yarn
key: yarn-${{ hashFiles(format('{0}{1}', github.workspace, '/yarn.lock')) }}
Expand Down Expand Up @@ -45,8 +45,8 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: .node-version
- name: Use Yarn Cache
Expand Down
10 changes: 5 additions & 5 deletions .github/workflows/idol-notifications.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@ jobs:
profile-update-notifications:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
with:
token: ${{ secrets.BOT_TOKEN }}
- uses: actions/setup-node@v2
- uses: actions/setup-node@v4
with:
node-version-file: .node-version
- name: Use Yarn Cache
uses: actions/cache@v1
uses: actions/cache@v4
with:
path: ~/.cache/yarn
key: yarn-${{ hashFiles(format('{0}{1}', github.workspace, '/yarn.lock')) }}
Expand All @@ -34,10 +34,10 @@ jobs:
tec-request-notification:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
with:
token: ${{ secrets.BOT_TOKEN }}
- uses: actions/setup-node@v2
- uses: actions/setup-node@v4
with:
node-version-file: .node-version
- name: Use Yarn Cache
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/pull-from-idol.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
with:
token: ${{ secrets.BOT_TOKEN }}
- uses: actions/setup-node@v2
- uses: actions/setup-node@v4
with:
node-version-file: .node-version
- name: Use Yarn Cache
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/restore-from-backup.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
with:
token: ${{ secrets.BOT_TOKEN }}
- uses: actions/setup-node@v2
- uses: actions/setup-node@v4
with:
node-version-file: .node-version
- name: Use Yarn Cache
uses: actions/cache@v1
uses: actions/cache@v4
with:
path: ~/.cache/yarn
key: yarn-${{ hashFiles(format('{0}{1}', github.workspace, '/yarn.lock')) }}
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/update-dev-db.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
with:
token: ${{ secrets.BOT_TOKEN }}
- uses: actions/setup-node@v2
- uses: actions/setup-node@v4
with:
node-version-file: .node-version
- name: Use Yarn Cache
uses: actions/cache@v1
uses: actions/cache@v4
with:
path: ~/.cache/yarn
key: yarn-${{ hashFiles(format('{0}{1}', github.workspace, '/yarn.lock')) }}
Expand Down
21 changes: 21 additions & 0 deletions backend/src/API/memberAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,3 +160,24 @@ export const reviewUserInformationChange = async (
MembersDao.revertMemberInformationChanges(rejected)
]);
};

/**
* Generates an archive of all IDOL members, separated into three categories: current, inactive, and alumni.
* @param membershipChanges - an object with lists of NetIds corresponding to the status of IDOL members in the next semester.
* @param user - the `IdolMember` submitting the request.
* @param semesters - the number of previous semesters to look back, undefined if no limit.
* @returns an object with the categories as the keys, each with value `MemberProfile[]`.
*/
export const generateMemberArchive = async (
membershipChanges: { [key: string]: string[] },
user: IdolMember,
semesters: number | undefined
): Promise<{ [key: string]: string[] }> => {
const canEditMembers = await PermissionsManager.canEditMembers(user);
if (!canEditMembers) {
throw new PermissionError(
`User with email: ${user.email} does not have permission to generate an archive!`
);
}
return MembersDao.generateArchive(membershipChanges, semesters);
};
7 changes: 6 additions & 1 deletion backend/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ import {
deleteMember,
updateMember,
getUserInformationDifference,
reviewUserInformationChange
reviewUserInformationChange,
generateMemberArchive
} from './API/memberAPI';
import { getMemberImage, setMemberImage, allMemberImages } from './API/imageAPI';
import { allTeams, setTeam, deleteTeam } from './API/teamAPI';
Expand Down Expand Up @@ -228,6 +229,10 @@ loginCheckedPut('/memberDiffs', async (req, user) => ({
member: await reviewUserInformationChange(req.body.approved, req.body.rejected, user)
}));

loginCheckedPost('/member-archive', async (req, user) => ({
archive: await generateMemberArchive(req.body, user, req.query.semesters as number | undefined)
}));

// Teams
loginCheckedGet('/team', async () => ({ teams: await allTeams() }));
loginCheckedPut('/team', async (req, user) => ({
Expand Down
56 changes: 56 additions & 0 deletions backend/src/dao/MembersDao.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { db, approvedMemberCollection, memberCollection } from '../firebase';
import { Team } from '../types/DataTypes';
import { archivedMembersBySemesters, archivedMembersByEmail } from '../members-archive';
import BaseDao from './BaseDao';
import { allMemberImages } from '../API/imageAPI';

export default class MembersDao extends BaseDao<IdolMember, IdolMember> {
constructor() {
Expand Down Expand Up @@ -157,4 +158,59 @@ export default class MembersDao extends BaseDao<IdolMember, IdolMember> {
if (team.leaders.length + team.members.length + team.formerMembers.length === 0) return null;
return team;
}

/**
* Generates an archive of all IDOL members into three categories: current, alumni, and inactive.
* The data for each member includes additional information for display on the website.
* @param updates - the status of IDOL members in the next semester.
* @param semesters - the limit, if any, of the number of semesters to include in archive.
* @returns A promise that resolves to an Archive object.
*/
static async generateArchive(
updates: {
[key: string]: string[];
},
semesters: number | undefined
): Promise<{ [key: string]: string[] }> {
const allMembers: Set<string> = new Set();
const archive = { current: [], alumni: [], inactive: [] };

const currentMembers = await MembersDao.getAllMembers(true);
const images = await allMemberImages();

const addToArchive = (returning: boolean, member: IdolMember) => {
allMembers.add(member.netid);
const image = images.find((image) => image.fileName.split('.')[0] === member.netid);

let key = 'current';
if (!returning) {
key = Date.parse(member.graduation) < Date.now() ? 'alumni' : 'inactive';
}

archive[key].push({
...member,
image: image ? image.url : null,
coffeeChatLink: `mailto:${member.email}`
});
};

// Each current member's netid should be found in the returning members survey.
currentMembers.forEach((member) => {
addToArchive(updates.returning.includes(member.netid), member);
});

Object.values(archivedMembersBySemesters)
.reverse()
.forEach((semester, index) => {
if (!semesters || index < semesters) {
semester.forEach((member) => {
if (!allMembers.has(member.netid)) {
addToArchive(false, member);
}
});
}
});

return archive;
}
}
4 changes: 1 addition & 3 deletions backend/src/dao/TeamEventsDao.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,7 @@ export default class TeamEventsDao {

/* Returns all team event information details through 'TeamEventInfo' objects */
static async getAllTeamEventInfo(): Promise<TeamEventInfo[]> {
const docRefs = await teamEventsCollection
.select('name', 'date', 'numCredits', 'hasHours', 'uuid', 'isInitiativeEvent')
.get();
const docRefs = await teamEventsCollection.get();
return docRefs.docs.map((doc) => doc.data() as TeamEventInfo);
}
}
11 changes: 10 additions & 1 deletion common-types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ interface IdolMemberDiff {
readonly diffString: string;
}

/** The data type used by Nova site to represent a DTI member. */
/** The data type used by Nova site to represent a DTI member.
* @deprecated used in past DTI websites
*/
interface NovaMember {
readonly netid: string;
readonly name: string;
Expand All @@ -62,6 +64,12 @@ interface NovaMember {
readonly roleDescription: string;
}

/** The data type used by new DTI website to represent a DTI member. */
interface MemberProfile extends IdolMember {
readonly image?: string | null;
readonly coffeeChatLink?: string | null;
}

interface ProfileImage {
readonly url: string;
readonly fileName: string;
Expand Down Expand Up @@ -101,6 +109,7 @@ interface TeamEventInfo {
readonly uuid: string;
readonly isCommunity?: boolean;
readonly isInitiativeEvent: boolean;
readonly maxCredits: string;
}

interface TeamEvent extends TeamEventInfo {
Expand Down
1 change: 1 addition & 0 deletions frontend/public/sample_returning_members.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
NetID,Returning
6 changes: 6 additions & 0 deletions frontend/src/API/MembersAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ export class MembersAPI {
);
}

public static getArchive(body: {
[key: string]: string[];
}): Promise<{ [key: string]: MemberProfile[] }> {
return APIWrapper.post(`${backendURL}/member-archive`, body).then((res) => res.data);
}

public static notifyMember(
member: Member,
endOfSemesterReminder: boolean
Expand Down
66 changes: 66 additions & 0 deletions frontend/src/components/Admin/AddUser/AddUser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ export default function AddUser(): JSX.Element {
});
const [csvFile, setCsvFile] = useState<File | undefined>(undefined);
const [uploadStatus, setUploadStatus] = useState<UploadStatus>();
const [archiveCsvFile, setArchiveCsvFile] = useState<File | undefined>(undefined);
const [loading, setLoading] = useState<boolean>(false);

function createNewUser(): void {
setState({
Expand Down Expand Up @@ -291,6 +293,44 @@ export default function AddUser(): JSX.Element {
});
}

async function processSurvey(survey: File) {
const fields = ['NetID', 'Returning'];
const rows = (await survey.text()).split('\n');
const tokens = rows.map((row) => {
const words = row.split(',');
return words.map((word, index) =>
index === words.length - 1 ? word.substring(0, word.length - 1) : word
);
});

const [netID, returning] = fields.map((field) => tokens[0].indexOf(field));

if (netID === -1 || returning === -1) {
return;
}

setLoading(true);

const json: { [key: string]: string[] } = { returning: [], leaving: [] };
tokens.forEach((row, index) => {
if (index === 0 || index === rows.length - 1) return;
if (row[returning].toLowerCase() === 'yes') {
json.returning.push(row[netID]);
} else {
json.leaving.push(row[netID]);
}
});

const archive = await MembersAPI.getArchive(json);

const download = document.createElement('a');
download.href = `data:text/json;charset=utf-8,${encodeURIComponent(JSON.stringify(archive))}`;
download.download = 'archive.json';
download.click();

setLoading(false);
}

return (
<div className={styles.AddUser} data-testid="AddUser">
<ErrorModal onEmitter={Emitters.userEditError}></ErrorModal>
Expand Down Expand Up @@ -342,6 +382,7 @@ export default function AddUser(): JSX.Element {
</div>
</Card.Content>
<Card.Content>
<h3>Update Members</h3>
{csvFile ? (
<div className={`ui one buttons ${styles.fullWidth}`}>
<Button
Expand Down Expand Up @@ -380,6 +421,31 @@ export default function AddUser(): JSX.Element {
</div>
) : undefined}
</Card.Content>
<Card.Content>
<h3>Generate Archive</h3>
{archiveCsvFile ? (
<div className={`ui one buttons ${styles.fullWidth}`}>
<Button
basic
color="green"
className={styles.fullWidth}
onClick={() => {
processSurvey(archiveCsvFile);
}}
disabled={loading}
>
{loading ? 'Generating...' : `Download Archive: ${archiveCsvFile.name}`}
</Button>
</div>
) : undefined}
<input
className={styles.wrap}
type="file"
accept=".csv"
onChange={(e) => setArchiveCsvFile(e.target.files?.[0])}
/>
<a href="/sample_returning_members.csv">Download sample .csv file</a>
</Card.Content>
</Card>
{state.currentSelectedMember !== undefined ? (
<Card className={styles.userEditor}>
Expand Down
Loading

0 comments on commit e36dd7a

Please sign in to comment.