Skip to content

Commit fb9caae

Browse files
authored
Merge pull request #1303 from makeabilitylab/1300-hide-private-projects-remaining-surfaces
Hide private projects on all remaining surfaces (#1300 follow-up)
2 parents 35ada26 + dd272b8 commit fb9caae

8 files changed

Lines changed: 144 additions & 17 deletions

File tree

website/models/award.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,16 @@ def get_project_names(self):
7070

7171
get_project_names.short_description = "Projects"
7272

73+
def get_visible_projects(self):
74+
"""
75+
Returns the honored projects that are publicly visible (#1300).
76+
77+
Used by the public award snippet so a private project is not mentioned
78+
on the Awards page. The admin-facing get_project_names() intentionally
79+
still lists all projects.
80+
"""
81+
return self.projects.filter(is_visible=True)
82+
7383
def get_honorees(self):
7484
"""Returns a combined, human-readable list of recipients and projects."""
7585
parts = [self.get_recipient_names(), self.get_project_names()]

website/models/person.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -598,9 +598,12 @@ def get_projects_sorted_by_contrib(self, filter_out_projs_with_zero_pubs=True):
598598
"""
599599
Project = apps.get_model('website', 'Project')
600600

601-
# Start with projects where this person has a role
601+
# Start with publicly-visible projects where this person has a role.
602+
# This feeds the public People page, so private projects (#1300) are
603+
# excluded.
602604
projects_qs = Project.objects.filter(
603-
projectrole__person=self
605+
projectrole__person=self,
606+
is_visible=True
604607
).annotate(
605608
# Count publications by this person on each project
606609
pub_count=Count(

website/templates/snippets/display_award_snippet.html

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,11 @@
2121
<a href="{% url 'website:member_by_name' person.get_url_name %}">{{ person.get_full_name }}</a>{% if not forloop.last %}, {% endif %}
2222
{% endfor %}
2323

24-
{% if award.recipients.exists and award.projects.exists %}, {% endif %}
24+
{% if award.recipients.exists and award.get_visible_projects.exists %}, {% endif %}
2525

26-
{% for project in award.projects.all %}
27-
{% if project.can_show_online %}
26+
{# Only list publicly-visible projects so private projects aren't named here (#1300) #}
27+
{% for project in award.get_visible_projects %}
2828
<a href="{% url 'website:project' project.short_name %}">{{ project.name }}</a>{% if not forloop.last %}, {% endif %}
29-
{% else %}
30-
{{ project.name }}{% if not forloop.last %}, {% endif %}
31-
{% endif %}
3229
{% endfor %}
3330
</p>
3431

website/tests/test_project_visibility.py

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from django.urls import reverse
2121

2222
from website.models import Project
23+
from website.models.project_role import ProjectRole
2324
from website.tests.base import DatabaseTestCase
2425

2526

@@ -191,3 +192,108 @@ def test_visible_project_200_for_anonymous(self):
191192
reverse("website:project", kwargs={"project_name": project.short_name})
192193
)
193194
self.assertEqual(response.status_code, 200)
195+
196+
197+
# --- Secondary surfaces: nothing should mention a private project --------
198+
199+
200+
class AwardVisibleProjectsTests(DatabaseTestCase):
201+
"""Award.get_visible_projects (used by the public awards snippet) excludes private projects."""
202+
203+
def test_only_visible_projects_returned(self):
204+
from website.models import Award
205+
visible = self.make_project(name="Award Visible", is_visible=True)
206+
private = self.make_project(name="Award Private", is_visible=False)
207+
award = Award.objects.create(title="Best Paper", date=date(2024, 1, 1))
208+
award.projects.add(visible, private)
209+
210+
names = {p.name for p in award.get_visible_projects()}
211+
self.assertEqual(names, {"Award Visible"})
212+
213+
214+
class PersonProjectsContribVisibilityTests(DatabaseTestCase):
215+
"""get_projects_sorted_by_contrib (public People page) excludes private projects."""
216+
217+
def _link(self, person, project):
218+
ProjectRole.objects.create(
219+
project=project, person=person, start_date=date(2024, 1, 1)
220+
)
221+
pub = self.make_publication(title=f"Pub {project.name}")
222+
pub.authors.add(person)
223+
pub.projects.add(project)
224+
225+
def test_private_project_excluded(self):
226+
person = self.make_person(first_name="Grace", last_name="Hopper")
227+
visible = self.make_project(name="Contrib Visible", is_visible=True)
228+
private = self.make_project(name="Contrib Private", is_visible=False)
229+
self._link(person, visible)
230+
self._link(person, private)
231+
232+
names = {p.name for p in person.get_projects_sorted_by_contrib()}
233+
self.assertEqual(names, {"Contrib Visible"})
234+
235+
236+
class LandingBannerVisibilityTests(DatabaseTestCase):
237+
"""get_landing_page_banners drops banners tied to a private project."""
238+
239+
def test_private_project_banner_excluded_but_projectless_kept(self):
240+
from website.models import Banner
241+
from website.views.index import get_landing_page_banners
242+
243+
private = self.make_project(name="Banner Private", is_visible=False)
244+
visible = self.make_project(name="Banner Visible", is_visible=True)
245+
private_banner = Banner.objects.create(
246+
title="Private Banner", landing_page=True, favorite=True, project=private
247+
)
248+
visible_banner = Banner.objects.create(
249+
title="Visible Banner", landing_page=True, favorite=True, project=visible
250+
)
251+
projectless_banner = Banner.objects.create(
252+
title="Projectless Banner", landing_page=True, favorite=True
253+
)
254+
255+
returned = set(get_landing_page_banners(10))
256+
self.assertIn(visible_banner, returned)
257+
self.assertIn(projectless_banner, returned)
258+
self.assertNotIn(private_banner, returned)
259+
260+
261+
class ProjectListingUmbrellaFilterVisibilityTests(DatabaseTestCase):
262+
"""The umbrella filter counts/names only publicly-visible projects."""
263+
264+
def test_private_project_excluded_from_umbrella_map(self):
265+
from website.models import ProjectUmbrella
266+
umbrella = ProjectUmbrella.objects.create(
267+
name="Accessibility", short_name="a11y"
268+
)
269+
for name, vis in [("U Visible", True), ("U Private", False)]:
270+
project = self.make_project(
271+
name=name, with_thumbnail=True, is_visible=vis,
272+
start_date=date(2020, 1, 1),
273+
)
274+
project.project_umbrellas.add(umbrella)
275+
pub = self.make_publication(title=f"Pub {name}")
276+
pub.projects.add(project)
277+
278+
response = self.client.get(reverse("website:projects"))
279+
umbrella_map = response.context["map_project_umbrella_to_projects"]
280+
self.assertEqual(umbrella_map.get("a11y"), ["U Visible"])
281+
282+
283+
class NewsItemRelatedProjectsVisibilityTests(DatabaseTestCase):
284+
"""The news item page lists only publicly-visible related projects."""
285+
286+
def test_private_related_project_hidden(self):
287+
visible = self.make_project(name="News Visible Proj", is_visible=True,
288+
start_date=date(2020, 1, 1))
289+
private = self.make_project(name="News Private Proj", is_visible=False,
290+
start_date=date(2020, 1, 1))
291+
news = self.make_news_item(title="A Discovery")
292+
news.project.add(visible, private)
293+
294+
response = self.client.get(
295+
reverse("website:news_item_by_id", kwargs={"id": news.id})
296+
)
297+
self.assertEqual(response.status_code, 200)
298+
self.assertContains(response, "News Visible Proj")
299+
self.assertNotContains(response, "News Private Proj")

website/views/index.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,12 +95,17 @@ def index(request):
9595
def get_landing_page_banners(max_num_banners=5):
9696
# Get favorite banners that should appear on the landing page. Order by recency.
9797
# The "?" allows us to randomize the order of banners added on the same day
98-
fav_banners = list(Banner.objects.filter(favorite=True, landing_page=True).order_by('-date_added'))
98+
# Exclude banners tied to a private project (#1300) so a hidden project
99+
# isn't named (with a now-404 link) in the landing carousel. Banners with
100+
# no project, or a visible project, are unaffected.
101+
fav_banners = list(Banner.objects.filter(favorite=True, landing_page=True)
102+
.exclude(project__is_visible=False).order_by('-date_added'))
99103
random.shuffle(fav_banners)
100104
banners = fav_banners[:max_num_banners]
101105

102106
if len(banners) < max_num_banners:
103107
other_banners = list(Banner.objects.filter(landing_page=True)
108+
.exclude(project__is_visible=False)
104109
.exclude(id__in=[b.id for b in banners]) # exclude banners in original list
105110
.order_by('-date_added', '?')[:max_num_banners-len(banners)])
106111
random.shuffle(other_banners)

website/views/news_item.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,14 @@ def news_item(request, slug=None, id=None):
4040

4141

4242
excluded_ids = list(recent_ml_news.values_list('id', flat=True))
43-
related_projects = cur_news_item.project.all()
43+
# All projects tied to this news item drive the "related news" lookup below;
44+
# only the publicly-visible ones are shown to the reader (#1300).
45+
all_related_projects = cur_news_item.project.all()
46+
related_projects = all_related_projects.filter(is_visible=True)
4447
recent_news_about_projects_mentioned = (News.objects
4548
.exclude(id=cur_news_item.id) # exclude the current news item
4649
.exclude(id__in=excluded_ids) # don't want to repeat
47-
.filter(project__in=related_projects)
50+
.filter(project__in=all_related_projects)
4851
.order_by('-date').distinct()[:MAX_RECENT_NEWS_ITEMS_BY_AUTHOR])
4952

5053

website/views/project.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,9 @@ def project(request, project_name):
7878
# Get all candidates first
7979
related_project_candidates = project.get_related_projects_by_umbrella(match_all_umbrellas=True)
8080

81-
# Filter using Python list comprehension to ensure the attribute exists and is not empty
82-
# This matches the logic used in your template: {% if related_project.gallery_image %}
83-
related_projects = [p for p in related_project_candidates if p.gallery_image][:5]
81+
# Only surface related projects that are publicly visible (#1300) and have a
82+
# thumbnail (the related-project cards render gallery_image).
83+
related_projects = [p for p in related_project_candidates if p.is_visible and p.gallery_image][:5]
8484

8585
# related_projects_by_pub = project.get_related_projects_by_pub()
8686
# _logger.debug(f"Related projects by publication: {related_projects_by_pub}")

website/views/project_listing.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,16 @@ def project_listing(request):
4141
# Now get all project umbrellas for interactive project filtering
4242
map_project_umbrella_to_projects = {}
4343

44+
# Only count/list publicly-visible projects so private projects (#1300)
45+
# don't inflate the filter counts or leak their names into the page.
4446
project_umbrellas_with_projects = (ProjectUmbrella.objects.annotate(
45-
num_projects=Count('project')).filter(num_projects__gt=0)) # Get all project umbrellas with at least one project
47+
num_projects=Count('project', filter=Q(project__is_visible=True)))
48+
.filter(num_projects__gt=0)) # Get all project umbrellas with at least one visible project
4649

4750
# Iterate over the queryset
4851
for project_umbrella in project_umbrellas_with_projects:
49-
# Get the list of associated Project instances
50-
projects = project_umbrella.project_set.all()
52+
# Get the list of associated, publicly-visible Project instances
53+
projects = project_umbrella.project_set.filter(is_visible=True)
5154
map_project_umbrella_to_projects[project_umbrella.short_name] = [project.name for project in projects]
5255

5356
# Sort the dictionary by project count

0 commit comments

Comments
 (0)