Merge pull request #1100 from umap-project/stats-view
Add a very basic `/stats/` JSON view
This commit is contained in:
commit
7f85684d52
4 changed files with 96 additions and 42 deletions
|
@ -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.
|
||||||
|
|
|
@ -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,
|
||||||
|
}
|
||||||
|
|
13
umap/urls.py
13
umap/urls.py
|
@ -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)
|
||||||
|
|
|
@ -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="/")
|
||||||
|
|
Loading…
Reference in a new issue