Skip to content

Commit 2c16322

Browse files
authored
Merge pull request #477 from STAPLE-verse/dev
merge new features into main
2 parents bf60545 + 4bc61f0 commit 2c16322

101 files changed

Lines changed: 2272 additions & 660 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/CONTRIBUTING.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,40 @@ flowchart LR
5858
C -->|Declined| F[Resubmit if requested]
5959
E -->|Accept| D[Merged dev]
6060
```
61+
62+
## Developer Guidelines
63+
64+
### Clear and Helpful Commit Messages
65+
66+
When you make a commit (a save point in our project), it's important to describe what you've changed in a way that's easy to understand. Use simple, active sentences. For example, say "Update button styles" instead of something vague like "changed styling." This helps everyone see what's been done just by looking at the commit history.
67+
Keep Your Branch Up-to-Date
68+
69+
If you’re working on a feature in a separate branch, it’s a good idea to regularly pull in the latest changes from the main branch. This helps prevent conflicts later and keeps your work relevant. Also, if you’ve been working on a branch for a long time without pulling updates, let the team know why in your pull request. This way, everyone understands your reasoning.
70+
71+
### Protecting the Main Branch
72+
73+
We want to make sure only high-quality code makes it into our main branch, which is what goes into production. To do this, we’ll require all changes to go through a pull request (PR). This means your code needs to be reviewed by someone else, and automated checks will be run to catch any issues before they become bigger problems.
74+
75+
### Keep Pull Requests Small and Focused
76+
77+
When possible, try to keep your pull requests small and focused on one thing. This makes it easier for others to review your changes quickly and reduces the chance of missing issues. If a pull request is too big, it can take longer to review and might introduce more bugs.
78+
79+
### Automated Testing
80+
81+
Before you commit your code, it’s important to write tests to make sure everything works as expected. Automated tests help keep our project stable by catching issues early. You can use GitHub Actions to run these tests automatically, so you know everything is working before you merge your changes into the main branch.
82+
83+
### Write and Keep Up Documentation
84+
85+
Good documentation is key to helping everyone understand how our project works. A clear README file and other documents can save a lot of time. It’s helpful to document both public info (like how to set up the project on your computer) and private info (like how our servers are managed). This way, everyone can find the information they need.
86+
87+
### Use GitHub Issues for Proposing Changes
88+
89+
Before you start working on something new, it’s a good idea to open a GitHub issue to get feedback. This can help you refine your ideas and make sure you’re on the right track. It also helps avoid doing work that might need to be redone later. Getting early feedback can save time and make sure your efforts are aligned with the team.
90+
91+
### Write Clear GitHub Issues
92+
93+
When you create a GitHub issue, try to include all the information needed to understand the problem or task. If you don’t have all the details, ask questions and tag the right people. A well-written issue is half the work, making it easier for someone to jump in and help.
94+
95+
### Communicate Clearly
96+
97+
Good communication is key to working well together. Before you send a message or comment, think about how others will read it. Try to make your point clear and simple. If you’re in a hurry, it’s easy to be misunderstood, so take a moment to check if your message is clear and concise. This will help others understand you better and keep things moving smoothly.

LICENSE.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@ Copyright (c) 2025 STAPLE Development Team
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the “Software”), to deal
7-
in the Software without restriction, including without limitation the rights
8-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
99
copies of the Software, subject to the following conditions:
1010

1111
1. **Attribution** must be given to the original authors of the STAPLE software.
12-
2. **Non-Commercial Use Only**: The Software may not be used, in whole or in part, for commercial purposes without prior written permission from the copyright holders.
12+
2. **Non-Commercial Use Only**: The Software may not be used, in whole or in part, for commercial purposes without prior written permission from the copyright holders.
1313
- “Commercial purposes” include selling the Software, offering it as a service for a fee, or incorporating it into a commercial product or platform.
1414
3. The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
1515

README.md

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,25 @@
11
## STAPLE: Software for Scientists
22

3-
Cite: [![DOI](https://zenodo.org/badge/665542257.svg)](https://doi.org/10.5281/zenodo.13916969)
3+
*A research project management platform for open, transparent, and collaborative science.*
44

5-
(use https://doi.org/10.5281/zenodo.13916969 for the concept version, or use the specific doi for a release if desired).
5+
[![DOI](https://zenodo.org/badge/665542257.svg)](https://doi.org/10.5281/zenodo.13916969)
6+
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
7+
[![Build Status](https://github.com/STAPLE-verse/STAPLE/actions/workflows/dry-run.yml/badge.svg)](https://github.com/STAPLE-verse/STAPLE/actions/workflows/dry-run.yml)
8+
[![Contributions Welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg)](https://github.com/STAPLE-verse/STAPLE?tab=contributing-ov-file)
9+
[![Slack](https://img.shields.io/badge/chat-on%20Slack-purple.svg?logo=slack)]([https://your-slack-invite-link](https://join.slack.com/t/staple-talk/shared_invite/zt-25c08jrdt-f66do2kbIZExpAou5ZQYew))
610

7-
Docs:
11+
### 📖 Documentation:
812

9-
Contribute:
13+
👉 [Full installation and usage guide](https://staple.science/documentation/)
14+
15+
### 📦 Citation
16+
17+
If you use STAPLE in your work, please cite it using the concept DOI or a release-specific DOI:
18+
19+
- Concept DOI: https://doi.org/10.5281/zenodo.13916969
20+
- For specific releases, use the DOI associated with that release (badge above links to the latest).
21+
22+
### 🤝 Contributing
23+
24+
We welcome contributions of all kinds — code, documentation, testing, and feedback.
25+
[See our contribution guidelines.](https://github.com/STAPLE-verse/STAPLE?tab=contributing-ov-file)

cron/cronJobMailer.mjs

Lines changed: 143 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,12 @@ import { resolver } from "@blitzjs/rpc"
77

88
const db = new PrismaClient() // Create Prisma client instance
99

10+
function fmtDate(date) {
11+
return moment(date).format("MMM D, YYYY")
12+
}
13+
1014
// Helper function to create email content
11-
function createDailyNotification(email, notificationContent) {
15+
function createDailyNotification(email, notificationContent, overdueContent) {
1216
const html_message = `
1317
<html>
1418
<body>
@@ -20,11 +24,15 @@ function createDailyNotification(email, notificationContent) {
2024
<h3>STAPLE Daily Notifications</h3>
2125
2226
<p>
23-
This email is to notify you about recent updates to your project.
24-
Here are new announcements, tasks, and other project updates:
25-
</p>
27+
This email is to notify you about overdue tasks and recent updates to your project(s).
28+
You can view all notifications on the <a href="https://app.staple.science/auth/login?next=%2Fnotifications">Notifications page</a>.
29+
</p>
30+
31+
<h3>⏰ Overdue Tasks</h3>
32+
<div style="margin:0 0 16px;">${overdueContent}</div>
2633
27-
${notificationContent}
34+
<h3>📢 Project Updates</h3>
35+
<div style="margin:0 0 16px;">${notificationContent}</div>
2836
</body>
2937
</html>
3038
`
@@ -79,6 +87,85 @@ export async function fetchAndGroupNotifications() {
7987
}, {})
8088
}
8189

90+
// Function to fetch and group overdue tasks by email and project
91+
export async function fetchAndGroupOverdueTasks() {
92+
const now = new Date()
93+
94+
const tasks = await db.task.findMany({
95+
where: {
96+
deadline: { lt: now },
97+
},
98+
include: {
99+
project: { select: { name: true } },
100+
assignedMembers: {
101+
include: {
102+
users: { select: { email: true } },
103+
},
104+
},
105+
taskLogs: {
106+
select: {
107+
id: true,
108+
createdAt: true,
109+
assignedToId: true,
110+
status: true,
111+
completedById: true,
112+
completedAs: true,
113+
},
114+
orderBy: { createdAt: "desc" },
115+
},
116+
},
117+
orderBy: { deadline: "asc" },
118+
})
119+
120+
// A task counts as overdue for a member only if that member has a latest log and it is NOT_COMPLETED.
121+
// If there is no log for that member, assume not assigned → do not include.
122+
const isUnfinishedLatest = (log) => {
123+
if (!log) return false
124+
const s = (log.status || "").toString().toUpperCase()
125+
return s === "NOT_COMPLETED"
126+
}
127+
128+
// Group as: email -> projectName -> [task rows]
129+
return tasks.reduce((acc, task) => {
130+
const projectName = task?.project?.name || "No Project"
131+
const taskName = task?.name || `Task #${task?.id}`
132+
const due = task?.deadline ? fmtDate(task.deadline) : "no due date"
133+
const pastDeadline = task?.deadline && task.deadline < now
134+
135+
// Map latest TaskLog by assignee (assignedToId) — schema note: TaskLog does not have projectmemberId
136+
const latestByMember = new Map()
137+
for (const log of task.taskLogs || []) {
138+
if (!latestByMember.has(log.assignedToId)) {
139+
latestByMember.set(log.assignedToId, log)
140+
}
141+
}
142+
143+
const members = task?.assignedMembers || []
144+
if (members.length === 0) return acc
145+
146+
for (const m of members) {
147+
const latest = latestByMember.get(m.id)
148+
const isUnfinished = isUnfinishedLatest(latest)
149+
150+
if (pastDeadline && isUnfinished) {
151+
const line = `${projectName} - ${taskName} - Due: ${due}`
152+
const users = m?.users || []
153+
for (const u of users) {
154+
const email = u?.email
155+
if (!email) continue
156+
if (!acc[email]) acc[email] = {}
157+
if (!acc[email][projectName]) acc[email][projectName] = []
158+
acc[email][projectName].push(line)
159+
}
160+
161+
// TODO: If assignment is to a team, add logic here to notify team distribution list or members
162+
}
163+
}
164+
165+
return acc
166+
}, {})
167+
}
168+
82169
// Function to introduce a delay (in milliseconds)
83170
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
84171

