Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions config/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -694,3 +694,6 @@
# ------------------------------------------------------------------------------
MAX_PAGES = env.int("MAX_PAGES", default=50)
GRAFT_DEBUG = env.bool("GRAFT_DEBUG", default=False)

# STATS API
UPLOAD_WINDOW_DAYS = env.int("UPLOAD_WINDOW_DAYS", default=90)
7 changes: 7 additions & 0 deletions config/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,15 @@
from documentcloud.drf_bulk.routers import BulkDefaultRouter, BulkRouterMixin
from documentcloud.entities.views import EntityOccurrenceViewSet, EntityViewSet
from documentcloud.flatpages.views import FlatPageViewSet
from documentcloud.organizations.stats_api.views import OrganizationStatsViewSet
from documentcloud.organizations.views import OrganizationViewSet
from documentcloud.projects.views import (
CollaborationViewSet,
ProjectMembershipViewSet,
ProjectViewSet,
)
from documentcloud.statistics.views import StatisticsViewSet
from documentcloud.users.stats_api.views import UserStatsViewSet
from documentcloud.users.views import MessageView, UserViewSet


Expand Down Expand Up @@ -94,6 +96,10 @@ class BulkNestedDefaultRouter(BulkRouterMixin, NestedDefaultRouter):

router.register("documents/search/saved", SavedSearchViewSet, basename="saved_search")

stats_router = BulkDefaultRouter()
stats_router.register("users", UserStatsViewSet, basename="user-stats")
stats_router.register("organizations", OrganizationStatsViewSet, basename="org-stats")

urlpatterns = [
path("", RedirectView.as_view(url="/api/"), name="index"),
path(settings.ADMIN_URL, admin.site.urls),
Expand Down Expand Up @@ -138,6 +144,7 @@ class BulkNestedDefaultRouter(BulkRouterMixin, NestedDefaultRouter):
path(
"addons/dashboard/scraper/", scraper_dashboard, name="addon-scraper-dashboard"
),
path("stats_api/", include(stats_router.urls)),
]

if "debug_toolbar" in settings.INSTALLED_APPS:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
# Generated by Django 5.2.15 on 2026-08-03 15:06

import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("organizations", "0022_organization_created_at_organization_updated_at"),
]

