public inbox for [email protected]  
help / color / mirror / Atom feed
From: Melanie Plageman <[email protected]>
To: [email protected]
Cc: PostgreSQL Contributors <[email protected]>
Subject: Public contributor profile pages
Date: Tue, 16 Dec 2025 19:24:38 -0500
Message-ID: <CAAKRu_Z42AAq7N=usSS3UPMtXqbVvsQzktNmH1X20oypyqA_Xg@mail.gmail.com> (raw)

Hello,

The Contributors Committee is working on a new program to recognize
specific contributions (e.g. "volunteered at PGConf.dev 2024" or
"contributed code to PostgreSQL 18"). These would be listed on an
individual contributor's profile. As a step toward that, we are
proposing to add public contributor profile pages.

Attached is a patch set which implements this by:
1) allowing all those with pgo user accounts to edit their
"contributions" (currently only enabled for recognized contributors)
2) adds a public contributor profile view accessible via
postgresql.org/community/contributors/[username]
3) links all recognized contributors on [1] to their public contributor profile

There are quite a few details to work out. A few I know of are:
- How should opting into a public profile work? What about existing
recognized contributors, do we opt them all in automatically? Or just
opt-in Major Contributors since their "Contributions" are already on a
public web page? (see patch 0003)
- How to make the public profile URL scheme work for contributors who
do not have a pgo user account? Or should it work at all?

This is my first real web patch, so I'm sure I did some things in a
non-pgweb-idiomatic (or non web-idiomatic) way and didn't think of
lots of things. I'll probably need lots of guidance.

- Melanie

[1] https://www.postgresql.org/community/contributors/


Attachments:

  [application/octet-stream] v1-0002-Add-public-contributor-profiles.patch (5.7K, ../CAAKRu_Z42AAq7N=usSS3UPMtXqbVvsQzktNmH1X20oypyqA_Xg@mail.gmail.com/2-v1-0002-Add-public-contributor-profiles.patch)
  download | inline diff:
From 8b8d0cc59de9ecc7e7201722edf8abce5b7e224b Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email protected]>
Date: Tue, 16 Dec 2025 15:35:21 -0500
Subject: [PATCH v1 2/3] Add public contributor profiles

Now anyone can visit postgresql.org/community/contributors/[username] to
see a user's self-described contributions. This commit adds the profile
view and also links recognized contributors' names to their profiles.

The urls are based on username, which may not be the right design
because not all contributors have user profiles, but it is a starting
point.
---
 pgweb/contributors/views.py         | 10 +++++-
 pgweb/urls.py                       |  1 +
 templates/contributors/list.html    |  6 ++--
 templates/contributors/profile.html | 51 +++++++++++++++++++++++++++++
 4 files changed, 64 insertions(+), 4 deletions(-)
 create mode 100644 templates/contributors/profile.html

diff --git a/pgweb/contributors/views.py b/pgweb/contributors/views.py
index 45b8551e..60fadbd9 100644
--- a/pgweb/contributors/views.py
+++ b/pgweb/contributors/views.py
@@ -1,6 +1,7 @@
+from django.shortcuts import get_object_or_404
 from pgweb.util.contexts import render_pgweb
 
-from .models import ContributorType
+from .models import ContributorType, Contributor
 
 
 def completelist(request):
@@ -8,3 +9,10 @@ def completelist(request):
     return render_pgweb(request, 'community', 'contributors/list.html', {
         'contributortypes': contributortypes,
     })
