Skip to content

Commit

Permalink
Added absentemail setting
Browse files Browse the repository at this point in the history
  • Loading branch information
clr-li committed Apr 28, 2024
1 parent 3f31e4b commit 059b3e3
Show file tree
Hide file tree
Showing 11 changed files with 195 additions and 60 deletions.
8 changes: 4 additions & 4 deletions .badges/coverage.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
25 changes: 25 additions & 0 deletions migrations/0002_modify_businesses.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Automatically created by 'sqlite auto migrator (SAM)' on 2024-04-28T18:51:38.159Z

import { Database } from 'sqlite-auto-migrator';

// Pragmas can't be changed in transactions, so they are tracked separately.
// Note that most pragmas are not persisted in the database file and will have to be set on each new connection.
export const PRAGMAS = {"analysis_limit":0,"application_id":0,"auto_vacuum":0,"automatic_index":1,"timeout":1000,"cache_size":-2000,"cache_spill":483,"cell_size_check":0,"checkpoint_fullfsync":0,"seq":0,"name":"Records","compile_options":"ATOMIC_INTRINSICS=1","count_changes":0,"data_version":1,"file":"","defer_foreign_keys":0,"empty_result_callbacks":0,"encoding":"UTF-8","foreign_keys":0,"freelist_count":0,"full_column_names":0,"fullfsync":0,"builtin":1,"type":"table","enc":"utf8","narg":2,"flags":2099200,"hard_heap_limit":0,"ignore_check_constraints":0,"integrity_check":"ok","journal_mode":"delete","journal_size_limit":-1,"legacy_alter_table":0,"locking_mode":"normal","max_page_count":1073741823,"page_count":12,"page_size":4096,"query_only":0,"quick_check":"ok","read_uncommitted":0,"recursive_triggers":0,"reverse_unordered_selects":0,"schema_version":5,"secure_delete":0,"short_column_names":1,"soft_heap_limit":0,"synchronous":2,"schema":"main","ncol":5,"wr":0,"strict":0,"temp_store":0,"threads":0,"trusted_schema":1,"user_version":0,"wal_autocheckpoint":1000,"busy":0,"log":-1,"checkpointed":-1,"writable_schema":0};

/**
* Runs the necessary SQL commands to migrate the database up to this version from the previous version.
* Automatically runs in a transaction with deferred foreign keys.
* @param {Database} db database instance to run SQL commands on
*/
export async function up(db) {
await db.run("ALTER TABLE \"businesses\" ADD COLUMN \"absentemail\" INTEGER DEFAULT 1");
}

/**
* Runs the necessary SQL commands to migrate the database down to the previous version from this version.
* Automatically runs in a transaction with deferred foreign keys.
* @param {Database} db database instance to run SQL commands on
*/
export async function down(db) {
await db.run("ALTER TABLE \"businesses\" DROP COLUMN \"absentemail\"");
}
31 changes: 31 additions & 0 deletions migrations/0003_modify_businesses.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Automatically created by 'sqlite auto migrator (SAM)' on 2024-04-28T18:51:51.161Z

import { Database } from 'sqlite-auto-migrator';

// Pragmas can't be changed in transactions, so they are tracked separately.
// Note that most pragmas are not persisted in the database file and will have to be set on each new connection.
export const PRAGMAS = {"analysis_limit":0,"application_id":0,"auto_vacuum":0,"automatic_index":1,"timeout":1000,"cache_size":-2000,"cache_spill":483,"cell_size_check":0,"checkpoint_fullfsync":0,"seq":0,"name":"Records","compile_options":"ATOMIC_INTRINSICS=1","count_changes":0,"data_version":1,"file":"","defer_foreign_keys":0,"empty_result_callbacks":0,"encoding":"UTF-8","foreign_keys":0,"freelist_count":0,"full_column_names":0,"fullfsync":0,"builtin":1,"type":"table","enc":"utf8","narg":2,"flags":2099200,"hard_heap_limit":0,"ignore_check_constraints":0,"integrity_check":"ok","journal_mode":"delete","journal_size_limit":-1,"legacy_alter_table":0,"locking_mode":"normal","max_page_count":1073741823,"page_count":12,"page_size":4096,"query_only":0,"quick_check":"ok","read_uncommitted":0,"recursive_triggers":0,"reverse_unordered_selects":0,"schema_version":5,"secure_delete":0,"short_column_names":1,"soft_heap_limit":0,"synchronous":2,"schema":"main","ncol":5,"wr":0,"strict":0,"temp_store":0,"threads":0,"trusted_schema":1,"user_version":0,"wal_autocheckpoint":1000,"busy":0,"log":-1,"checkpointed":-1,"writable_schema":0};

