Merge pull request #1100 from umap-project/stats-view

Add a very basic `/stats/` JSON view
This commit is contained in:
David Larlet 2023-05-23 13:05:11 -04:00 committed by GitHub
commit 7f85684d52
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 96 additions and 42 deletions

View file

@ -2,6 +2,7 @@ import shutil
import tempfile import tempfile
import pytest import pytest
from django.core.cache import cache
from django.core.signing import get_cookie_signer from django.core.signing import get_cookie_signer
from .base import DataLayerFactory, MapFactory, UserFactory from .base import DataLayerFactory, MapFactory, UserFactory
@ -12,6 +13,7 @@ TMP_ROOT = tempfile.mkdtemp()
def pytest_configure(config): def pytest_configure(config):
from django.conf import settings from django.conf import settings
settings.MEDIA_ROOT = TMP_ROOT settings.MEDIA_ROOT = TMP_ROOT
@ -19,11 +21,20 @@ def pytest_unconfigure(config):
shutil.rmtree(TMP_ROOT, ignore_errors=True) shutil.rmtree(TMP_ROOT, ignore_errors=True)
def pytest_runtest_teardown():
cache.clear()
@pytest.fixture @pytest.fixture
def user(): def user():
return UserFactory(password="123123") return UserFactory(password="123123")
@pytest.fixture
def user2():
return UserFactory(username="Averell", password="456456")
@pytest.fixture @pytest.fixture
def licence(): def licence():
# Should be created by the migrations. # Should be created by the migrations.

View file

@ -1,4 +1,6 @@
import json
import socket import socket
from datetime import date, timedelta
import pytest import pytest
from django.conf import settings from django.conf import settings
@ -11,12 +13,12 @@ from umap.views import validate_url
def get(target="http://osm.org/georss.xml", verb="get", **kwargs): def get(target="http://osm.org/georss.xml", verb="get", **kwargs):
defaults = { defaults = {
'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest', "HTTP_X_REQUESTED_WITH": "XMLHttpRequest",
'HTTP_REFERER': '%s/path/' % settings.SITE_URL "HTTP_REFERER": "%s/path/" % settings.SITE_URL,
} }
defaults.update(kwargs) defaults.update(kwargs)
func = getattr(RequestFactory(**defaults), verb) func = getattr(RequestFactory(**defaults), verb)
return func('/', {'url': target}) return func("/", {"url": target})
def test_good_request_passes(): def test_good_request_passes():
@ -70,67 +72,93 @@ def test_unkown_domain_raises():
def test_valid_proxy_request(client): def test_valid_proxy_request(client):
url = reverse('ajax-proxy') url = reverse("ajax-proxy")
params = {'url': 'http://example.org'} params = {"url": "http://example.org"}
headers = { headers = {
'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest', "HTTP_X_REQUESTED_WITH": "XMLHttpRequest",
'HTTP_REFERER': settings.SITE_URL "HTTP_REFERER": settings.SITE_URL,
} }
response = client.get(url, params, **headers) response = client.get(url, params, **headers)
assert response.status_code == 200 assert response.status_code == 200
assert 'Example Domain' in response.content.decode() assert "Example Domain" in response.content.decode()
assert 'Cookie' not in response['Vary'] assert "Cookie" not in response["Vary"]
def test_valid_proxy_request_with_ttl(client): def test_valid_proxy_request_with_ttl(client):
url = reverse('ajax-proxy') url = reverse("ajax-proxy")
params = {'url': 'http://example.org', 'ttl': 3600} params = {"url": "http://example.org", "ttl": 3600}
headers = { headers = {
'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest', "HTTP_X_REQUESTED_WITH": "XMLHttpRequest",
'HTTP_REFERER': settings.SITE_URL "HTTP_REFERER": settings.SITE_URL,
} }
response = client.get(url, params, **headers) response = client.get(url, params, **headers)
assert response.status_code == 200 assert response.status_code == 200
assert 'Example Domain' in response.content.decode() assert "Example Domain" in response.content.decode()
assert 'Cookie' not in response['Vary'] assert "Cookie" not in response["Vary"]
assert response['X-Accel-Expires'] == '3600' assert response["X-Accel-Expires"] == "3600"
def test_valid_proxy_request_with_invalid_ttl(client): def test_valid_proxy_request_with_invalid_ttl(client):
url = reverse('ajax-proxy') url = reverse("ajax-proxy")
params = {'url': 'http://example.org', 'ttl': 'invalid'} params = {"url": "http://example.org", "ttl": "invalid"}
headers = { headers = {
'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest', "HTTP_X_REQUESTED_WITH": "XMLHttpRequest",
'HTTP_REFERER': settings.SITE_URL "HTTP_REFERER": settings.SITE_URL,
} }
response = client.get(url, params, **headers) response = client.get(url, params, **headers)
assert response.status_code == 200 assert response.status_code == 200
assert 'Example Domain' in response.content.decode() assert "Example Domain" in response.content.decode()
assert 'Cookie' not in response['Vary'] assert "Cookie" not in response["Vary"]
assert 'X-Accel-Expires' not in response assert "X-Accel-Expires" not in response
@pytest.mark.django_db @pytest.mark.django_db
def test_login_does_not_contain_form_if_not_enabled(client, settings): def test_login_does_not_contain_form_if_not_enabled(client, settings):
settings.ENABLE_ACCOUNT_LOGIN = False settings.ENABLE_ACCOUNT_LOGIN = False
response = client.get(reverse('login')) response = client.get(reverse("login"))
assert 'username' not in response.content.decode() assert "username" not in response.content.decode()
@pytest.mark.django_db @pytest.mark.django_db
def test_login_contains_form_if_enabled(client, settings): def test_login_contains_form_if_enabled(client, settings):
settings.ENABLE_ACCOUNT_LOGIN = True settings.ENABLE_ACCOUNT_LOGIN = True
response = client.get(reverse('login')) response = client.get(reverse("login"))
assert 'username' in response.content.decode() assert "username" in response.content.decode()
@pytest.mark.django_db @pytest.mark.django_db
def test_can_login_with_username_and_password_if_enabled(client, settings): def test_can_login_with_username_and_password_if_enabled(client, settings):
settings.ENABLE_ACCOUNT_LOGIN = True settings.ENABLE_ACCOUNT_LOGIN = True
User = get_user_model() User = get_user_model()
user = User.objects.create(username='test') user = User.objects.create(username="test")
user.set_password('test') user.set_password("test")
user.save() user.save()
client.post(reverse('login'), {'username': 'test', 'password': 'test'}) client.post(reverse("login"), {"username": "test", "password": "test"})
user = get_user(client) user = get_user(client)
assert user.is_authenticated assert user.is_authenticated
@pytest.mark.django_db
def test_stats_empty(client):
response = client.get(reverse("stats"))
assert json.loads(response.content.decode()) == {
"maps_active_last_week_count": 0,
"maps_count": 0,
"users_active_last_week_count": 0,
"users_count": 0,
}
@pytest.mark.django_db
def test_stats_basic(client, map, datalayer, user2):
map.owner.last_login = date.today()
map.owner.save()
user2.last_login = date.today() - timedelta(days=8)
user2.save()
response = client.get(reverse("stats"))
assert json.loads(response.content.decode()) == {
"maps_active_last_week_count": 1,
"maps_count": 1,
"users_active_last_week_count": 1,
"users_count": 2,
}