operations = [
migrations.CreateModel(
name="OrganizationStats",
fields=[
(
"organization",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
primary_key=True,
related_name="stats",
serialize=False,
to=settings.SQUARELET_ORGANIZATION_MODEL,
),
),
(
"last_upload_at",
models.DateTimeField(blank=True, db_index=True, null=True),
),
],
),
migrations.AlterField(
model_name="organization",
name="document_language",
field=models.CharField(
blank=True,
choices=[
("afr", "Afrikaans"),
("amh", "Amharic"),
("ara", "Arabic"),
("asm", "Assamese"),
("aze", "Azerbaijani"),
("aze_cyrl", "Azerbaijani - Cyrillic"),
("bel", "Belarusian"),
("ben", "Bengali"),
("bod", "Tibetan"),
("bos", "Bosnian"),
("bul", "Bulgarian"),
("cat", "Catalan; Valencian"),
("ceb", "Cebuano"),
("ces", "Czech"),
("zho", "chi_sim"),
("tra", "chi_tra"),
("chr", "Cherokee"),
("cym", "Welsh"),
("dan", "Danish"),
("deu", "German"),
("dzo", "Dzongkha"),
("ell", "Greek"),
("eng", "English"),
("enm", "Middle English"),
("epo", "Esperanto"),
("est", "Estonian"),
("eus", "Basque"),
("fas", "Persian"),
("fin", "Finnish"),
("fra", "French"),
("frk", "German Fraktur"),
("frm", "Middle French"),
("gle", "Irish"),
("glg", "Galician"),
("grc", "Ancient Greek"),
("guj", "Gujarati"),
("hat", "Haitian; Haitian Creole"),
("heb", "Hebrew"),
("hin", "Hindi"),
("hrv", "Croatian"),
("hun", "Hungarian"),
("iku", "Inuktitut"),
("ind", "Indonesian"),
("isl", "Icelandic"),
("ita", "Italian"),
("ita_old", "Italian - Old"),
("jav", "Javanese"),
("jpn", "Japanese"),
("kan", "Kannada"),
("kat", "Georgian"),
("kat_old", "Georgian - Old"),
("kaz", "Kazakh"),
("khm", "Central Khmer"),
("kir", "Kirghiz; Kyrgyz"),
("kor", "Korean"),
("kur", "Kurdish"),
("lao", "Lao"),
("lat", "Latin"),
("lav", "Latvian"),
("lit", "Lithuanian"),
("mal", "Malayalam"),
("mar", "Marathi"),
("mkd", "Macedonian"),
("mlt", "Maltese"),
("msa", "Malay"),
("mya", "Burmese"),
("nep", "Nepali"),
("nld", "Dutch; Flemish"),
("nor", "Norwegian"),
("ori", "Oriya"),
("pan", "Panjabi; Punjabi"),
("pol", "Polish"),
("por", "Portuguese"),
("pus", "Pushto; Pashto"),
("ron", "Romanian; Moldavian; Moldovan"),
("rus", "Russian"),
("san", "Sanskrit"),
("sin", "Sinhala; Sinhalese"),
("slk", "Slovak"),
("slv", "Slovenian"),
("spa", "Spanish; Castilian"),
("spa_old", "Spanish; Castilian - Old"),
("sqi", "Albanian"),
("srp", "Serbian"),
("srp_latn", "Serbian - Latin"),
("swa", "Swahili"),
("swe", "Swedish"),
("syr", "Syriac"),
("tam", "Tamil"),
("tel", "Telugu"),
("tgk", "Tajik"),
("tgl", "Tagalog"),
("tha", "Thai"),
("tir", "Tigrinya"),
("tur", "Turkish"),
("uig", "Uighur; Uyghur"),
("ukr", "Ukrainian"),
("urd", "Urdu"),
("uzb", "Uzbek"),
("uzb_cyrl", "Uzbek - Cyrillic"),
("vie", "Vietnamese"),
("yid", "Yiddish"),
],
default="eng",
help_text="The default document language for user's in this organization",
max_length=8,
verbose_name="document language",
),
),
migrations.AlterField(
model_name="organization",
name="language",
field=models.CharField(
blank=True,
choices=[
("afr", "Afrikaans"),
("amh", "Amharic"),
("ara", "Arabic"),
("asm", "Assamese"),
("aze", "Azerbaijani"),
("aze_cyrl", "Azerbaijani - Cyrillic"),
("bel", "Belarusian"),
("ben", "Bengali"),
("bod", "Tibetan"),
("bos", "Bosnian"),
("bul", "Bulgarian"),
("cat", "Catalan; Valencian"),
("ceb", "Cebuano"),
("ces", "Czech"),
("zho", "chi_sim"),
("tra", "chi_tra"),
("chr", "Cherokee"),
("cym", "Welsh"),
("dan", "Danish"),
("deu", "German"),
("dzo", "Dzongkha"),
("ell", "Greek"),
("eng", "English"),
("enm", "Middle English"),
("epo", "Esperanto"),
("est", "Estonian"),
("eus", "Basque"),
("fas", "Persian"),
("fin", "Finnish"),
("fra", "French"),
("frk", "German Fraktur"),
("frm", "Middle French"),
("gle", "Irish"),
("glg", "Galician"),
("grc", "Ancient Greek"),
("guj", "Gujarati"),
("hat", "Haitian; Haitian Creole"),
("heb", "Hebrew"),
("hin", "Hindi"),
("hrv", "Croatian"),
("hun", "Hungarian"),
("iku", "Inuktitut"),
("ind", "Indonesian"),
("isl", "Icelandic"),
("ita", "Italian"),
("ita_old", "Italian - Old"),
("jav", "Javanese"),
("jpn", "Japanese"),
("kan", "Kannada"),
("kat", "Georgian"),
("kat_old", "Georgian - Old"),
("kaz", "Kazakh"),
("khm", "Central Khmer"),
("kir", "Kirghiz; Kyrgyz"),
("kor", "Korean"),
("kur", "Kurdish"),
("lao", "Lao"),
("lat", "Latin"),
("lav", "Latvian"),
("lit", "Lithuanian"),
("mal", "Malayalam"),
("mar", "Marathi"),
("mkd", "Macedonian"),
("mlt", "Maltese"),
("msa", "Malay"),
("mya", "Burmese"),
("nep", "Nepali"),
("nld", "Dutch; Flemish"),
("nor", "Norwegian"),
("ori", "Oriya"),
("pan", "Panjabi; Punjabi"),
("pol", "Polish"),
("por", "Portuguese"),
("pus", "Pushto; Pashto"),
("ron", "Romanian; Moldavian; Moldovan"),
("rus", "Russian"),
("san", "Sanskrit"),
("sin", "Sinhala; Sinhalese"),
("slk", "Slovak"),
("slv", "Slovenian"),
("spa", "Spanish; Castilian"),
("spa_old", "Spanish; Castilian - Old"),
("sqi", "Albanian"),
("srp", "Serbian"),
("srp_latn", "Serbian - Latin"),
("swa", "Swahili"),
("swe", "Swedish"),
("syr", "Syriac"),
("tam", "Tamil"),
("tel", "Telugu"),
("tgk", "Tajik"),
("tgl", "Tagalog"),
("tha", "Thai"),
("tir", "Tigrinya"),
("tur", "Turkish"),
("uig", "Uighur; Uyghur"),
("ukr", "Ukrainian"),
("urd", "Urdu"),
("uzb", "Uzbek"),
("uzb_cyrl", "Uzbek - Cyrillic"),
("vie", "Vietnamese"),
("yid", "Yiddish"),
],
default="eng",
help_text="The default interface language for user's in this organization",
max_length=8,
verbose_name="language",
),
),
]
3 changes: 3 additions & 0 deletions documentcloud/organizations/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
from documentcloud.core.fields import AutoCreatedField, AutoLastModifiedField
from documentcloud.organizations.exceptions import InsufficientAICreditsError
from documentcloud.organizations.querysets import OrganizationQuerySet
from documentcloud.organizations.stats_api.models import ( # pylint: disable=unused-import
OrganizationStats,
)

