src/templates/people/passwd.html \
src/templates/people/search.html \
src/templates/people/sip.html \
+ src/templates/people/stats.html \
src/templates/people/subscribe.html \
src/templates/people/subscribed.html \
src/templates/people/unsubscribe.html \
import datetime
import hashlib
import hmac
+import iso3166
import json
import ldap
import ldap.modlist
def init(self):
self.search_base = self.settings.get("ldap_search_base")
+ def __len__(self):
+ count = self.memcache.get("accounts:count")
+
+ if count is None:
+ count = self._count("(objectClass=person)")
+
+ self.memcache.set("accounts:count", count, 300)
+
+ return count
+
def __iter__(self):
# Only return developers (group with ID 1000)
accounts = self._search("(&(objectClass=posixAccount)(gidNumber=1000))")
return results
+ def _count(self, query):
+ res = self._query(query, attrlist=["dn"])
+
+ return len(res)
+
def _search(self, query, attrlist=None, limit=0):
accounts = []
for dn, attrs in self._query(query, attrlist=["dn"], limit=limit):
return h.hexdigest()
+ @property
+ def countries(self):
+ ret = {}
+
+ for country in iso3166.countries:
+ count = self._count("(&(objectClass=person)(st=%s))" % country.alpha2)
+
+ if count:
+ ret[country] = count
+
+ return ret
+
class Account(LDAPObject):
def __str__(self):
{{ _("Groups") }}
</a>
</li>
+
+ <li class="nav-item">
+ <a class="nav-link {% if request.path == "/stats" %}active{% end %}" href="/stats">
+ {{ _("Stats") }}
+ </a>
+ </li>
{% end %}
{% if current_user.has_sip() %}
--- /dev/null
+{% extends "../base.html" %}
+
+{% block title %}{{ _("Statistics") }}{% end block %}
+
+{% block content %}
+ <h1>{{ _("Statistics") }}</h1>
+
+ <div class="row">
+ <div class="col-12 col-lg-3">
+ <div class="card">
+ <div class="card-body text-center">
+ <h1>{{ len(backend.accounts) }}</h1>
+ <h5 class="mb-0">{{ _("Total Accounts") }}</h5>
+ </div>
+ </div>
+ </div>
+
+ <div class="col-12 col-lg-9">
+ <div class="card">
+ <div class="card-body">
+ <h4 class="mb-0">{{ _("By Country") }}</h4>
+ </div>
+
+ <ul class="list-group list-group-flush">
+ {% set countries = backend.accounts.countries %}
+
+ {% for country in sorted(countries, key=lambda c: countries[c], reverse=True) %}
+ <li class="list-group-item d-flex justify-content-between align-items-center">
+ <span>{{ country.apolitical_name }}</span>
+ <span>{{ countries[country] }}</span>
+ </li>
+ {% end %}
+ </ul>
+ </div>
+ </div>
+ </div>
+{% end block %}
(r"/password\-reset", auth.PasswordResetInitiationHandler),
(r"/password\-reset/([a-z_][a-z0-9_-]{0,31})/(\w+)", auth.PasswordResetHandler),
+ # Stats
+ (r"/stats", people.StatsHandler),
+
# API
(r"/api/check/uid", auth.APICheckUID),
] + authentication_handlers)
self.render("people/search.html", q=q, accounts=accounts)
+class StatsHandler(auth.CacheMixin, base.BaseHandler):
+ @tornado.web.authenticated
+ def get(self):
+ self.render("people/stats.html")
+
+
class SubscribeHandler(auth.CacheMixin, base.BaseHandler):
@tornado.web.authenticated
def get(self):