Skip to content
Merged
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
5 changes: 3 additions & 2 deletions .pa11yci.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@
"http://website:8000/people/",
"http://website:8000/talks/",
"http://website:8000/news/",
"http://website:8000/member/jonfroehlich/"
"http://website:8000/member/jonfroehlich/",
"http://website:8000/project/sidewalk/"
],
"urls": [
"http://website:8000/news/"
"http://website:8000/project/sidewalk/"
]
}
10 changes: 5 additions & 5 deletions docker-compose-local-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,21 @@
# 3. a11y - An accessibility testing container using Pa11y + Axe
#
# Usage:
# docker-compose -f docker-compose-local-dev.yml up
# docker compose -f docker-compose-local-dev.yml up
#
# Access checks:
# docker-compose -f docker-compose-local-dev.yml --profile testing run --rm a11y
# To run accessibility checks, you can edit .pa11yci.json and then run:
# docker compose -f docker-compose-local-dev.yml --profile testing run --rm a11y
#
# Access check with report generation:
# docker-compose -f docker-compose-local-dev.yml --profile testing run --rm a11y sh -c "
# docker compose -f docker-compose-local-dev.yml --profile testing run --rm a11y sh -c "
# npm install -g pa11y-ci &&
# pa11y-ci --config /workspace/.pa11yci.json --json | tee /workspace/a11y-report.json
# "
#
# After running, the website is available at: http://localhost:8571
#
# To stop:
# docker-compose down
# docker compose down
#
# =============================================================================

Expand Down
4 changes: 2 additions & 2 deletions makeabilitylab/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,8 @@
ALLOWED_HOSTS = ['*']

# Makeability Lab Global Variables, including Makeability Lab version
ML_WEBSITE_VERSION = "2.1.1" # Keep this updated with each release and also change the short description below
ML_WEBSITE_VERSION_DESCRIPTION = "Updated project page accessibility"
ML_WEBSITE_VERSION = "2.1.2" # Keep this updated with each release and also change the short description below
ML_WEBSITE_VERSION_DESCRIPTION = "Fixed a number of bugs, including project ordering on member pages"
DATE_MAKEABILITYLAB_FORMED = datetime.date(2012, 1, 1) # Date Makeability Lab was formed
MAX_BANNERS = 7 # Maximum number of banners on a page

Expand Down
134 changes: 65 additions & 69 deletions website/models/person.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
from django.db import models
from django.dispatch import receiver
from django.db.models.signals import pre_delete, post_save, m2m_changed, post_delete
from django.db.models.signals import pre_delete
from website.models.publication import PubType
from website.models.position import Role, Title
from website.models.project_role import ProjectRole
from django.core.files import File
import website.utils.fileutils as ml_fileutils

from django.db.models import F, Q, Sum, ExpressionWrapper, fields
from django.db.models.functions import Coalesce
from django.conf import settings

from django.db.models import Count, Max, Value, F, Q, Sum, ExpressionWrapper, fields
from django.utils import timezone
from django.db.models.functions import Coalesce

Expand All @@ -20,7 +23,7 @@
from uuid import uuid4 # for generating unique filenames

import re
from datetime import date, datetime, timedelta
from datetime import date, timedelta

from image_cropping import ImageRatioField

Expand Down Expand Up @@ -234,11 +237,11 @@ def get_total_time_on_project(self, project):
Returns the total time as a timedelta this person has worked on the given project.
If the end_date is None, the current date/time is used.
"""
print(f"person {self}, project {project}")
_logger.debug(f"person {self}, project {project}")
# Get the roles this person has had on the given project
roles = ProjectRole.objects.filter(person=self, project=project)

print(f"person {self}, roles {roles}")
_logger.debug(f"person {self}, roles {roles}")
# If there are no roles, return a timedelta of zero duration
if not roles.exists():
return timedelta()
Expand All @@ -261,7 +264,7 @@ def get_total_time_on_project(self, project):
)
).aggregate(total_time=Sum('time_worked'))['total_time']

print(f"person {self}, total_time_worked", total_time_worked)
_logger.debug(f"person {self}, total_time_worked {total_time_worked}")

# Returns a timedelta object, which is part of Python’s datetime module and it represents
# a duration, or the difference between two dates or times.
Expand Down Expand Up @@ -306,8 +309,12 @@ def is_active(self):
@cached_property
def has_started(self):
"""Returns True if person has started in the lab. False otherwise."""
return self.get_latest_position.has_started()

latest_position = self.get_latest_position
if latest_position is None:
return False
else:
return latest_position.has_started()

def get_total_time_in_role(self, role):
"""Returns the total time as in the specified role across all positions as a DurationField"""
duration = ExpressionWrapper(Coalesce(F('end_date'), date.today()) - F('start_date'), output_field=fields.DurationField())
Expand Down Expand Up @@ -406,11 +413,9 @@ def get_earliest_position_in_role(self, role, contiguous_constraint=True):
next_position = cur_position
elif (next_position.start_date - cur_position.end_date) <= max_time_gap:
time_gap = (next_position.start_date - cur_position.end_date)
# print("Met minimum time gap: gap= {} max_gap={}".format(time_gap, max_time_gap))
next_position = cur_position
else:
time_gap = (next_position.start_date - cur_position.end_date)
# print("Exceeded minimum time gap: gap= {} max_gap={}".format(time_gap, max_time_gap))
break

return next_position
Expand Down Expand Up @@ -474,9 +479,9 @@ def get_full_name(self, include_middle=True):
:return: the person's full name as a string
"""
if self.middle_name and include_middle:
return u"{0} {1} {2}".format(self.first_name, self.middle_name, self.last_name)
return f"{self.first_name} {self.middle_name} {self.last_name}"
else:
return u"{0} {1}".format(self.first_name, self.last_name)
return f"{self.first_name} {self.last_name}"