logger = logging.getLogger(__name__)

Expand Down
Empty file.
18 changes: 18 additions & 0 deletions documentcloud/organizations/stats_api/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Django
from django.db import models


class OrganizationStats(models.Model):
organization = models.OneToOneField(
"organizations.Organization",
on_delete=models.CASCADE,
related_name="stats",
primary_key=True,
)
last_upload_at = models.DateTimeField(null=True, blank=True, db_index=True)

class Meta:
app_label = "organizations"

def __str__(self):
return f"Stats for organization {self.organization_id}"
35 changes: 35 additions & 0 deletions documentcloud/organizations/stats_api/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Django
from django.utils import timezone
from rest_framework import serializers

# DocumentCloud
from documentcloud.organizations.stats_api.models import OrganizationStats


class OrganizationStatsSerializer(serializers.ModelSerializer):
uuid = serializers.UUIDField(source="organization.uuid", read_only=True)
total_documents = serializers.IntegerField(read_only=True)
days_since_last_upload = serializers.SerializerMethodField()
recent_upload_count = serializers.SerializerMethodField(
help_text="Documents uploaded by the org within the configured window "
"(UPLOAD_WINDOW_DAYS, defaults to 90)."
)

class Meta:
model = OrganizationStats
fields = [
"uuid",
"total_documents",
"last_upload_at",
"days_since_last_upload",
"recent_upload_count",
]
read_only_fields = fields

def get_days_since_last_upload(self, obj):
if obj.last_upload_at is None:
return None
return (timezone.now() - obj.last_upload_at).days

def get_recent_upload_count(self, obj):
return getattr(obj, "recent_upload_count", None)
Loading
Loading