/**
* Runs the necessary SQL commands to migrate the database up to this version from the previous version.
* Automatically runs in a transaction with deferred foreign keys.
* @param {Database} db database instance to run SQL commands on
*/
export async function up(db) {
await db.run("CREATE TABLE temp_c7r62mgn8zu(id INTEGER PRIMARY KEY,name TEXT NOT NULL,joincode TEXT NOT NULL UNIQUE,subscriptionId TEXT NOT NULL UNIQUE,requireJoin INTEGER DEFAULT 0 NOT NULL,absentEmail INTEGER DEFAULT 1 NOT NULL)");
await db.run("INSERT INTO temp_c7r62mgn8zu (\"id\", \"name\", \"joincode\", \"subscriptionid\", \"requirejoin\", \"absentemail\") SELECT \"id\", \"name\", \"joincode\", \"subscriptionid\", \"requirejoin\", \"absentemail\" FROM \"businesses\"");
await db.run("DROP TABLE \"businesses\"");
await db.run("ALTER TABLE temp_c7r62mgn8zu RENAME TO \"businesses\"");
}

/**
* Runs the necessary SQL commands to migrate the database down to the previous version from this version.
* Automatically runs in a transaction with deferred foreign keys.
* @param {Database} db database instance to run SQL commands on
*/
export async function down(db) {
await db.run("CREATE TABLE temp_c7r62mgn8zu(id INTEGER PRIMARY KEY,name TEXT NOT NULL,joincode TEXT NOT NULL UNIQUE,subscriptionId TEXT NOT NULL UNIQUE,requireJoin INTEGER DEFAULT 0 NOT NULL,absentemail INTEGER DEFAULT 1)");
await db.run("INSERT INTO temp_c7r62mgn8zu (\"id\", \"name\", \"joincode\", \"subscriptionid\", \"requirejoin\", \"absentemail\") SELECT \"id\", \"name\", \"joincode\", \"subscriptionid\", \"requirejoin\", \"absentemail\" FROM \"businesses\"");
await db.run("DROP TABLE \"businesses\"");
await db.run("ALTER TABLE temp_c7r62mgn8zu RENAME TO \"businesses\"");

This comment has been minimized.

Copy link
@SanderGi

SanderGi Apr 28, 2024

Collaborator

Don't forget to squash migrations when you make multiple schema changes in the same commit. I've done it for you in this commit: e7121aa. We want to keep the migrations as few as possible to make them easier to work with in case we need to (also fewer migrations is always going to be faster to execute). All you need to do is delete the migration files you've added and a new equivalent one will be created while npm run dev is active

}
25 changes: 25 additions & 0 deletions public/admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,19 @@ class GroupSettings extends Component {
<div class="tgl-btn" style="font-size: 16px; width: 4em; height: 2em"></div>
</div>
</label>
<label style="display: flex; justify-content: space-evenly;">
<span>
Send email if members mark themselves absent
<span style="position: relative;">
<i role="button" onclick="this.nextElementSibling.show(); event.preventDefault()" class="fa-solid fa-circle-info smaller-text"></i>
<dialog onblur="this.close()" class="tooltip-info" style="font-size: medium;">When enabled, members can send an email letting owners, admins, and moderators of a business know they'll be absent ahead of time.</dialog>
</span>
</span>
<div class="checkbox-wrapper-6">
<input id="absent-email" class="tgl tgl-light" type="checkbox" />
<div class="tgl-btn" style="font-size: 16px; width: 4em; height: 2em"></div>
</div>
</label>
<br><hr><br>
<a class="button" href="/payment.html">Manage Subscription &nbsp; <i class="fa-regular fa-money-bill-1"></i></a>
<a class="button" id="genericScannerLink" href="/scanner.html">Take Attendance &nbsp; <i class="icon-scanner"></i></a>
Expand Down Expand Up @@ -81,7 +94,11 @@ class GroupSettings extends Component {
const requireJoin = await GET(`/businesses/${getBusinessId()}/settings/requirejoin`).then(
res => res.json(),
);
const absentEmail = await GET(`/businesses/${getBusinessId()}/settings/absentemail`).then(
res => res.json(),
);
this.shadowRoot.getElementById('require-join').checked = requireJoin.requireJoin === 1;
this.shadowRoot.getElementById('absent-email').checked = absentEmail.absentEmail === 1;

// initialize email notification
this.shadowRoot.getElementById('email-notification').textContent = `
Expand Down Expand Up @@ -113,6 +130,14 @@ class GroupSettings extends Component {
}`,
);
};
const absentEmail = this.shadowRoot.getElementById('absent-email');
absentEmail.onchange = async () => {
await PUT(
`/businesses/${getBusinessId()}/settings/absentemail?new=${
absentEmail.checked ? 1 : 0
}`,
);
};
// send email notification
const emailNotification = this.shadowRoot.getElementById('email-notification');
this.shadowRoot.getElementById('sent-email').onclick = async () => {
Expand Down
68 changes: 34 additions & 34 deletions public/calendar.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { GET, PUT, sendGmail } from './util/Client.js';
import { requireLogin, getCurrentUser, requestGoogleCredential } from './util/Auth.js';
import { GET, PUT, sendEmail } from './util/Client.js';
import { requireLogin, getCurrentUser } from './util/Auth.js';
import { sanitizeText } from './util/util.js';
import { Popup } from './components/Popup.js';
const $ = window.$;
Expand Down Expand Up @@ -147,44 +147,44 @@ window.markAbsent = async (businessId, eventId) => {
await Popup.alert(await res.text(), 'var(--error)');
return;
}
await Popup.alert(
'You have been marked absent! Close this popup and click on your email account to send notifications to the event host(s).',
'var(--success)',
);
await Popup.alert('You have been marked absent!', 'var(--success)');

// Send email notification to business owner and admins
const credential = await requestGoogleCredential([
'https://www.googleapis.com/auth/gmail.send',
]);
let success = true;
const res1 = await GET(`/businesses/${businessId}/writemembers`);
const writeMembers = await res1.json();
console.log(writeMembers);
// Send email notification to business owner and admins if setting is enabled
const absentEmail = await GET(`/businesses/${businessId}/settings/absentemail`).then(res =>

This comment has been minimized.

Copy link
@SanderGi

SanderGi Apr 28, 2024

Collaborator

The user role does not have read privileges so they won't be able to check this setting. The email sending logic for absent notifications should all be moved to the PUT(/businesses/${businessId}/events/${eventId}/attendance/markabsent) endpoint (i.e. check the setting there and the write members and send the email there). Then you don't need the writemembers endpoint anymore as well (the fewer endpoints we have to maintain the better)

This comment has been minimized.

Copy link
@clr-li

clr-li Apr 29, 2024

Author Owner

How do I import the sendemail function in a server file? The sendEmail function calls /email so I could call that endpoint in markabsent but that would be repeating a bit of code

This comment has been minimized.

Copy link
@SanderGi

SanderGi Apr 29, 2024

Collaborator

You can factor out the fetch call in the /email endpoint function and call that on the server.

This comment has been minimized.

Copy link
@clr-li

clr-li Apr 29, 2024

Author Owner

I think I got that working but the status codes are giving me some issue. Also what about popups? I can't import those either

This comment has been minimized.

Copy link
@SanderGi

SanderGi Apr 29, 2024

Collaborator

You are not supposed to show any popups on the server side. Send back the necessary info in the response to the client that requested the markabsent endpoint so you can add code to act appropriately there (show popups etc.)

res.json(),
);
if (absentEmail.absentEmail === 1) {
let success = true;
const res1 = await GET(`/businesses/${businessId}/writemembers`);
const writeMembers = await res1.json();

for (const member of writeMembers) {
const res = await sendGmail(
member.email,
'Attendance Scanner Notification of Absence',
'Hi ' +
member.name +
',\n\n' +
user.name +
' has marked themselves absent from the event.\n\n(automatically sent via Attendance Scanner QR)',
credential,
);
if (!res.ok) {
success = false;
const obj = await res.json();
const message = obj.error.message;
for (const member of writeMembers) {
const res = await sendEmail(
member.email,
'Attendance Scanner Notification of Absence',
'Hi ' +
member.name +
',\n\n' +
user.name +
' has marked themselves absent from the event.\n\n(automatically sent via Attendance Scanner QR)',
);
if (!res.ok) {
success = false;
const obj = await res.json();
const message = obj.error.message;
Popup.alert(
`Email to ${sanitizeText(member[1])} failed to send. ` + message,
'var(--error)',
);
}
}
if (success) {
Popup.alert(
`Email to ${sanitizeText(member[1])} failed to send. ` + message,
'var(--error)',
'An email has been sent, notifying event host(s) of your absence',
'var(--success)',
);
}
}
if (success) {
Popup.alert('Emails sent successfully!', 'var(--success)');
}

// manually change html since evo-calendar is broken when adding/removing or updating events
const badge = document.createElement('span');
Expand Down
11 changes: 0 additions & 11 deletions public/components/DataTable.js
Original file line number Diff line number Diff line change
Expand Up @@ -352,17 +352,6 @@ export class DataTable extends Component {
}
}
};

// let initialRows = this.rows.filter(row =>
// row[header].toLowerCase().includes(search.value.toLowerCase()),
// );
// search.oninput = () => {
// const searchValue = search.value;
// this.rows = initialRows.filter(row =>
// row[header].toLowerCase().includes(searchValue.toLowerCase()),
// );
// this.showPage(1);
// };
}

