Source code for cdhweb.projects.models

from django.db import models
from django.utils import timezone
from modelcluster.contrib.taggit import ClusterTaggableManager
from modelcluster.fields import ParentalKey
from modelcluster.models import ClusterableModel
from taggit.models import TaggedItemBase
from wagtail.admin.edit_handlers import (
    FieldPanel,
    FieldRowPanel,
    InlinePanel,
    StreamFieldPanel,
)
from wagtail.core.models import Page, PageManager, PageQuerySet
from wagtail.images.edit_handlers import ImageChooserPanel
from wagtail.search import index

from cdhweb.pages.models import BasePage, DateRange, LandingPage, LinkPage, RelatedLink
from cdhweb.people.models import Person


[docs]class ProjectQuerySet(PageQuerySet):
[docs] def highlighted(self): """return projects that are marked as highlighted""" return self.filter(highlight=True)
def _current_grant_query(self): """QuerySet filter to find projects with a current grant, based on start date before current date and end date after current date or not set. """ today = timezone.now() return models.Q(grants__start_date__lt=today) & ( models.Q(grants__end_date__gt=today) | models.Q(grants__end_date__isnull=True) )
[docs] def current(self): """Projects with a current grant, based on dates""" return self.filter(self._current_grant_query()).distinct()
[docs] def not_current(self): """Projects with no current grant, based on dates""" return self.exclude(self._current_grant_query())
#: grant types that indicate staff or postdoc project staff_postdoc_grants = [ "Staff R&D", "Staff Project", "Postdoctoral Research Project", ]
[docs] def staff_or_postdoc(self): """Staff and postdoc projects, based on grant type""" return self.filter( grants__grant_type__grant_type__in=self.staff_postdoc_grants ).exclude(working_group=True)
[docs] def not_staff_or_postdoc(self): """Exclude staff and postdoc projects, based on grant type""" return self.exclude( grants__grant_type__grant_type__in=self.staff_postdoc_grants ).exclude(working_group=True)
[docs] def working_groups(self): """Include only projects with the working group flag set""" return self.filter(working_group=True)
[docs] def order_by_newest_grant(self): """order by grant start date, most recent grants first; secondary sort by project title""" # NOTE: using annotation to get just the most recent start date # to avoid issues with projects appearing multiple times. return self.annotate(last_start=models.Max("grants__start_date")).order_by( "-last_start", "title" )
# custom manager for wagtail pages, see: # https://docs.wagtail.io/en/stable/topics/pages.html#custom-page-managers ProjectManager = PageManager.from_queryset(ProjectQuerySet)
[docs]class ProjectTag(TaggedItemBase): """Tags for Project pages.""" content_object = ParentalKey( "projects.Project", on_delete=models.CASCADE, related_name="tagged_items" )
[docs]class Project(BasePage, ClusterableModel): """Page type for a CDH sponsored project or working group.""" short_description = models.CharField( max_length=255, blank=True, help_text="Brief tagline for display on project card in browse view", ) highlight = models.BooleanField( default=False, help_text="Include in randomized project display on the home page.", ) cdh_built = models.BooleanField( "CDH Built", default=False, help_text="Project built by CDH Development & Design team.", ) working_group = models.BooleanField( "Working Group", default=False, help_text="Project is a long-term collaborative group associated with the CDH.", ) image = models.ForeignKey( "wagtailimages.image", null=True, blank=True, on_delete=models.SET_NULL, related_name="+", help_text="Image for display on project detail page (optional)", ) thumbnail = models.ForeignKey( "wagtailimages.image", null=True, blank=True, on_delete=models.SET_NULL, related_name="+", help_text="Image for display on project card (optional)", ) members = models.ManyToManyField(Person, through="Membership") tags = ClusterTaggableManager(through=ProjectTag, blank=True) # TODO attachments (#245) # can only be created underneath project landing page parent_page_types = ["projects.ProjectsLandingPage"] # no allowed subpages subpage_types = [] # admin edit configuration content_panels = Page.content_panels + [ FieldRowPanel( ( FieldPanel("highlight"), FieldPanel("cdh_built"), FieldPanel("working_group"), ), "Settings", ), FieldRowPanel( (ImageChooserPanel("thumbnail"), ImageChooserPanel("image")), "Images" ), FieldPanel("short_description"), StreamFieldPanel("body"), InlinePanel("related_links", label="Links"), InlinePanel( "grants", panels=[ FieldRowPanel((FieldPanel("start_date"), FieldPanel("end_date"))), FieldPanel("grant_type"), ], label="Grants", ), InlinePanel( "memberships", panels=[ FieldRowPanel((FieldPanel("start_date"), FieldPanel("end_date"))), FieldPanel("person"), FieldPanel("role"), ], label="Members", ), StreamFieldPanel("attachments"), ] promote_panels = Page.promote_panels + [FieldPanel("tags")] # custom manager/queryset logic objects = ProjectManager() # search fields search_fields = BasePage.search_fields + [ index.SearchField("short_description"), index.RelatedFields( "members", [ index.SearchField("first_name"), index.SearchField("last_name"), ], ), ] def __str__(self): return self.title @property def website_url(self): """URL for this Project's website, if set""" website = self.related_links.filter(type__name="Website").first() if website: return website.url
[docs] def latest_grant(self): """Most recent :class:`Grant` for this Project""" if self.grants.count(): return self.grants.order_by("-start_date").first()
[docs] def current_memberships(self): """:class:`MembershipQueryset` of current members sorted by role""" # NOTE memberships is a FakeQuerySet from modelcluster.ParentalKey when # the page is being previewed in wagtail, so Q lookups are not possible. # see: https://github.com/wagtail/django-modelcluster/issues/121 memberships = Membership.objects.filter(project__pk=self.pk) # uses memberships rather than members so that we can retain role # information attached to the membership today = timezone.now().date() # if the last grant for this project is over, display the team # for that grant period latest_grant = self.latest_grant() if latest_grant and latest_grant.end_date and latest_grant.end_date < today: return memberships.filter(start_date__lte=latest_grant.end_date).filter( models.Q(end_date__gte=latest_grant.start_date) | models.Q(end_date__isnull=True) ) # otherwise, return current members based on date return memberships.filter(start_date__lte=today).filter( models.Q(end_date__gte=today) | models.Q(end_date__isnull=True) )
[docs] def alums(self): """:class:`PersonQueryset` of past members sorted by last name""" # uses people rather than memberships so that we can use distinct() # to ensure people aren't counted multiple times for each grant # and because we don't care about role (always 'alum') return ( self.members.distinct() .exclude(membership__in=self.current_memberships()) .order_by("last_name") )
[docs] def get_sitemap_urls(self, request): """Override sitemap to prioritize projects built by CDH with a website.""" # output is a list of dict; there should only ever be one element. see: # https://docs.wagtail.io/en/stable/reference/contrib/sitemaps.html#urls urls = super().get_sitemap_urls(request=request) if self.website_url and self.cdh_built: urls[0]["priority"] = 0.7 elif self.website_url or self.cdh_built: urls[0]["priority"] = 0.6 return urls
[docs]class GrantType(models.Model): """Model to track kinds of grants""" grant_type = models.CharField(max_length=255, unique=True) def __str__(self): return self.grant_type
[docs]class Grant(DateRange): """A specific grant associated with a project""" project = ParentalKey(Project, on_delete=models.CASCADE, related_name="grants") grant_type = models.ForeignKey(GrantType, on_delete=models.CASCADE) class Meta: ordering = ["start_date", "project"] def __str__(self): return "%s: %s (%s)" % ( self.project.title, self.grant_type.grant_type, self.years, )
[docs]class Role(models.Model): """A role on a project""" title = models.CharField(max_length=255, unique=True) sort_order = models.PositiveIntegerField(default=0, blank=False, null=False) class Meta: ordering = ["sort_order"] def __str__(self): return self.title def __lt__(self, other): # NOTE we need to order Memberships using role sort order by default, # but modelcluster doesn't support ordering via related lookups, so # we can't order by role__sort_order on Membership. Instead we do this. # see: https://github.com/wagtail/django-modelcluster/issues/45 return self.sort_order < other.sort_order
[docs]class Membership(DateRange): """Project membership - joins project, user, and role.""" project = ParentalKey(Project, on_delete=models.CASCADE, related_name="memberships") person = models.ForeignKey(Person, on_delete=models.CASCADE) role = models.ForeignKey(Role, on_delete=models.CASCADE) class Meta: ordering = ("role", "person") # admin edit configuration panels = [ FieldRowPanel((FieldPanel("start_date"), FieldPanel("end_date")), "Dates"), FieldPanel("person"), FieldPanel("role"), FieldPanel("project"), ] def __str__(self): return "%s - %s on %s (%s)" % (self.person, self.role, self.project, self.years)
[docs]class ProjectsLandingPage(LandingPage): """Container page that defines where Project pages can be created.""" # NOTE this page can't be created in the page editor; it is only ever made # via a script or the console, since there's only one. parent_page_types = [] # NOTE the only allowed child page type is a Project; this is so that # Projects made in the admin automatically are created here. subpage_types = [Project, LinkPage] # use the regular landing page template template = LandingPage.template