View file

@ -1,5 +1,5 @@
from django.conf import settings from django.conf import settings
from django.urls import include, re_path from django.urls import include, path, re_path
from django.conf.urls.i18n import i18n_patterns from django.conf.urls.i18n import i18n_patterns
from django.conf.urls.static import static from django.conf.urls.static import static
from django.contrib import admin from django.contrib import admin
@ -41,9 +41,7 @@ urlpatterns = [
] ]
i18n_urls = [ i18n_urls = [
re_path( re_path(r"^login/$", jsonize_view(auth_views.LoginView.as_view()), name="login"),
r"^login/$", jsonize_view(auth_views.LoginView.as_view()), name="login"
),
re_path( re_path(
r"^login/popup/end/$", views.LoginPopupEnd.as_view(), name="login_popup_end" r"^login/popup/end/$", views.LoginPopupEnd.as_view(), name="login_popup_end"
), ),
@ -96,9 +94,9 @@ i18n_urls += decorated_patterns(
i18n_urls += decorated_patterns( i18n_urls += decorated_patterns(
[login_required], [login_required],
re_path( re_path(
r'^map/(?P<map_id>[\d]+)/star/$', r"^map/(?P<map_id>[\d]+)/star/$",
views.ToggleMapStarStatus.as_view(), views.ToggleMapStarStatus.as_view(),
name='map_star' name="map_star",
), ),
) )
i18n_urls += decorated_patterns( i18n_urls += decorated_patterns(
@ -151,10 +149,11 @@ urlpatterns += i18n_patterns(
), ),
re_path(r"^search/$", views.search, name="search"), re_path(r"^search/$", views.search, name="search"),
re_path(r"^about/$", views.about, name="about"), re_path(r"^about/$", views.about, name="about"),
re_path(r"^user/(?P<username>.+)/stars/$", views.user_stars, name='user_stars'), re_path(r"^user/(?P<username>.+)/stars/$", views.user_stars, name="user_stars"),
re_path(r"^user/(?P<username>.+)/$", views.user_maps, name="user_maps"), re_path(r"^user/(?P<username>.+)/$", views.user_maps, name="user_maps"),
re_path(r"", include(i18n_urls)), re_path(r"", include(i18n_urls)),
) )
urlpatterns += (path("stats/", cache_page(60 * 60)(views.stats), name="stats"),)
if settings.DEBUG and settings.MEDIA_ROOT: if settings.DEBUG and settings.MEDIA_ROOT:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

View file

@ -3,6 +3,7 @@ import mimetypes
import os import os
import re import re
import socket import socket
from datetime import date, timedelta
from pathlib import Path from pathlib import Path
from django.conf import settings from django.conf import settings
@ -10,7 +11,7 @@ from django.contrib import messages
from django.contrib.auth import logout as do_logout from django.contrib.auth import logout as do_logout
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.gis.measure import D from django.contrib.gis.measure import D
from django.contrib.postgres.search import SearchQuery, SearchRank, SearchVector from django.contrib.postgres.search import SearchQuery, SearchVector
from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
from django.core.signing import BadSignature, Signer from django.core.signing import BadSignature, Signer
from django.core.validators import URLValidator, ValidationError from django.core.validators import URLValidator, ValidationError
@ -214,7 +215,7 @@ class Search(TemplateView, PaginatorMixin):
q, config=settings.UMAP_SEARCH_CONFIGURATION, search_type="websearch" q, config=settings.UMAP_SEARCH_CONFIGURATION, search_type="websearch"
) )
qs = Map.objects.annotate(search=vector).filter(search=query) qs = Map.objects.annotate(search=vector).filter(search=query)
qs = qs.filter(share_status=Map.PUBLIC).order_by('-modified_at') qs = qs.filter(share_status=Map.PUBLIC).order_by("-modified_at")
results = self.paginate(qs) results = self.paginate(qs)
kwargs.update({"maps": results, "q": q}) kwargs.update({"maps": results, "q": q})
return kwargs return kwargs
@ -381,7 +382,7 @@ class MapDetailMixin:
"allowEdit": self.is_edit_allowed(), "allowEdit": self.is_edit_allowed(),
"default_iconUrl": "%sumap/img/marker.png" % settings.STATIC_URL, # noqa "default_iconUrl": "%sumap/img/marker.png" % settings.STATIC_URL, # noqa
"umap_id": self.get_umap_id(), "umap_id": self.get_umap_id(),
'starred': self.is_starred(), "starred": self.is_starred(),
"licences": dict((l.name, l.json) for l in Licence.objects.all()), "licences": dict((l.name, l.json) for l in Licence.objects.all()),
"edit_statuses": [(i, str(label)) for i, label in Map.EDIT_STATUS], "edit_statuses": [(i, str(label)) for i, label in Map.EDIT_STATUS],
"share_statuses": [ "share_statuses": [
@ -663,9 +664,8 @@ class MapClone(PermissionsMixin, View):
class ToggleMapStarStatus(View): class ToggleMapStarStatus(View):
def post(self, *args, **kwargs): def post(self, *args, **kwargs):
map_inst = get_object_or_404(Map, pk=kwargs['map_id']) map_inst = get_object_or_404(Map, pk=kwargs["map_id"])
qs = Star.objects.filter(map=map_inst, by=self.request.user) qs = Star.objects.filter(map=map_inst, by=self.request.user)
if qs.exists(): if qs.exists():
qs.delete() qs.delete()
@ -852,6 +852,22 @@ class PictogramJSONList(ListView):
# ############## # # ############## #
def stats(request):
last_week = date.today() - timedelta(days=7)
return simple_json_response(
**{
"maps_count": Map.objects.count(),
"maps_active_last_week_count": Map.objects.filter(
modified_at__gt=last_week
).count(),
"users_count": User.objects.count(),
"users_active_last_week_count": User.objects.filter(
last_login__gt=last_week
).count(),
}
)
def logout(request): def logout(request):
do_logout(request) do_logout(request)
return simple_json_response(redirect="/") return simple_json_response(redirect="/")