/**
Expand Down
11 changes: 3 additions & 8 deletions public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -165,27 +165,22 @@ <h3 style="margin-bottom: 3px">Free Plan</h3>
<hr />
<ul style="text-align: center; max-width: 80%; margin: auto">
<li>Fixed Monthly Price: $0.00</li>
<li>Up to 50 members</li>
<!-- <li>Up to 150 members</li> -->
<li>Unlimited role and user privilege management</li>
<li>
<a href="https://github.com/clr-li/AttendanceScanner/issues"
>Community support</a
>
</li>
<li>Export/import custom data via CSV</li>
<li>Gmail invites and notifications</li>
<li>Email invites and notifications</li>
</ul>
</div>
<div id="standard" class="subscription">
<h3 style="margin-bottom: 3px">Standard Plan</h3>
<hr />
<ul style="text-align: center; max-width: 80%; margin: auto">
<li>Fixed Monthly Price: $7.00</li>
<li>Up to 100 members</li>
<li>Unlimited role and user privilege management</li>
<li>Email+Community support</li>
<li>API+Google Sheets integration</li>
<li>Gmail+Email invites and notifications</li>
<li>Coming Soon</li>
</ul>
</div>
</section>
Expand Down
2 changes: 1 addition & 1 deletion public/payment.html
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ <h3 style="margin-bottom: 3px">Free Plan</h3>
>
</li>
<li>Export/import custom data via CSV</li>
<li>Gmail invites and notifications</li>
<li>Email invites and notifications</li>
</ul>
</div>
<div id="standard" class="subscription">
Expand Down
39 changes: 38 additions & 1 deletion server/Business.js
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,43 @@ router.put('/businesses/:businessId/settings/requirejoin', async (request, respo
response.sendStatus(changes === 1 ? 200 : 400);
});

/**
* Gets the absentEmail setting for the specified business.
* @pathParams businessId - id of the business to get the role in
* @requiredPrivileges read access to the specified business
* @response json object with the absentEmail setting of the specified business
*/
router.get('/businesses/:businessId/settings/absentemail', async (request, response) => {
const uid = await handleAuth(request, response, request.params.businessId, { read: true });
if (!uid) return;

const businessId = request.params.businessId;

const absentEmail = await db().get(
...SQL`SELECT absentEmail FROM Businesses WHERE id = ${businessId}`,
);
response.send(absentEmail);
});

