Schema{{ organization.public_schemas.count|pluralize }}
+-
+ {% for schema in organization.public_schemas.all %}
+
- + + {{ schema.name }} + + + {% endfor %} +
This organization has no schemas.
+ {% endif %} +diff --git a/core/admin.py b/core/admin.py index 457fdca..ef51d0f 100644 --- a/core/admin.py +++ b/core/admin.py @@ -1,6 +1,12 @@ from django.contrib import admin from django.contrib.admin.decorators import register -from .models import Schema, SchemaRef, DocumentationItem +from .models import ( + Schema, + SchemaRef, + DocumentationItem, + Organization, + Profile +) @register(Schema) @@ -17,3 +23,12 @@ class SchemaRefAdmin(admin.ModelAdmin): class DocumentationItemAdmin(admin.ModelAdmin): list_display = ['schema', 'name', 'url'] + +@register(Organization) +class OrganizationAdmin(admin.ModelAdmin): + list_display = ['name', 'slug'] + + +@register(Profile) +class ProfileAdmin(admin.ModelAdmin): + list_display = ['user', 'organization'] diff --git a/core/apps.py b/core/apps.py index 8115ae6..da7b127 100644 --- a/core/apps.py +++ b/core/apps.py @@ -4,3 +4,6 @@ class CoreConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'core' + + def ready(self): + import core.signals diff --git a/core/migrations/0009_organization_profile.py b/core/migrations/0009_organization_profile.py new file mode 100644 index 0000000..8467e57 --- /dev/null +++ b/core/migrations/0009_organization_profile.py @@ -0,0 +1,56 @@ +# Generated by Django 5.2.5 on 2026-01-21 17:08 +# Edited to create Profiles for existing users + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +def create_profiles(apps, schema_editor): + User = apps.get_model("auth", "User") + Profile = apps.get_model("core", "Profile") + + profiles = [] + for user in User.objects.all(): + profiles.append(Profile(user=user)) + + Profile.objects.bulk_create(profiles, ignore_conflicts=True) + + +def remove_profiles(apps, schema_editor): + Profile = apps.get_model("core", "Profile") + Profile.objects.all().delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0008_alter_documentationitem_format_and_more'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Organization', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('name', models.CharField(max_length=200)), + ('slug', models.SlugField(unique=True)), + ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.RESTRICT, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Profile', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('organization', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='core.organization')), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.RunPython(create_profiles, remove_profiles) + ] diff --git a/core/models.py b/core/models.py index cd6e605..f49dc52 100644 --- a/core/models.py +++ b/core/models.py @@ -3,6 +3,7 @@ from django.db.models import Q from django.contrib.auth.models import User from django.utils import timezone +from django.conf import settings from urllib.parse import urlparse import requests from .utils import guess_specification_language_by_extension, guess_language_by_extension @@ -58,6 +59,10 @@ def url_providers(self): } return provider_names + @property + def organization(self): + return self.created_by.profile.organization + def _latest_documentation_item_of_type(self, role): return self.documentationitem_set.filter(role=role).order_by('-created_at').first() @@ -207,6 +212,7 @@ def repo_url(self): normal_path = "/".join([user, repo, "blob", branch] + filepath) return f"https://{self.REPO_NETLOC}/{normal_path}" + class ReferenceItem(BaseModel): class Meta: abstract = True @@ -272,3 +278,22 @@ def __str__(self): def language(self): return guess_language_by_extension(self.url, ['markdown']) + +class Organization(BaseModel): + name = models.CharField(max_length=200) + slug = models.SlugField(unique=True) + + def __str__(self): + return self.name + + @property + def public_schemas(self): + return Schema.public_objects.filter( + created_by_id__in=self.profile_set.values_list("user_id", flat=True) + ) + + +class Profile(models.Model): + user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) + organization = models.ForeignKey(Organization, blank=True, null=True, on_delete=models.SET_NULL) + diff --git a/core/signals.py b/core/signals.py new file mode 100644 index 0000000..aa2e6d8 --- /dev/null +++ b/core/signals.py @@ -0,0 +1,13 @@ +from django.conf import settings +from django.db.models.signals import post_save +from django.dispatch import receiver + +from .models import Profile + +User = settings.AUTH_USER_MODEL + +@receiver(post_save, sender=User) +def create_user_profile(sender, instance, created, **kwargs): + if created: + Profile.objects.create(user=instance) + diff --git a/core/static/css/site.css b/core/static/css/site.css index 7ef790c..0389aae 100644 --- a/core/static/css/site.css +++ b/core/static/css/site.css @@ -904,3 +904,12 @@ a.badge--w3c { padding: 1rem; gap: 1rem; } + +/* Organization detail */ +.organization-detail { + display: flex; + flex-direction: column; + align-items: center; + padding: 1rem; + gap: 1rem; +} diff --git a/core/templates/account/profile.html b/core/templates/account/profile.html index 15da98b..deb4229 100644 --- a/core/templates/account/profile.html +++ b/core/templates/account/profile.html @@ -44,6 +44,18 @@
This organization has no schemas.
+ {% endif %} +