get_full_name.short_description = "Full Name"
get_full_name.admin_order_field = 'first_name' # Allows column order sorting based on full name
Expand Down Expand Up @@ -540,76 +545,67 @@ def get_projects(self):

def get_mentees(self, randomize=False):
"""
Returns a list of all students this person has mentored
Returns a QuerySet of all students this person has mentored.
Uses the reverse relation from Position.grad_mentor.
"""
grad_mentors = Position.objects.filter(grad_mentor=self).values('person')
mentees = Person.objects.filter(id__in=grad_mentors)
# Use the reverse relation 'Grad_Mentor' from Position model
mentees = Person.objects.filter(position__grad_mentor=self).distinct()

# Randomize the order of the mentees if randomize is True
if randomize:
mentees = mentees.order_by('?')

return mentees

def get_grad_mentors(self):
"""
Retrieve a list of grad mentors for the current person instance.

This method filters the Person objects to find those who are listed as
grad mentors for the current person in the Position model. It ensures
that each mentor is listed only once using the distinct() method.

Retrieve a QuerySet of grad mentors for the current person instance.

Returns:
QuerySet: A QuerySet of Person objects who are grad mentors for the current person.
QuerySet: Person objects who are grad mentors for the current person.
"""
positions = Position.objects.filter(person=self)
grad_mentors = positions.values('grad_mentor')
return Person.objects.filter(id__in=grad_mentors)
# Get all grad mentors from this person's positions
return Person.objects.filter(
id__in=self.position_set.exclude(
grad_mentor__isnull=True
).values_list('grad_mentor', flat=True)
).distinct()

def get_projects_sorted_by_contrib(self, filter_out_projs_with_zero_pubs=True):
"""Returns a set of all projects this person is involved in ordered by number of pubs"""
map_project_name_to_tuple = dict() # tuple is (count, most_recent_pub_date, project)
#publications = self.publication_set.order_by('-date')

# Go through all the projects by this person and track how much
# they've contributed to each one (via publication)
#print("******{}*******".format(self.get_full_name()))
for pub in self.publication_set.all():
for proj in pub.projects.all():
#print("pub", pub, "proj", proj)
if proj.name not in map_project_name_to_tuple:
most_recent_date = proj.start_date
if most_recent_date is None:
most_recent_date = pub.date
if most_recent_date is None:
most_recent_date = datetime.date(2012, 1, 1) # when the lab was founded

map_project_name_to_tuple[proj.name] = (0, most_recent_date, proj)

tuple_cnt_proj = map_project_name_to_tuple[proj.name]
most_recent_date = tuple_cnt_proj[1]
if pub.date is not None and pub.date > most_recent_date:
most_recent_date = pub.date

map_project_name_to_tuple[proj.name] = (tuple_cnt_proj[0] + 1, # pub cnt
most_recent_date, # most recent pub date
tuple_cnt_proj[2]) # project

list_tuples = list([tuple_cnt_proj for tuple_cnt_proj in map_project_name_to_tuple.values()])
list_tuples_sorted = sorted(list_tuples, key=lambda t: (t[0], t[1]), reverse=True)

#print("list_tuples_sorted", list_tuples_sorted)

ordered_projects = []
if len(list_tuples_sorted) > 0:
list_cnts, list_dates, ordered_projects = zip(*list_tuples_sorted)

if len(ordered_projects) <= 0 and not filter_out_projs_with_zero_pubs:
# if a person hasn't published but is still on projects
# default to this
ordered_projects = self.get_projects()

return ordered_projects
"""
Returns projects involving this person, sorted by the number of
publications and the date of the most recent publication.
"""
Project = apps.get_model('website', 'Project')

# Start with projects where this person has a role
projects_qs = Project.objects.filter(
projectrole__person=self
).annotate(
# Count publications by this person on each project
pub_count=Count(
'publication',
filter=Q(publication__authors=self),
distinct=True
),
# Get most recent publication date by this person
most_recent_pub_date=Max(
'publication__date',
filter=Q(publication__authors=self)
)
).annotate(
# Fallback date logic: pub date > project start > lab founding
sort_date=Coalesce(
'most_recent_pub_date',
'start_date',
Value(settings.DATE_MAKEABILITYLAB_FORMED)
)
).order_by('-pub_count', '-sort_date').distinct()

