Skip to content

Commit

Permalink
Merge pull request #432 from bcgov/development
Browse files Browse the repository at this point in the history
release 5.6.0
  • Loading branch information
bdolor authored Nov 9, 2023
2 parents cec9939 + ef3b011 commit 8d083f8
Show file tree
Hide file tree
Showing 26 changed files with 1,452 additions and 277 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "bcgov-digital-marketplace",
"version": "3.1.1",
"version": "5.5.1",
"description": "Digital Marketplace",
"homepage": "https://marketplace.digital.gov.bc.ca",
"bugs": {
Expand Down
68 changes: 53 additions & 15 deletions src/back-end/lib/db/affiliation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,32 @@ export const approveAffiliation = tryDb<[Id], Affiliation>(
}
);

export const updateAdminStatus = tryDb<[Id, MembershipType], Affiliation>(
async (connection, id, membershipType: MembershipType) => {
const now = new Date();
const [result] = await connection<RawAffiliation>("affiliations")
.update(
{
membershipType,
updatedAt: now
} as RawAffiliation,
"*"
)
.where({
id
})
.whereIn("organization", function () {
this.select("id").from("organizations").where({
active: true
});
});
if (!result) {
throw new Error("unable to update admin status");
}
return valid(await rawAffiliationToAffiliation(connection, result));
}
);

export const deleteAffiliation = tryDb<[Id], Affiliation>(
async (connection, id) => {
const now = new Date();
Expand All @@ -274,24 +300,36 @@ export const deleteAffiliation = tryDb<[Id], Affiliation>(
}
);

export async function isUserOwnerOfOrg(
function makeIsUserTypeChecker(
membershipType: MembershipType
): (connection: Connection, user: User, ordId: Id) => Promise<boolean> {
return async (connection: Connection, user: User, orgId: Id) => {
if (!user) {
return false;
}
const result = await connection<RawAffiliation>("affiliations")
.where({
user: user.id,
organization: orgId,
membershipType,
membershipStatus: MembershipStatus.Active
})
.first();

return !!result;
};
}
const isUserAdminOfOrg = makeIsUserTypeChecker(MembershipType.Admin);

export const isUserOwnerOfOrg = makeIsUserTypeChecker(MembershipType.Owner);

export const isUserOwnerOrAdminOfOrg = async (
connection: Connection,
user: User,
orgId: Id
): Promise<boolean> {
if (!user) {
return false;
}
const result = await connection<RawAffiliation>("affiliations")
.where({
user: user.id,
organization: orgId,
membershipType: MembershipType.Owner
})
.first();

return !!result;
}
) =>
(await isUserOwnerOfOrg(connection, user, orgId)) ||
(await isUserAdminOfOrg(connection, user, orgId));

export async function readActiveOwnerCount(
connection: Connection,
Expand Down
63 changes: 42 additions & 21 deletions src/back-end/lib/db/organization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,8 @@ async function rawOrganizationSlimToOrganizationSlim(
possessOneServiceArea,
numTeamMembers,
active,
serviceAreas
serviceAreas,
viewerIsOrgAdmin
} = raw;
let fetchedLogoImageFile: FileRecord | undefined;
if (logoImageFile) {
Expand All @@ -130,7 +131,8 @@ async function rawOrganizationSlimToOrganizationSlim(
active,
numTeamMembers:
numTeamMembers === undefined ? undefined : parseInt(numTeamMembers, 10),
serviceAreas
serviceAreas,
...(viewerIsOrgAdmin ? { viewerIsOrgAdmin } : {})
};
}

Expand Down Expand Up @@ -180,18 +182,7 @@ function generateOrganizationQuery(connection: Connection) {
})
.as("numTeamMembers"),
connection.raw(
`(
SELECT
coalesce(
json_agg(sa),
'[]' :: json
) AS "serviceAreas"
FROM
"twuOrganizationServiceAreas" tosa
JOIN "serviceAreas" sa ON tosa."serviceArea" = sa.id
WHERE
tosa.organization = ?
)`,
'(select coalesce(json_agg(sa), \'[]\' :: json) as "serviceAreas" from "twuOrganizationServiceAreas" tosa join "serviceAreas" sa on tosa."serviceArea" = sa.id where tosa.organization = ?)',
connection.ref("organizations.id")
)
);
Expand Down Expand Up @@ -333,6 +324,24 @@ export const readManyOrganizations = tryDb<
query = query.andWhere({ "organizations.active": true });
}

// Used to render links for viewing organizations.
if (session) {
query = query.select(
connection.raw('exists(?) as "viewerIsOrgAdmin"', [
connection
.select("user")
.from("affiliations")
.where({
organization: connection.ref("organizations.id"),
membershipStatus: MembershipStatus.Active,
membershipType: MembershipType.Admin,
user: session.user.id
})
.first()
])
);
}

// Default is to only have one page because we are requesting everything.
let numPages = 1;

