Skip to content

Commit 9f0cc79

Browse files
authored
Merge pull request #11697 from jimchamp/login-stats
Add unique login stats to `/stats` page
2 parents c13b5be + b1d8f5f commit 9f0cc79

File tree

6 files changed

+112
-1
lines changed

6 files changed

+112
-1
lines changed

openlibrary/core/admin.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
from infogami import config
1010
from openlibrary.core import cache
1111

12+
from . import db
13+
1214

1315
class Stats:
1416
def __init__(self, docs, key, total_key):
@@ -175,6 +177,39 @@ def get_stats(ndays=30, use_mock_data=False):
175177
}
176178

177179

180+
def get_unique_logins_since(since_days=30):
181+
since_date = datetime.datetime.now() - datetime.timedelta(days=since_days)
182+
date_str = since_date.strftime("%Y-%m-%d")
183+
184+
query = """
185+
SELECT COUNT(id) FROM store_index
186+
WHERE type = 'account'
187+
AND name = 'last_login'
188+
AND value > $date
189+
"""
190+
191+
oldb = db.get_db()
192+
results = list(oldb.query(query, vars={"date": date_str}))
193+
194+
if not results:
195+
return 0
196+
return results[0].get('count', 0)
197+
198+
199+
def get_cached_unique_logins_since(since_days=30):
200+
from openlibrary.plugins.openlibrary.home import caching_prethread
201+
202+
twelve_hours = 60 * 60 * 12
203+
key_prefix = 'logins_since'
204+
mc = cache.memcache_memoize(
205+
get_unique_logins_since,
206+
key_prefix=key_prefix,
207+
timeout=twelve_hours,
208+
prethread=caching_prethread(),
209+
)
210+
return mc(since_days=since_days)
211+
212+
178213
def mock_get_stats():
179214
keyNames = [
180215
"human_edits",

openlibrary/i18n/messages.pot

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3478,6 +3478,12 @@ msgstr ""
34783478
msgid "Items"
34793479
msgstr ""
34803480

3481+
#: admin/index.html
3482+
msgid ""
3483+
"<span class=\"login-counts\">0</span> patrons have logged in at least "
3484+
"once over the past 30 days."
3485+
msgstr ""
3486+
34813487
#: admin/index.html
34823488
msgid "Stats"
34833489
msgstr ""
@@ -3614,6 +3620,14 @@ msgstr ""
36143620
msgid "Borrows, last 2 years graph"
36153621
msgstr ""
36163622

3623+
#: admin/index.html
3624+
msgid "Unique Logins"
3625+
msgstr ""
3626+
3627+
#: admin/index.html
3628+
msgid "Gathering login statistics..."
3629+
msgstr ""
3630+
36173631
#: admin/loans_table.html
36183632
msgid "BookReader"
36193633
msgstr ""

openlibrary/plugins/openlibrary/api.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
)
2626
from openlibrary.core import cache, lending, models
2727
from openlibrary.core import helpers as h
28+
from openlibrary.core.admin import get_cached_unique_logins_since
2829
from openlibrary.core.auth import ExpiredTokenError, HMACToken
2930
from openlibrary.core.bestbook import Bestbook
3031
from openlibrary.core.bookshelves_events import BookshelvesEvents
@@ -1061,3 +1062,14 @@ def make_dark(self, edition):
10611062
if not data['source_records']:
10621063
del data['source_records']
10631064
web.ctx.site.save(data, 'Remove OCAID: Item no longer available to borrow.')
1065+
1066+
1067+
class monthly_logins(delegate.page):
1068+
path = "/api/monthly_logins"
1069+
encoding = "json"
1070+
1071+
def GET(self):
1072+
return delegate.RawText(
1073+
json.dumps({"loginCount": get_cached_unique_logins_since()}),
1074+
content_type="application/json",
1075+
)

openlibrary/plugins/openlibrary/js/index.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -582,4 +582,11 @@ jQuery(function () {
582582
import(/* webpackChunkName: "list-books" */ './list_books')
583583
.then(module => module.ListBooks.init());
584584
}
585+
586+
// Stats page login counts
587+
const monthlyLoginStats = document.querySelector('.monthly-login-counts')
588+
if (monthlyLoginStats) {
589+
import(/* webpackChunkName: "stats" */ './stats')
590+
.then(module => module.initUniqueLoginCounts(monthlyLoginStats))
591+
}
585592
});
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/**
2+
* Fetches unique login counts and updates the view with the results.
3+
*
4+
* @param {HTMLElement} containerElem
5+
* @returns {Promise<void>}
6+
* @see /openlibrary/templates/admin/index.html
7+
*/
8+
export async function initUniqueLoginCounts(containerElem) {
9+
const loadingIndicator = containerElem.querySelector('.loadingIndicator')
10+
const i18nStrings = JSON.parse(containerElem.dataset.i18n)
11+
12+
const counts = await fetchCounts()
13+
.then((resp) => {
14+
if (resp.status !== 200) {
15+
throw new Error(`Failed to fetch partials. Status code: ${resp.status}`)
16+
}
17+
return resp.json()
18+
})
19+
20+
const countDiv = document.createElement('DIV')
21+
countDiv.innerHTML = i18nStrings.uniqueLoginsCopy
22+
const countSpan = countDiv.querySelector('.login-counts')
23+
countSpan.textContent = counts.loginCount
24+
loadingIndicator.replaceWith(countDiv)
25+
}
26+
27+
/**
28+
* Fetches login counts from the server.
29+
*
30+
* @returns {Promise<Response>}
31+
* @see `monthly_logins` class in /openlibrary/plugins/openlibrary/api.py
32+
*/
33+
async function fetchCounts() {
34+
return fetch('/api/monthly_logins.json')
35+
}

openlibrary/templates/admin/index.html

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@
44
$ _x = ctx.setdefault('usergroup', 'admin')
55

66
$ stats = get_admin_stats()
7+
8+
$ i18n_strings = {
9+
$ "uniqueLoginsCopy": _('<span class="login-counts">0</span> patrons have logged in at least once over the past 30 days.')
10+
$ }
11+
712
<script type="text/json+graph" id="graph-json-editgraph">$:json_encode(counts.human_edits.get_counts(80, True))</script>
813
<script type="text/json+graph" id="graph-json-membergraph">$:json_encode(counts.members.get_counts(80, True))</script>
914

@@ -248,5 +253,8 @@ <h2>$_("Borrows")</h2>
248253
<img alt="$_('Borrows, last 2 years graph')" src="https://graphite.us.archive.org/render?target=hitcount(stats.ol.loans.bookreader,%221d%22)&from=-24months&tz=UTC&width=900"/>
249254

250255
<div class="contentSpacer"></div>
251-
256+
<h2>$_("Unique Logins")</h2>
257+
<div class="monthly-login-counts" data-i18n="$json_encode(i18n_strings)">
258+
$:macros.LoadingIndicator(_("Gathering login statistics..."), hidden=False)
259+
</div>
252260
</div>

0 commit comments

Comments
 (0)