# Filter out projects with zero publications if requested
if filter_out_projs_with_zero_pubs:
projects_qs = projects_qs.filter(pub_count__gt=0)

return projects_qs


def __str__(self):
Expand Down
44 changes: 37 additions & 7 deletions website/models/position.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ class Position(models.Model):
Title.UNKNOWN: 11
}

# BETTER - Use class constant:
PROFESSOR_TITLES = {Title.FULL_PROF, Title.ASSOCIATE_PROF, Title.ASSISTANT_PROF}
GRAD_STUDENT_TITLES = {Title.MS_STUDENT, Title.PHD_STUDENT}

def save(self, *args, **kwargs):
# Save the Position instance first
super(Position, self).save(*args, **kwargs)
Expand Down Expand Up @@ -150,15 +154,12 @@ def is_member(self):
return self.role == Role.MEMBER

def is_professor(self):
"""Returns true if professor"""
return (self.title == Title.FULL_PROF or
self.title == Title.ASSOCIATE_PROF or
self.title == Title.ASSISTANT_PROF)
"""Returns True if this position is a professor."""
return self.title in self.PROFESSOR_TITLES

def is_grad_student(self):
"""Returns true if grad student"""
return (self.title == Title.MS_STUDENT or
self.title == Title.PHD_STUDENT)
"""Returns True if this position is a grad student."""
return self.title in self.GRAD_STUDENT_TITLES

def is_high_school(self):
"""Returns true if high school student"""
Expand Down Expand Up @@ -207,6 +208,35 @@ def __str__(self):
return "Name={}, Role={}, Title={}, Start={} End={}".format(
self.person.get_full_name(), self.role, self.title, self.start_date, self.end_date)

# In position.py, add to the Position class:

@staticmethod
def get_indefinite_article_for_title(title):
"""
Returns the appropriate indefinite article ('a' or 'an') for a given title.

Args:
title: Title enum value or string

Returns:
'a' or 'an' depending on whether the title starts with a vowel sound

Example:
>>> Position.get_indefinite_article(Title.UGRAD)
'an'
>>> Position.get_indefinite_article(Title.PHD_STUDENT)
'a'
"""
# Titles that require "an" (start with vowel sound)
titles_needing_an = {
Title.UGRAD, # "Undergrad"
Title.MS_STUDENT, # "MS Student" (M sounds like "em")
Title.ASSISTANT_PROF, # "Assistant Professor"
Title.ASSOCIATE_PROF, # "Associate Professor"
}

return "an" if title in titles_needing_an else "a"

@staticmethod
def get_sorted_abstracted_titles():
"""Static method returns a sorted list of abstracted title names"""
Expand Down
1 change: 1 addition & 0 deletions website/static/website/css/design-tokens.css
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@
--space-4: 1rem; /* 16px */
--space-5: 1.25rem; /* 20px */
--space-6: 1.5rem; /* 24px */
--space-7: 1.75rem; /* 28px */
--space-8: 2rem; /* 32px */
--space-10: 2.5rem; /* 40px */
--space-12: 3rem; /* 48px */
Expand Down
10 changes: 7 additions & 3 deletions website/static/website/css/project.css
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@
@media (min-width: 992px) {
.project-layout {
grid-template-columns: 1fr 320px;
gap: var(--space-8);
gap: var(--space-12);
}
}

Expand Down Expand Up @@ -261,7 +261,7 @@

@media (min-width: 992px) {
.project-sidebar-section {
margin-bottom: var(--space-6);
margin-bottom: var(--space-7);
}
}

Expand All @@ -271,7 +271,7 @@
============================================================================= */

.project-sidebar-header {
margin: 0 0 var(--space-2) 0;
margin: var(--space-7) 0 var(--space-2) 0;
font-family: var(--font-family-primary);
font-size: var(--font-size-sm);
font-weight: var(--font-weight-semibold);
Expand All @@ -280,6 +280,10 @@
letter-spacing: 0.03em;
}

.project-sidebar-header.first-section{
margin-top: var(--space-5);
}


/* =============================================================================
SIDEBAR TEXT & LINKS
Expand Down
4 changes: 2 additions & 2 deletions website/templates/website/project.html
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
- has_videos_beyond_featured_video: Boolean
- talks: QuerySet of Talk objects

@version 2.0.0 - Accessibility and design token refactor
@version 2.0.1 - Updated sidebar spacing
@author Makeability Lab
================================================================================
{% endcomment %}
Expand Down Expand Up @@ -284,7 +284,7 @@ <h2 id="project-sidebar-heading" class="sr-only">Project Information</h2>

<!-- Date -->
<section class="project-sidebar-section">
<h3 class="project-sidebar-header">Date</h3>
<h3 class="project-sidebar-header first-section">Date</h3>
<p class="project-sidebar-text">{{ date_str }}</p>
</section>

Expand Down
Loading