Expand Down Expand Up @@ -369,9 +378,13 @@ export const readManyOrganizations = tryDb<
acceptedTWUTerms,
acceptedSWUTerms,
active,
serviceAreas
serviceAreas,
viewerIsOrgAdmin
} = raw;
if (!isAdmin(session) && raw.owner !== session?.user.id) {
if (
!isAdmin(session) &&
!(raw.owner === session?.user.id || viewerIsOrgAdmin)
) {
return await rawOrganizationSlimToOrganizationSlim(connection, {
id,
legalName,
Expand All @@ -394,7 +407,8 @@ export const readManyOrganizations = tryDb<
possessOneServiceArea: serviceAreas.length > 0,
acceptedTWUTerms,
acceptedSWUTerms,
serviceAreas
serviceAreas,
viewerIsOrgAdmin
});
}
})
Expand All @@ -413,10 +427,17 @@ export const readOwnedOrganizations = tryDb<[Session], OrganizationSlim[]>(
return valid([]);
}
const results =
((await generateOrganizationQuery(connection).andWhere({
"organizations.active": true,
"affiliations.user": session.user.id
})) as RawOrganization[]) || [];
((await generateOrganizationQuery(connection)
.clearWhere()
.where(function () {
this.where({
"affiliations.membershipType": MembershipType.Owner
}).orWhere({ "affiliations.membershipType": MembershipType.Admin });
})
.andWhere({
"organizations.active": true,
"affiliations.user": session.user.id
})) as RawOrganization[]) || [];
return valid(
await Promise.all(
results.map(async (raw) => {
Expand Down
134 changes: 133 additions & 1 deletion src/back-end/lib/mailer/notifications/opportunity/code-with-us.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,141 @@ import React from "react";
import { CONTACT_EMAIL } from "shared/config";
import { formatAmount, formatDate, formatTime } from "shared/lib";
import { CWUOpportunity } from "shared/lib/resources/opportunity/code-with-us";
import { User } from "shared/lib/resources/user";
import { User, UserType } from "shared/lib/resources/user";
import { getValidValue } from "shared/lib/validation";

export async function handleCWUSubmittedForReview(
connection: db.Connection,
opportunity: CWUOpportunity
): Promise<void> {
// Notify all admin users of the submitted CWU
const adminUsers =
getValidValue(
await db.readManyUsersByRole(connection, UserType.Admin),
null
) || [];
await Promise.all(
adminUsers.map(
async (admin) =>
await newCWUOpportunitySubmittedForReview(admin, opportunity)
)
);

// Notify the authoring gov user of the submission
const author =
opportunity.createdBy &&
getValidValue(
await db.readOneUser(connection, opportunity.createdBy.id),
null
);
if (author) {
await newCWUOpportunitySubmittedForReviewAuthor(author, opportunity);
}
}

/**
* wrapper
*/
export const newCWUOpportunitySubmittedForReview = makeSend(
newCWUOpportunitySubmittedForReviewT
);

/**
* Creates content for an email.
*
* @param recipient - object, someone to send it to, in this case the administrators of the system
* @param opportunity - object, a CWU opportunity
* @returns - object, an email template with content
*/
export async function newCWUOpportunitySubmittedForReviewT(
recipient: User,
opportunity: CWUOpportunity
): Promise<Emails> {
const title = "A Code With Us Opportunity Has Been Submitted For Review";
const description =
"The following Digital Marketplace opportunity has been submitted for review:";
return [
{
summary:
"CWU opportunity submitted for review; sent to all administrators for the system.",
to: recipient.email || [],
subject: title,
html: templates.simple({
title,
description,
descriptionLists: [makeCWUOpportunityInformation(opportunity)],
body: (
<div>
<p>
You can review and publish this opportunity by{" "}
<templates.Link
text="signing in"
url={templates.makeUrl("sign-in")}
/>{" "}
and accessing it from the opportunity list.
</p>
<p>
You can also edit the opportunity prior to publishing. The
opportunity author will be notified when you publish, and the
opportunity will made visible to the public.
</p>
</div>
),
callsToAction: [viewCWUOpportunityCallToAction(opportunity)]
})
}
];
}

/**
* wrapper
*/
export const newCWUOpportunitySubmittedForReviewAuthor = makeSend(
newCWUOpportunitySubmittedForReviewAuthorT
);

/**
* Creates content for an email.
*
* @param recipient - object, someone to send it to, in this case the author of the opportunity
* @param opportunity - object, a CWU opportunity
* @returns - object, an email template with content
*/
export async function newCWUOpportunitySubmittedForReviewAuthorT(
recipient: User,
opportunity: CWUOpportunity
): Promise<Emails> {
const title = "Your Code With Us Opportunity Has Been Submitted For Review"; // Used for subject line and heading
const description =
"You have submitted the following Digital Marketplace opportunity for review:";
return [
{
summary:
"CWU opportunity submitted for review; sent to the submitting government user.",
to: recipient.email || "",
subject: title,
html: templates.simple({
title,
description,
descriptionLists: [makeCWUOpportunityInformation(opportunity)],
body: (
<div>
<p>
An administrator will review your opportunity. You will be
notified once the opportunity has been posted.
</p>
<p>
If you have any questions, please send an email to{" "}
<templates.Link text={CONTACT_EMAIL} url={CONTACT_EMAIL} />.
</p>
</div>
),
callsToAction: [viewCWUOpportunityCallToAction(opportunity)]
})
}
];
}

export async function handleCWUPublished(
connection: db.Connection,
opportunity: CWUOpportunity,
Expand Down
Loading

0 comments on commit 8d083f8

Please sign in to comment.