/**
* Updates the absentEmail setting for the specified business.
* @pathParams businessId - id of the business to update the role in
* @queryParams new - new absentEmail setting for the business
* @requiredPrivileges write access to the specified business
*/
router.put('/businesses/:businessId/settings/absentemail', async (request, response) => {
const uid = await handleAuth(request, response, request.params.businessId, { write: true });
if (!uid) return;

const businessId = request.params.businessId;
const newAbsentEmail = request.query.new;

const { changes } = await db().run(
...SQL`UPDATE Businesses SET absentEmail = ${newAbsentEmail} WHERE id = ${businessId}`,
);
response.sendStatus(changes === 1 ? 200 : 400);
});

/**
* Sets the custom data for a user in the specified business.
* @pathParams businessId - id of the business to set custom data in
Expand Down Expand Up @@ -407,7 +444,7 @@ router.get('/businesses/:businessId/writemembers', async (request, response) =>
SELECT Users.name, Users.email
FROM Members INNER JOIN Users on Members.user_id = Users.id
WHERE Members.business_id = ${businessId}
AND Members.role = 'owner' OR Members.role = 'admin' OR Members.role = 'moderator'`,
AND (Members.role = 'owner' OR Members.role = 'admin' OR Members.role = 'moderator')`,
);
response.send(rows);
});
Expand Down
3 changes: 2 additions & 1 deletion server/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ CREATE TABLE IF NOT EXISTS "Businesses" (
"name" TEXT NOT NULL,
"joincode" TEXT NOT NULL UNIQUE,
"subscriptionId" TEXT NOT NULL UNIQUE,
"requireJoin" INTEGER DEFAULT 0 NOT NULL
"requireJoin" INTEGER DEFAULT 0 NOT NULL,
"absentEmail" INTEGER DEFAULT 1 NOT NULL
);
CREATE TABLE IF NOT EXISTS "Events" (
"id" INTEGER PRIMARY KEY,
Expand Down
32 changes: 32 additions & 0 deletions server/super_admin/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,38 @@
"editview": {
"show": true
}
},
{
"name": "absentemail",
"verbose": "absentemail",
"control": {
"text": true
},
"type": "INTEGER",
"allowNull": true,
"defaultValue": "1",
"listview": {
"show": true
},
"editview": {
"show": true
}
},
{
"name": "absentEmail",
"verbose": "absentEmail",
"control": {
"text": true
},
"type": "INTEGER",
"allowNull": false,
"defaultValue": "1",
"listview": {
"show": true
},
"editview": {
"show": true
}
}
],
"mainview": {
Expand Down

0 comments on commit 059b3e3

Please sign in to comment.