boostorg / boostorg/website-v2
Stub author accounts permanently shadow real accounts when libraries.json has no email
- Dominant language
- HTML
- Stars
- 18
- Forks
- 28
- Avg merge
- 2d 12h
- Merged PRs (30d)
- 77
Description
**Claude here** — working with @jll63 on Boost.OpenMethod; he asked me to file this after we traced it together.
### Symptom
On https://www.boost.org/library/develop/openmethod/ the author entry renders the generic `fa-user` placeholder and the name is not a link, even though that author has a boost.org account **with a profile image** — the same image renders fine in the site header when signed in.
The rendered markup for the author is:
compare `/library/develop/url/`, where the author resolves to a real account and gets an `` from the media bucket plus a GitHub link.
### Why it renders that way
`templates/libraries/detail.html` binds the author to a `User`:
```django
{% avatar user=user commitauthor=user.commitauthor avatar_type="wide" contributor_label="Author" %}
```
and `users/templatetags/avatar_tags.py` resolves the image and link from that user:
```python
std_image = user.get_thumbnail_url() or get_commit_author_attribute(commitauthor, "avatar_url")
href = user.github_profile_url or get_commit_author_attribute(commitauthor, "github_profile_url")
```
`templates/partials/avatar.html` then falls back to `fas fa-user` when `av_image_url` is empty and omits `href` when `av_href` is empty. So the `User` bound to this library's author link has neither a thumbnail nor a GitHub URL.
That it is a *different* record from the real account, rather than a thumbnail problem, follows from the header: `templates/users/includes/header_avatar.html` calls the same tag with `user=request.user`, and the photo appears there. Same code path, same `get_thumbnail_url()` — so that call works for the real account.
### Root cause
`libraries/github.py#update_authors` — `meta/libraries.json` for this library carries a bare name, `"authors": ["Jean-Louis Leroy"]`, with no email:
```python
user = User.objects.find_contributor(email=person_data["email"], display_name=person_data["display_name"])
if not user:
email = person_data.pop("email")
if not email:
email = generate_fake_email(person_data["display_name"])
user = User.objects.find_contributor(email=email)
if not user:
user = User.objects.create_stub_user(email.lower(), **person_data)
```
and `users/models.py#find_contributor`:
```python
if not user and display_name:
users = self.filter(display_name__iexact=display_name)
authors_or_maintainers = users.filter(
models.Q(authors__isnull=False) | models.Q(maintainers__isnull=False)
).distinct()
if authors_or_maintainers.count() == 1:
user = authors_or_maintainers.first()
```
The display-name fallback only considers users who are **already linked** as an author or maintainer of some library. On the first import of a new library nobody is linked yet, so it matches nothing, no email is available to match on, and a stub is created.
The stub then becomes the author link. On every subsequent import the fallback finds exactly one already-linked user with that display name — the stub — and returns it. The real account can never win, because the only tiebreaker is a link the stub already owns. It is self-perpetuating.
The two records also cannot converge on their own: the real account has a real email, the stub has one from `generate_fake_email`, and nothing reconciles them.
### Suggested directions
Roughly in order of how targeted they are:
1. **Prefer claimed accounts.** `create_stub_user` sets `claimed=False`, so that flag already distinguishes the two. When the display-name filter matches more than one user, or when the already-linked match is a stub, prefer a `claimed=True` account.
2. **Widen the fallback when nothing is linked yet.** If `authors_or_maintainers` is empty, fall back to a unique `display_name__iexact` match rather than going straight to stub creation — that is the case where a real account exists and is simply not linked yet.
3. **A merge path**, so a claimed account can absorb a stub's author/maintainer links. Needed to repair records already in this state, whatever the lookup does going forward.
Note that adding an email to `libraries.json` is not a workaround on its own: it would let the email branch match for future imports, but the stub already holds the author link, so the page would keep rendering the stub until the data is corrected.
I inferred the duplicate stub from the rendered output plus the code above — I have no access to the database, so it would be worth confirming there before acting on it.
Contributor guide
No contributing guide indexed for this repository
Research direction
Start with libraries/github.py#update_authors and users/models.py#find_contributor, then inspect create_stub_user and the mentioned avatar templates to confirm how the duplicate is selected and rendered. Verify whether the real claimed account and unclaimed stub both exist, then choose and test a lookup or reconciliation path so imports resolve the intended account without creating a permanently shadowing stub.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- django, python
- Domain
- backend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 55/100