@@ -105,19 +192,43 @@ const checkRateLimit = async () => {
105192
}
106193
}
107194
// Function to send grouped notifications
108-
export async function sendGroupedNotifications(groupedNotifications) {
195+
export async function sendGroupedNotifications(groupedNotifications, groupedOverdues) {
109196
const delayTime = 500 // Delay time between each email in milliseconds (e.g., 1 second)
110197

111-
for (const [email, projects] of Object.entries(groupedNotifications)) {
112-
const notificationContent = Object.entries(projects)
113-
.map(([projectName, messages]) => {
114-
const projectHeader = `<h4>Project: ${projectName}</h4>`
115-
const messagesList = messages.map((message) => `<li>${message}</li>`).join("")
116-
return projectHeader + `<ul>${messagesList}</ul>`
117-
})
118-
.join("")
198+
const allEmails = new Set([
199+
...Object.keys(groupedNotifications || {}),
200+
...Object.keys(groupedOverdues || {}),
201+
])
202+
203+
for (const email of allEmails) {
204+
const projects = groupedNotifications?.[email] || {}
119205

120-
const emailContent = createDailyNotification(email, notificationContent)
206+
const notificationContent =
207+
Object.entries(projects)
208+
.map(([projectName, messages]) => {
209+
const projectHeader = `<h4>Project: ${projectName}</h4>`
210+
const messagesList = messages.map((message) => `<li>${message}</li>`).join("")
211+
return projectHeader + `<ul>${messagesList}</ul>`
212+
})
213+
.join("") || "<p>No new updates in the last 24 hours.</p>"
214+
215+
// Build overdue content for this recipient (if any)
216+
const overdueProjects = groupedOverdues?.[email] || {}
217+
const overdueContent =
218+
Object.entries(overdueProjects)
219+
.map(([projectName, rows]) => {
220+
const projectHeader = `<h4>Project: ${projectName}</h4>`
221+
const items = rows.map((row) => `<li>${row}</li>`).join("")
222+
return projectHeader + `<ul>${items}</ul>`
223+
})
224+
.join("") || "<p>No overdue tasks 🎉</p>"
225+
226+
const emailContent = createDailyNotification(email, notificationContent, overdueContent)
227+
228+
console.log(
229+
`[Mailer] Prepared email for ${email}: hasOverdues=${!!Object.keys(overdueProjects)
230+
.length}, hasUpdates=${!!Object.keys(projects).length}`
231+
)
121232

122233
// Check rate limit before sending email
123234
await checkRateLimit()
@@ -130,10 +241,18 @@ export async function sendGroupedNotifications(groupedNotifications) {
130241
body: JSON.stringify(emailContent),
131242
})
132243

244+
const respText = await response.text().catch(() => "<no body>")
133245
if (!response.ok) {
134-
console.error(`Failed to send email to ${email}:`, response.statusText)
246+
console.error(
247+
`Failed to send email to ${email}: ${response.status} ${response.statusText}${respText}`
248+
)
135249
} else {
136-
console.log(`Email sent successfully to ${email}`)
250+
console.log(
251+
`Email sent successfully to ${email}: ${response.status}${respText.substring(
252+
0,
253+
120
254+
)}...`
255+
)
137256
}
138257

139258
emailCount++ // Increment the email count after sending each email
@@ -144,13 +263,18 @@ export async function sendGroupedNotifications(groupedNotifications) {
144263
console.error(`Error sending email to ${email}:`, error)
145264
}
146265
}
266+
267+
console.log(`[Mailer] Processed ${allEmails.size} recipients.`)
147268
}
148269

149270
// Function to fetch and send daily notifications
150271
async function sendDailyNotifications() {
151272
try {
152-
const groupedNotifications = await fetchAndGroupNotifications()
153-
await sendGroupedNotifications(groupedNotifications)
273+
const [groupedNotifications, groupedOverdues] = await Promise.all([
274+
fetchAndGroupNotifications(),
275+
fetchAndGroupOverdueTasks(),
276+
])
277+
await sendGroupedNotifications(groupedNotifications, groupedOverdues)
154278
} catch (error) {
155279
console.error("Error in sendDailyNotifications:", error)
156280
}

db/migrations/migration_lock.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Please do not edit this file manually
2+
# It should be added in your version-control system (i.e. Git)
3+
provider = "postgresql"

db/schema.prisma

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,13 @@ enum Status {
170170
NOT_COMPLETED
171171
}
172172

173+
enum AutoAssignNew {
174+
NONE
175+
CONTRIBUTOR
176+
TEAM
177+
ALL
178+
}
179+
173180
model Task {
174181
id Int @id @default(autoincrement())
175182
createdAt DateTime @default(now())
@@ -200,6 +207,8 @@ model Task {
200207
assignedMembers ProjectMember[] @relation("AssignedTasks")
201208
elementId Int?
202209
element Element? @relation(fields: [elementId], references: [id])
210+
autoAssignNew AutoAssignNew @default(NONE)
211+
anonymous Boolean @default(false)
203212
}
204213

205214
model TaskLog {

integrations/emails.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ alt="STAPLE Logo" height="200"></center>
236236
<h3>STAPLE Password Change</h3>
237237
238238
This email is to notify you that you recently updated your
239-
password. If you did not make this change, please
239+
password at https://app.staple.science. If you did not make this change, please
240240
contact us immediately.
241241
<p>
242242
If you need more help, you can reply to this email to create a ticket.
@@ -289,7 +289,7 @@ alt="STAPLE Logo" height="200"></center>
289289
<h3>STAPLE Profile Change</h3>
290290
291291
This email is to notify you that you recently updated your
292-
profile information. If you did not make this change, please
292+
profile information at https://app.staple.science. If you did not make this change, please
293293
contact us immediately.
294294
<p>
295295
If you need more help, you can reply to this email to create a ticket.

integrations/mailer.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import nodemailer from "nodemailer"
33
import * as aws from "@aws-sdk/client-ses"
44
import { Resend } from "resend"
55

6+
// use for Gmail
67
export async function Mailer(msg) {
78
const pass = process.env.EMAIL_PASS
89

@@ -20,6 +21,7 @@ export async function Mailer(msg) {
2021
})
2122
}
2223

24+
// use for Amazon
2325
export async function Amazon(msg) {
2426
const ses = new aws.SES({
2527
apiVersion: "2010-12-01",
@@ -42,6 +44,7 @@ export async function Amazon(msg) {
4244
}
4345
}
4446

47+
// use for Resend
4548
const resend = new Resend(process.env.RESEND_API_KEY)
4649

4750
export async function ResendMsg(msg) {

mailers/forgotPasswordMailer.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,6 @@ export async function forgotPasswordMailer({ to, token }: ResetPasswordMailer) {
2222

2323
//send the email
2424
await ResendMsg(createForgotPasswordMsg(to, resetUrl))
25+
// await Amazon(createForgotPasswordMsg(to, resetUrl)) # or amazon
26+
// await Mailer(createForgotPasswordMsg(to, resetUrl)) # or gmail
2527
}

0 commit comments

Comments
 (0)