+
+
+def contributor_profile(request, username):
+    contributor = get_object_or_404(Contributor, user__username=username)
+    return render_pgweb(request, 'community', 'contributors/profile.html', {
+        'contributor': contributor,
+    })
diff --git a/pgweb/urls.py b/pgweb/urls.py
index d9b81f75..d4a946ea 100644
--- a/pgweb/urls.py
+++ b/pgweb/urls.py
@@ -70,6 +70,7 @@ urlpatterns = [
 
     re_path(r'^community/$', pgweb.core.views.community),
     re_path(r'^community/contributors/$', pgweb.contributors.views.completelist),
+    re_path(r'^community/contributors/([^/]+)/$', pgweb.contributors.views.contributor_profile),
     re_path(r'^community/lists/$', RedirectView.as_view(url='/list/', permanent=True)),
     re_path(r'^community/lists/subscribe/$', RedirectView.as_view(url='https://lists.postgresql.org/', permanent=True)),
 
diff --git a/templates/contributors/list.html b/templates/contributors/list.html
index 068fc949..e713cee2 100644
--- a/templates/contributors/list.html
+++ b/templates/contributors/list.html
@@ -32,7 +32,7 @@
     {%for c in t.contributor_set.all %}
      {%if t.detailed%}
       <tr>
-       <td>{{c.firstname}} {{c.lastname}} {%if t.showemail and c.email%}({{c.email|hidemail}}){%endif%}
+       <td>{% if c.user %}<a href="/community/contributors/{{c.user.username}}/">{{c.firstname}} {{c.lastname}}</a>{% else %}{{c.firstname}} {{c.lastname}}{% endif %} {%if t.showemail and c.email%}({{c.email|hidemail}}){%endif%}
           {%if c.company %}
           <br/>
             {% if c.companyurl %}
@@ -49,13 +49,13 @@
      {%else%}
       {%if forloop.counter0|divisibleby:"2" %}
        <tr>
-        <td>{{c.firstname}} {{c.lastname}}{%if t.showemail and c.email%} ({{c.email|hidemail}}){%endif%}</td>
+        <td>{% if c.user %}<a href="/community/contributors/{{c.user.username}}/">{{c.firstname}} {{c.lastname}}</a>{% else %}{{c.firstname}} {{c.lastname}}{% endif %}{%if t.showemail and c.email%} ({{c.email|hidemail}}){%endif%}</td>
         {%if forloop.last%}
         <td></td>
        </tr>
         {%endif%}
       {%else%}
-        <td>{{c.firstname}} {{c.lastname}}{%if t.showemail and c.email%} ({{c.email|hidemail}}){%endif%}</td>
+        <td>{% if c.user %}<a href="/community/contributors/{{c.user.username}}/">{{c.firstname}} {{c.lastname}}</a>{% else %}{{c.firstname}} {{c.lastname}}{% endif %}{%if t.showemail and c.email%} ({{c.email|hidemail}}){%endif%}</td>
        </tr>
       {%endif%}
      {%endif%}
diff --git a/templates/contributors/profile.html b/templates/contributors/profile.html
new file mode 100644
index 00000000..6f476c3c
--- /dev/null
+++ b/templates/contributors/profile.html
@@ -0,0 +1,51 @@
+{%extends "base/page.html"%}
+{%load pgfilters%}
+{%block title%}{{contributor.firstname}} {{contributor.lastname}} - Contributor Profile{%endblock%}
+{%block contents%}
+<h1>{{contributor.firstname}} {{contributor.lastname}} <i class="fa fa-user"></i></h1>
+
+<div class="row">
+  <div class="col-md-8">
+    {%if contributor.contribution%}
+    <h2>Contribution</h2>
+    <p>{{contributor.contribution}}</p>
+    {%endif%}
+
+    <h2>Information</h2>
+    <dl class="row">
+      <dt class="col-sm-3">Contributor Type</dt>
+      <dd class="col-sm-9">{{contributor.ctype.typename}}</dd>
+
+      {%if contributor.email%}
+      <dt class="col-sm-3">Email</dt>
+      <dd class="col-sm-9">{{contributor.email|hidemail}}</dd>
+      {%endif%}
+
+      {%if contributor.location%}
+      <dt class="col-sm-3">Location</dt>
+      <dd class="col-sm-9">{{contributor.location}}</dd>
+      {%endif%}
+
+      {%if contributor.company%}
+      <dt class="col-sm-3">Company</dt>
+      <dd class="col-sm-9">
+        {%if contributor.companyurl%}
+          <a href="{{contributor.companyurl}}" target="_blank" rel="noopener">{{contributor.company}}</a>
+        {%else%}
+          {{contributor.company}}
+        {%endif%}
+      </dd>
+      {%endif%}
+    </dl>
+  </div>
+</div>
+
+<div class="mt-4">
+  <p>
+    <a href="mailto:[email protected]?subject=Report contributor profile: {{contributor.firstname}} {{contributor.lastname}}" title="Report inappropriate profile that violates PostgreSQL Code of Conduct">
+      <i class="fa fa-flag"></i> Report this profile
+    </a>
+  </p>
+</div>
+
+{%endblock%}
-- 
2.51.2



  [application/octet-stream] v1-0001-Allow-all-users-a-contributor-description.patch (7.1K, ../CAAKRu_Z42AAq7N=usSS3UPMtXqbVvsQzktNmH1X20oypyqA_Xg@mail.gmail.com/3-v1-0001-Allow-all-users-a-contributor-description.patch)
  download | inline diff:
From f1c4be8509b46410f634f1f56ebf90876d4eb966 Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email protected]>
Date: Tue, 16 Dec 2025 15:18:44 -0500
Subject: [PATCH v1 1/3] Allow all users a contributor description

Previously only recognized contributors could add a description of their
contributions to Postgres. In order to support public contributor
profiles for all users (not just recognized contributors), contributors
must be able to add contributions.
---
 pgweb/account/views.py                        | 27 +++++++++----------
 .../0004_alter_contributor_ctype.py           | 24 +++++++++++++++++
 pgweb/contributors/models.py                  |  6 +++--
 templates/account/userprofileform.html        |  8 +++---
 4 files changed, 45 insertions(+), 20 deletions(-)
 create mode 100644 pgweb/contributors/migrations/0004_alter_contributor_ctype.py

diff --git a/pgweb/account/views.py b/pgweb/account/views.py
index 4f6cfa78..6900e04f 100644
--- a/pgweb/account/views.py
+++ b/pgweb/account/views.py
@@ -142,14 +142,14 @@ def profile(request):
     # accounts.
     can_change_email = (request.user.password != OAUTH_PASSWORD_STORE)
 
-    # We may have a contributor record - and we only show that part of the
-    # form if we have it for this user.
-    try:
-        contrib = Contributor.objects.get(user=request.user.pk)
-    except Contributor.DoesNotExist:
-        contrib = None
-
-    contribform = None
+    contrib, _ = Contributor.objects.get_or_create(
+        user=request.user,
+        defaults={
+            'firstname': request.user.first_name,
+            'lastname': request.user.last_name,
+            'email': request.user.email,
+        }
+    )
 
     secondaryaddresses = SecondaryEmail.objects.filter(user=request.user)
 
@@ -158,10 +158,9 @@ def profile(request):
         userform = UserForm(can_change_email, secondaryaddresses, data=request.POST, instance=request.user)
         profileform = UserProfileForm(data=request.POST, instance=profile)
         secondaryemailform = AddEmailForm(request.user, data=request.POST)
-        if contrib:
-            contribform = ContributorForm(data=request.POST, instance=contrib)
+        contribform = ContributorForm(data=request.POST, instance=contrib)
 
-        if userform.is_valid() and profileform.is_valid() and secondaryemailform.is_valid() and (not contrib or contribform.is_valid()):
+        if userform.is_valid() and profileform.is_valid() and secondaryemailform.is_valid() and contribform.is_valid():
             user = userform.save()
 
             # Email takes some magic special handling, since we only allow picking of existing secondary emails, but it's
@@ -179,8 +178,7 @@ def profile(request):
                 log.info("User {} changed primary email from {} to {}".format(user.username, oldemail, user.email))
 
             profileform.save()
-            if contrib:
-                contribform.save()
+            contribform.save()
             if secondaryemailform.cleaned_data.get('email1', ''):
                 sa = SecondaryEmail(user=request.user, email=secondaryemailform.cleaned_data['email1'], token=generate_random_token())
                 sa.save()
@@ -203,8 +201,7 @@ def profile(request):
         userform = UserForm(can_change_email, secondaryaddresses, instance=request.user)
         profileform = UserProfileForm(instance=profile)
         secondaryemailform = AddEmailForm(request.user)
-        if contrib:
-            contribform = ContributorForm(instance=contrib)
+        contribform = ContributorForm(instance=contrib)
 
     return render_pgweb(request, 'account', 'account/userprofileform.html', {
         'userform': userform,
diff --git a/pgweb/contributors/migrations/0004_alter_contributor_ctype.py b/pgweb/contributors/migrations/0004_alter_contributor_ctype.py
new file mode 100644
index 00000000..fdf15405
--- /dev/null
+++ b/pgweb/contributors/migrations/0004_alter_contributor_ctype.py
@@ -0,0 +1,24 @@
+# Generated by Django 5.2.8 on 2025-12-15 23:59
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('contributors', '0003_make_email_nullable'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='contributor',
+            name='ctype',
+            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='contributors.contributortype', verbose_name='Contributor Type'),
+        ),
+        migrations.AlterField(
+            model_name='contributor',
+            name='contribution',
+            field=models.TextField(blank=True, help_text='Only displayed for Major Contributors', null=True),
+        ),
+    ]
diff --git a/pgweb/contributors/models.py b/pgweb/contributors/models.py
index 342cddb2..afecf2be 100644
--- a/pgweb/contributors/models.py
+++ b/pgweb/contributors/models.py
@@ -19,7 +19,9 @@ class ContributorType(models.Model):
 
 
 class Contributor(models.Model):
-    ctype = models.ForeignKey(ContributorType, on_delete=models.CASCADE, verbose_name='Contributor Type')
+    ctype = models.ForeignKey(ContributorType,
+                              on_delete=models.CASCADE,
+                              verbose_name='Contributor Type', null=True, blank=True)
     lastname = models.CharField(max_length=100, null=False, blank=False)
     firstname = models.CharField(max_length=100, null=False, blank=False)
     email = models.EmailField(null=False, blank=True)
@@ -27,7 +29,7 @@ class Contributor(models.Model):
     companyurl = models.URLField(max_length=100, null=True, blank=True, verbose_name='Company URL')
     location = models.CharField(max_length=100, null=True, blank=True)
     contribution = models.TextField(null=True, blank=True,
-                                    help_text='This description is currently used for major contributors only')
+                                    help_text='Only displayed for Major Contributors')
     user = models.ForeignKey(User, null=True, blank=True, on_delete=models.CASCADE)
 
     send_notification = True
diff --git a/templates/account/userprofileform.html b/templates/account/userprofileform.html
index 96ed2779..12d6ace4 100644
--- a/templates/account/userprofileform.html
+++ b/templates/account/userprofileform.html
@@ -94,9 +94,12 @@
       </div>
     {%endfor%}
 
-  {% if contribform %}
     <h2>Edit contributor information</h2>
-    <p>You can edit the information that's shown on the <a href="/community/contributors/" target="_blank" rel="noopener">contributors</a> page. Please be careful as your changes will take effect immediately!
+    <p>You can edit the information that is shown on your contributor profile.
+      If you are a recognized Major Contributor, this is what will be displayed
+      on the
+      <a href="/community/contributors/" target="_blank" rel="noopener">contributors</a>
+      page. Please be careful as your changes will take effect immediately!
     </p>
     {% for field in contribform %}
       <div class="form-group row">
@@ -116,7 +119,6 @@
         </div>
       </div>
     {% endfor %}
-  {% endif %}
 
   <div class="submit-row">
     <input class="btn btn-primary" type="submit" value="Save" />
-- 
2.51.2



  [application/octet-stream] v1-0003-Make-public-contributor-profile-opt-in.patch (6.8K, ../CAAKRu_Z42AAq7N=usSS3UPMtXqbVvsQzktNmH1X20oypyqA_Xg@mail.gmail.com/4-v1-0003-Make-public-contributor-profile-opt-in.patch)
  download | inline diff:
From cd32478c2bb49eed10eb56f2f21db9f10a34b989 Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email protected]>
Date: Tue, 16 Dec 2025 18:00:57 -0500
Subject: [PATCH v1 3/3] Make public contributor profile opt-in

I'm not really sure if this is the right way to do this at all, so I
made it a separate patch. Basically, we probably don't want to just
suddenly have everybody with a user account at postgresql.org have a
public profile page. But, I don't know how it should work exactly --
both from a feature specification perspective and an implementation
perspective.

For one, we can't have all existing contributors not be displayed on the
recognized contributor page just because they didn't opt-in (which is
what I did here). But do we want all of them to get profiles without
first opting in?
---
 pgweb/account/forms.py                         |  3 +++
 .../migrations/0005_add_hidden_field.py        | 18 ++++++++++++++++++
 pgweb/contributors/models.py                   |  3 +++
 pgweb/contributors/views.py                    |  7 ++++++-
 pgweb/core/models.py                           |  3 +++
 templates/contributors/list.html               |  6 +++---
 6 files changed, 36 insertions(+), 4 deletions(-)
 create mode 100644 pgweb/contributors/migrations/0005_add_hidden_field.py

diff --git a/pgweb/account/forms.py b/pgweb/account/forms.py
index fb3641b0..5e8002ed 100644
--- a/pgweb/account/forms.py
+++ b/pgweb/account/forms.py
@@ -161,6 +161,9 @@ class ContributorForm(forms.ModelForm):
     class Meta:
         model = Contributor
         exclude = ('ctype', 'lastname', 'firstname', 'user', )
+        widgets = {
+            'hidden': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
+        }
 
 
 class AddEmailForm(forms.Form):
diff --git a/pgweb/contributors/migrations/0005_add_hidden_field.py b/pgweb/contributors/migrations/0005_add_hidden_field.py
new file mode 100644
index 00000000..ad5799da
--- /dev/null
+++ b/pgweb/contributors/migrations/0005_add_hidden_field.py
@@ -0,0 +1,18 @@
+# Generated by Django 5.2.8 on 2025-12-16 23:47
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('contributors', '0004_alter_contributor_ctype'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='contributor',
+            name='hidden',
+            field=models.BooleanField(default=True, help_text='Hide your contributor profile from /community/contributors/[username]/', verbose_name='Hide public profile'),
+        ),
+    ]
diff --git a/pgweb/contributors/models.py b/pgweb/contributors/models.py
index afecf2be..592227ec 100644
--- a/pgweb/contributors/models.py
+++ b/pgweb/contributors/models.py
@@ -31,6 +31,9 @@ class Contributor(models.Model):
     contribution = models.TextField(null=True, blank=True,
                                     help_text='Only displayed for Major Contributors')
     user = models.ForeignKey(User, null=True, blank=True, on_delete=models.CASCADE)
+    hidden = models.BooleanField(null=False, blank=False, default=True,
+                                 verbose_name='Hide public profile',
+                                 help_text='Check to hide your contributor profile. Uncheck to make it publicly accessible at /community/contributors/[username]/')
 
     send_notification = True
     purge_urls = ('/community/contributors/', )
diff --git a/pgweb/contributors/views.py b/pgweb/contributors/views.py
index 60fadbd9..767e7624 100644
--- a/pgweb/contributors/views.py
+++ b/pgweb/contributors/views.py
@@ -12,7 +12,12 @@ def completelist(request):
 
 
 def contributor_profile(request, username):
-    contributor = get_object_or_404(Contributor, user__username=username)
+    # Only show profiles that are not hidden
+    contributor = get_object_or_404(
+        Contributor,
+        user__username=username,
+        hidden=False
+    )
     return render_pgweb(request, 'community', 'contributors/profile.html', {
         'contributor': contributor,
     })
diff --git a/pgweb/core/models.py b/pgweb/core/models.py
index 54185917..2e88c04f 100644
--- a/pgweb/core/models.py
+++ b/pgweb/core/models.py
@@ -272,6 +272,9 @@ class UserProfile(models.Model):
     block_oauth = models.BooleanField(null=False, blank=False, default=False,
                                       verbose_name="Block OAuth login",
                                       help_text="Disallow login to this account using OAuth providers like Google or Microsoft.")
+    show_contributor_profile = models.BooleanField(null=False, blank=False, default=False,
+                                                   verbose_name="Show public contributor profile",
+                                                   help_text="Allow others to view your contributor profile at /community/contributors/[username]/")
 
 
 class UserSubmission(models.Model):
diff --git a/templates/contributors/list.html b/templates/contributors/list.html
index e713cee2..ef324572 100644
--- a/templates/contributors/list.html
+++ b/templates/contributors/list.html
@@ -32,7 +32,7 @@
     {%for c in t.contributor_set.all %}
      {%if t.detailed%}
       <tr>
-       <td>{% if c.user %}<a href="/community/contributors/{{c.user.username}}/">{{c.firstname}} {{c.lastname}}</a>{% else %}{{c.firstname}} {{c.lastname}}{% endif %} {%if t.showemail and c.email%}({{c.email|hidemail}}){%endif%}
+       <td>{% if c.user and not c.hidden %}<a href="/community/contributors/{{c.user.username}}/">{{c.firstname}} {{c.lastname}}</a>{% else %}{{c.firstname}} {{c.lastname}}{% endif %} {%if t.showemail and c.email%}({{c.email|hidemail}}){%endif%}
           {%if c.company %}
           <br/>
             {% if c.companyurl %}
@@ -49,13 +49,13 @@
      {%else%}
       {%if forloop.counter0|divisibleby:"2" %}
        <tr>
-        <td>{% if c.user %}<a href="/community/contributors/{{c.user.username}}/">{{c.firstname}} {{c.lastname}}</a>{% else %}{{c.firstname}} {{c.lastname}}{% endif %}{%if t.showemail and c.email%} ({{c.email|hidemail}}){%endif%}</td>
+        <td>{% if c.user and not c.hidden %}<a href="/community/contributors/{{c.user.username}}/">{{c.firstname}} {{c.lastname}}</a>{% else %}{{c.firstname}} {{c.lastname}}{% endif %}{%if t.showemail and c.email%} ({{c.email|hidemail}}){%endif%}</td>
         {%if forloop.last%}
         <td></td>
        </tr>
         {%endif%}
       {%else%}
-        <td>{% if c.user %}<a href="/community/contributors/{{c.user.username}}/">{{c.firstname}} {{c.lastname}}</a>{% else %}{{c.firstname}} {{c.lastname}}{% endif %}{%if t.showemail and c.email%} ({{c.email|hidemail}}){%endif%}</td>
+        <td>{% if c.user and not c.hidden %}<a href="/community/contributors/{{c.user.username}}/">{{c.firstname}} {{c.lastname}}</a>{% else %}{{c.firstname}} {{c.lastname}}{% endif %}{%if t.showemail and c.email%} ({{c.email|hidemail}}){%endif%}</td>
        </tr>
       {%endif%}
      {%endif%}
-- 
2.51.2



view thread (5+ messages)  latest in thread

reply

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Reply to all the recipients using the --to and --cc options:
  reply via email

  To: [email protected]
  Cc: [email protected], [email protected], [email protected]
  Subject: Re: Public contributor profile pages
  In-Reply-To: <CAAKRu_Z42AAq7N=usSS3UPMtXqbVvsQzktNmH1X20oypyqA_Xg@mail.gmail.com>

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox