Content-Length: 611105 | pFad | http://github.com/internetarchive/openlibrary/commit/76304ecdb3a5954fcf13feb710e8c40fcf24b73c

5F Merge pull request #7279 from jimchamp/7219/feature/yearly-reading-go… · internetarchive/openlibrary@76304ec · GitHub
Skip to content

Commit

Permalink
Merge pull request #7279 from jimchamp/7219/feature/yearly-reading-go…
Browse files Browse the repository at this point in the history
…al-form

Add yearly reading goal form and progress UI
  • Loading branch information
mekarpeles authored Dec 23, 2022
2 parents 5281079 + e0d3425 commit 76304ec
Show file tree
Hide file tree
Showing 13 changed files with 500 additions and 40 deletions.
41 changes: 40 additions & 1 deletion openlibrary/core/bookshelves_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,46 @@ def select_by_book_user_and_type(cls, username, work_id, edition_id, event_type)
event_type=$event_type
"""

return list(db.select(cls.TABLENAME, where=where, vars=data))
return list(oldb.select(cls.TABLENAME, where=where, vars=data))

@classmethod
def select_by_user_type_and_year(cls, username, event_type, year):
oldb = db.get_db()

data = {
'username': username,
'event_type': event_type,
'event_date': f'{year}%',
}

where = """
username=$username AND
event_type=$event_type AND
event_date LIKE $event_date
"""

return list(oldb.select(cls.TABLENAME, where=where, vars=data))

@classmethod
def select_distinct_by_user_type_and_year(cls, username, event_type, year):
"""Returns a list of the most recent check-in events, with no repeating
work IDs. Useful for calculating one's yearly reading goal progress.
"""
oldb = db.get_db()

data = {
'username': username,
'event_type': event_type,
'event_date': f'{year}%',
}
query = (
f"select distinct on (work_id) work_id, * from {cls.TABLENAME} "
"where username=$username and event_type=$event_type and "
"event_date LIKE $event_date "
"order by work_id, updated desc"
)

return list(oldb.query(query, vars=data))

@classmethod
def exists(cls, pid) -> bool:
Expand Down
1 change: 0 additions & 1 deletion openlibrary/core/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,6 @@ CREATE TABLE yearly_reading_goals (
username text not null,
year integer not null,
target integer not null,
current integer default 0,
created timestamp without time zone default (current_timestamp at time zone 'utc'),
updated timestamp without time zone default (current_timestamp at time zone 'utc'),
primary key (username, year)
Expand Down
14 changes: 12 additions & 2 deletions openlibrary/core/yearly_reading_goals.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,21 @@ class YearlyReadingGoals:
def create(cls, username: str, year: int, target: int):
oldb = db.get_db()

# TODO: Fetch number of books already read this year

return oldb.insert(cls.TABLENAME, username=username, year=year, target=target)

# Read methods:
@classmethod
def select_by_username(cls, username: str):
oldb = db.get_db()

where = 'username=$username'
order = 'year ASC'
data = {
'username': username,
}

return list(oldb.select(cls.TABLENAME, where=where, order=order, vars=data))

@classmethod
def select_by_username_and_year(cls, username: str, year: int):
oldb = db.get_db()
Expand Down
165 changes: 165 additions & 0 deletions openlibrary/plugins/openlibrary/js/check-ins/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* @module check-ins/index
*/
import { PersistentToast } from '../Toast'
import { initDialogs } from '../native-dialog'

/**
* Enum for check-in event types.
Expand Down Expand Up @@ -387,3 +388,167 @@ function deleteEvent(rootElem, workOlid, eventId) {
}
});
}

/**
* Adds listener to open reading goal modal, and updates year to
* local year.
*
* @param {HTMLElement} link Prompt for adding a reading goal
*/
export function initYearlyGoalPrompt(link) {
const yearlyGoalModal = document.querySelector('#yearly-goal-modal')

setLocalYear(yearlyGoalModal.querySelector('form'))

link.addEventListener('click', function() {
yearlyGoalModal.showModal()
})
}

/**
* Updates year to the client's local year.
*
* Used to prevent issues on New Year's day. Sets
* the year input in the reading log form, and updates
* the year in the "set goal" CTA link.
*
* @param {HTMLFormElement} form Reading goal form
*/
function setLocalYear(form) {
const currentYearSpan = document.querySelector('#reading-goal-current-year')
const currentYear = new Date().getFullYear();
currentYearSpan.textContent = currentYear

form.querySelector('input[name=year]').value = currentYear
}

/**
* Adds click listeners to the given edit goal links.
*
* @param {HTMLCollection<HTMLElement>} editLinks Edit goal links
*/
export function initGoalEditLinks(editLinks) {
for (const link of editLinks) {
const parent = link.closest('.reading-goal-progress')
const modal = parent.querySelector('dialog')
addGoalEditClickListener(link, modal)
}
}

/**
* Adds click listener to the given edit link.
*
* Given modal will be displayed when the edit link
* is clicked.
* @param {HTMLElement} editLink An edit goal link
* @param {HTMLDialogElement} modal The modal that will be shown
*/
function addGoalEditClickListener(editLink, modal) {
editLink.addEventListener('click', function() {
modal.showModal()
})
}

/**
* Adds click listeners to given collection of goal submission
* buttons.
*
* @param {HTMLCollection<HTMLElement>} submitButtons Submit goal buttons
*/
export function initGoalSubmitButtons(submitButtons) {
for (const button of submitButtons) {
addGoalSubmissionListener(button)
}
}

/**
* Adds click listener to given reading goal form submission button.
*
* On click, POSTs form to server. Updates view depending on whether
* the action set a new goal, or updated an existing goal.
* @param {HTMLELement} submitButton Reading goal form submit button
*/
function addGoalSubmissionListener(submitButton) {
submitButton.addEventListener('click', function(event) {
event.preventDefault()

const form = submitButton.closest('form')
const formData = new FormData(form)

fetch(form.action, {
method: 'POST',
headers: {
'content-type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams(formData)
})
.then((response) => {
if (!response.ok) {
throw new Error('Failed to set reading goal')
}
const modal = form.closest('dialog')
if (modal) {
modal.close()
}

if (formData.get('is_update')) {
const progressComponent = modal.closest('.reading-goal-progress')
updateProgressComponent(progressComponent, Number(formData.get('goal')))
} else {
const chipGroup = modal.closest('.chip-group')
updateChipGroup(chipGroup)
}
})
})
}

/**
* Updates given reading goal progress component with a new
* goal.
*
* @param {HTMLElement} elem A reading goal progress component
* @param {Number} goal The new reading goal
*/
function updateProgressComponent(elem, goal) {
// Calculate new percentage:
const booksReadSpan = elem.querySelector('.reading-goal-progress__books-read')
const booksRead = Number(booksReadSpan.textContent)
const percentComplete = Math.floor((booksRead / goal) * 100)

// Update view:
const goalSpan = elem.querySelector('.reading-goal-progress__goal')
const percentageSpan = elem.querySelector('.reading-goal-progress__percentage')
const completedBar = elem.querySelector('.reading-goal-progress__completed')
goalSpan.textContent = goal
percentageSpan.textContent = `(${percentComplete}%)`
completedBar.style.width = `${Math.min(100, percentComplete)}%`
}

/**
* Replaces "Set reading goal" chip on My Books page with reading goal
* progress component.
*
* @param {HTMLElement} chipGroup Parent container of "set goal" CTA link
*/
function updateChipGroup(chipGroup) {
fetch('/reading-goal/partials')
.then((response) => {
if (!response.ok) {
throw new Error('Failed to fetch progress element')
}
return response.text()
})
.then(function(html) {
const progress = document.createElement('SPAN')
progress.innerHTML = html
chipGroup.parentElement.insertBefore(progress, chipGroup)
chipGroup.remove()

const progressEditLink = progress.querySelector('.edit-reading-goal-link')
const updateModal = progress.querySelector('dialog')
initDialogs([updateModal])
addGoalEditClickListener(progressEditLink, updateModal)
const submitButton = updateModal.querySelector('.reading-goal-submit-button')
addGoalSubmissionListener(submitButton)
})
}
14 changes: 13 additions & 1 deletion openlibrary/plugins/openlibrary/js/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -383,12 +383,24 @@ jQuery(function () {
import(/* webpackChunkName: "dialog" */ './native-dialog')
.then(module => module.initDialogs(nativeDialogs))
}
const setGoalLink = document.querySelector('#set-reading-goal-link')
const goalEditLinks = document.querySelectorAll('.edit-reading-goal-link')
const goalSubmitButtons = document.querySelectorAll('.reading-goal-submit-button')
const checkInForms = document.querySelectorAll('.check-in')
const checkInPrompts = document.querySelectorAll('.check-in-prompt')
const checkInEditLinks = document.querySelectorAll('.prompt-edit-date')
if (checkInForms.length || checkInPrompts.length || checkInEditLinks.length) {
if (setGoalLink || goalEditLinks.length || goalSubmitButtons.length || checkInForms.length || checkInPrompts.length || checkInEditLinks.length) {
import(/* webpackChunkName: "check-ins" */ './check-ins')
.then((module) => {
if (setGoalLink) {
module.initYearlyGoalPrompt(setGoalLink)
}
if (goalEditLinks.length) {
module.initGoalEditLinks(goalEditLinks)
}
if (goalSubmitButtons.length) {
module.initGoalSubmitButtons(goalSubmitButtons)
}
if (checkInForms.length) {
module.initCheckInForms(checkInForms)
}
Expand Down
98 changes: 98 additions & 0 deletions openlibrary/plugins/upstream/checkins.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,16 @@
import json
import web

from datetime import datetime
from math import floor
from typing import Optional

from infogami.utils import delegate
from infogami.utils.view import public

from openlibrary.accounts import get_current_user
from openlibrary.app import render_template
from openlibrary.core.yearly_reading_goals import YearlyReadingGoals
from openlibrary.utils import extract_numeric_id_from_olid
from openlibrary.core.bookshelves_events import BookshelfEvent, BookshelvesEvents
from openlibrary.utils.decorators import authorized_for
Expand Down Expand Up @@ -144,5 +148,99 @@ def DELETE(self, check_in_id):
return web.ok()


class yearly_reading_goal_json(delegate.page):
path = '/reading-goal'
encoding = 'json'

@authorized_for('/usergroup/beta-testers')
def GET(self):
i = web.input(year=None)

user = get_current_user()
username = user['key'].split('/')[-1]

if i.year:
results = [
{'year': i.year, 'goal': record.target, 'progress': record.current}
for record in YearlyReadingGoals.select_by_username_and_year(
username, i.year
)
]
else:
results = [
{'year': record.year, 'goal': record.target, 'progress': record.current}
for record in YearlyReadingGoals.select_by_username(username)
]

return delegate.RawText(json.dumps({'status': 'ok', 'goal': results}))

@authorized_for('/usergroup/beta-testers')
def POST(self):
i = web.input(goal=0, year=None, is_update=None)

goal = int(i.goal)

if not goal or goal < 0:
raise web.badrequest('Reading goal must be a positive integer')

if i.is_update and not i.year:
raise web.badrequest('Year required to update reading goals')

user = get_current_user()
username = user['key'].split('/')[-1]

current_year = i.year or datetime.now().year

if i.is_update:
YearlyReadingGoals.update_target(username, i.year, goal)
else:
YearlyReadingGoals.create(username, current_year, goal)

return delegate.RawText(json.dumps({'status': 'ok'}))


@public
def get_reading_goals(year=None):
user = get_current_user()
username = user['key'].split('/')[-1]

if not year:
year = datetime.now().year

if not (data := YearlyReadingGoals.select_by_username_and_year(username, year)):
return None

books_read = BookshelvesEvents.select_distinct_by_user_type_and_year(
username, BookshelfEvent.FINISH, year
)
read_count = len(books_read)
result = YearlyGoal(data[0].year, data[0].target, read_count)

return result


class YearlyGoal:
def __init__(self, year, goal, books_read):
self.year = year
self.goal = goal
self.books_read = books_read
self.progress = floor((books_read / goal) * 100)

@classmethod
def calc_progress(cls, books_read, goal):
return floor((books_read / goal) * 100)


class ui_partials(delegate.page):
path = '/reading-goal/partials'

def GET(self):
i = web.input(year=None)
year = i.year or datetime.now().year
goal = get_reading_goals(year=year)
component = render_template('check_ins/reading_goal_progress', [goal])
return delegate.RawText(component)


def setup():
pass
Loading

0 comments on commit 76304ec

Please sign in to comment.








ApplySandwichStrip

pFad - (p)hone/(F)rame/(a)nonymizer/(d)eclutterfier!      Saves Data!


--- a PPN by Garber Painting Akron. With Image Size Reduction included!

Fetched URL: http://github.com/internetarchive/openlibrary/commit/76304ecdb3a5954fcf13feb710e8c40fcf24b73c

Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy