Skip to content
Permalink
master
Switch branches/tags
Go to file
 
 
Cannot retrieve contributors at this time
from __future__ import absolute_import, unicode_literals
from os import urandom
from base64 import urlsafe_b64encode
from collections import Counter
import datetime
from email.headerregistry import Address
import random
import os.path
import re
import math
from django.contrib.auth.models import User
from django.core import validators
from django.core.exceptions import ObjectDoesNotExist
from django.db import models
from django.db import transaction
from django.forms import ValidationError
from django.shortcuts import redirect
from django.urls import reverse
from django.utils.functional import cached_property
from itertools import chain, groupby
from urllib.parse import urlsplit, urlparse
from ckeditor.fields import RichTextField as CKEditorField
from modelcluster.fields import ParentalKey
from reversion.models import Version
from timezone_field.fields import TimeZoneField
from wagtail.core.models import Orderable
from wagtail.core.models import Page
from wagtail.core.fields import RichTextField
from wagtail.core.fields import StreamField
from wagtail.admin.edit_handlers import FieldPanel
from wagtail.admin.edit_handlers import InlinePanel
from wagtail.core import blocks
from wagtail.admin.edit_handlers import StreamFieldPanel
from wagtail.images.edit_handlers import ImageChooserPanel
from wagtail.images.blocks import ImageChooserBlock
from wagtail.contrib.table_block.blocks import TableBlock
from wagtail.contrib.routable_page.models import RoutablePageMixin, route
from . import email
from .feeds import WagtailFeed
# These constants are used across several different models
# Please be cautious about shorting them,
# as it may mean stored objects are no longer valid.
# When in doubt, use a longer max character length or define a new constant.
SENTENCE_LENGTH=100
PARAGRAPH_LENGTH=800
THREE_PARAGRAPH_LENGTH=3000
EIGHT_PARAGRAPH_LENGTH=8000
TIMELINE_LENGTH=30000
LONG_LEGAL_NAME=800
SHORT_NAME=100
class HomePage(Page):
body = StreamField([
('heading', blocks.CharBlock(template="home/blocks/heading.html")),
('paragraph', blocks.RichTextBlock()),
('image', ImageChooserBlock()),
('logo', ImageChooserBlock(template="home/blocks/logo.html")),
('date', blocks.DateBlock()),
('table', TableBlock(template="home/blocks/table.html")),
('quote', blocks.RichTextBlock(template="home/blocks/quote.html")),
])
content_panels = Page.content_panels + [
StreamFieldPanel('body', classname="full"),
]
class RichTextOnly(Page):
body = RichTextField(blank=True)
content_panels = Page.content_panels + [
FieldPanel('body', classname="full"),
]
class RoundsIndexPage(Page):
intro = RichTextField(blank=True)
content_panels = Page.content_panels + [
FieldPanel('intro', classname="full")
]
class DonatePage(Page):
intro = RichTextField(blank=True, default='<p>Individual donations can be made via PayPal, check, or wire. Donations are tax deductible, and are handled by our 501(c)(3) non-profit parent organization, Software Freedom Conservancy. Individual donations are directed to the Outreachy general fund, unless otherwise specified.</p>')
paypal_text = RichTextField(blank=True, default='<p><strong>PayPal</strong> To donate through PayPal, please click on the "Donate" button below.</p>')
check_text = RichTextField(blank=True, default='<p><strong>Check</strong> We can accept check donations drawn in USD from banks in the USA. Please make the check payable to "Software Freedom Conservancy, Inc." and put "Directed donation: Outreachy" in the memo field. Please mail the check to: <br/><span class="offset1">Software Freedom Conservancy, Inc.</span><br/><span class="offset1">137 Montague ST Ste 380</span><br/><span class="offset1">Brooklyn, NY 11201</span><br/><span class="offset1">USA</span></p>')
wire_text = RichTextField(blank=True, default='<p><strong>Wire</strong> Please write to <a href="mailto:accounting@sfconservancy.org">accounting@sfconservancy.org</a> and include the country of origin of your wire transfer and the native currency of your donation to receive instructions for a donation via wire.</p>')
outro = RichTextField(blank=True, default='<p>If you are a corporation seeking to sponsor Outreachy, please see <a href="https://www.outreachy.org/sponsor/">our sponsor page.</a></p>')
content_panels = Page.content_panels + [
FieldPanel('intro', classname="full"),
FieldPanel('paypal_text', classname="full"),
FieldPanel('check_text', classname="full"),
FieldPanel('wire_text', classname="full"),
FieldPanel('outro', classname="full"),
]
class StatsRoundFifteen(Page):
unused = RichTextField(blank=True)
content_panels = Page.content_panels + [
FieldPanel('unused', classname="full"),
]
class BlogIndex(RoutablePageMixin, Page):
feed_generator = WagtailFeed()
@route(r'^feed/$')
def feed(self, request):
return self.feed_generator(request, self)
def get_context(self, request, *args, **kwargs):
context = super(BlogIndex, self).get_context(request, *args, **kwargs)
context['feed'] = self.feed_generator.get_feed(self, request)
return context
# All dates in RoundPage below, if an exact time matters, actually represent
# the given date at 4PM UTC.
DEADLINE_TIME = datetime.time(hour=16, tzinfo=datetime.timezone.utc)
def get_deadline_date_for(dt):
"""
Takes a timezone-aware datetime and returns the date which is
comparable to dates in RoundPage deadlines. If the datetime has
not reached the deadline time of 4PM UTC, then this is the
previous day's date.
This is handy for comparing an arbitrary point in time against
any of the deadlines in RoundPage, like the following example to
find rounds where the intern announcement deadline has passed but
the internship end deadline has not:
>>> import datetime
>>> now = datetime.datetime.now(datetime.timezone.utc)
>>> today = get_deadline_date_for(now)
>>> RoundPage.objects.filter(
... internannounce__lte=today,
... internends__gt=today,
... ) # doctest: +ELLIPSIS
<PageQuerySet [...]>
"""
if dt.timetz() < DEADLINE_TIME:
return dt.date() - datetime.timedelta(days=1)
return dt.date()
class Deadline(datetime.date):
"""
An extension of datetime.date which adds extra methods that are useful when
the date represents a deadline. This class also stores an extra ``today``
value that's used to determine whether the deadline has already passed.
"""
@classmethod
def at(cls, base, today):
"""
This is the only valid constructor for this type, because other methods
won't set self._today.
"""
new = cls(base.year, base.month, base.day)
new._today = today
return new
def __add__(self, other):
# In Python 3.7, date.__add__ always returns a date. In 3.8 it returns
# the same type as self, e.g. Deadline, but without setting _today.
# Either way, extracting the year/month/day and building a new Deadline
# object produces the right result.
new = super(Deadline, self).__add__(other)
return Deadline.at(new, self._today)
def __sub__(self, other):
# Override to make sure that the above implementation of __add__ gets
# used.
if isinstance(other, datetime.timedelta):
return self + -other
return super(Deadline, self).__sub__(other)
def deadline(self):
"""
Returns this deadline with time and timezone set from
``DEADLINE_TIME``.
"""
return datetime.datetime.combine(self, DEADLINE_TIME)
def has_passed(self):
"""
Returns whether this deadline is in the past.
"""
return self <= self._today
class NoDeadline(object):
"""
Like ``Deadline``, but for situations where there is no expiration date.
This class can't act like a ``date`` or implement the ``deadline`` method.
But it does work if the caller only needs to know whether the deadline has
passed yet, because the answer is always: no, it hasn't.
There's no need to construct instances of this class. Instead of using
``NoDeadline()``, just use ``NoDeadline``.
"""
@staticmethod
def has_passed():
return False
class AugmentDeadlines(object):
"""
Mixin to transparently make date fields return Deadline objects instead.
"""
def __init__(self, *args, **kwargs):
now = datetime.datetime.now(DEADLINE_TIME.tzinfo)
self.today = get_deadline_date_for(now)
super(AugmentDeadlines, self).__init__(*args, **kwargs)
def __getattribute__(self, name):
"""
Extend all fields that are plain dates to return an instance of
Deadline instead, where the Deadline's idea of ``today`` is set from
``self.today``. For example:
>>> rp = RoundPage(internstarts=datetime.date(2019, 1, 1))
>>> rp.internstarts.deadline()
datetime.datetime(2019, 1, 1, 16, 0, tzinfo=datetime.timezone.utc)
>>> rp.today = datetime.date(2018, 12, 1)
>>> rp.internstarts.has_passed()
False
>>> rp.today = datetime.date(2019, 1, 1)
>>> rp.internstarts.has_passed()
True
Any Python class can override what ``obj.field`` means by implementing
this method, which then gets called like
``obj.__getattribute__("field")``. See:
https://docs.python.org/3/reference/datamodel.html#object.__getattribute__
"""
# Call the default implementation first...
value = super(AugmentDeadlines, self).__getattribute__(name)
if name != "today" and type(value) is datetime.date:
# This is a plain date; augment it with the Deadline extras. Note
# that accessing ``self.today`` triggers a recursive call to this
# function with name="today", so we have to be very careful: that
# call must not reach this same statement or it will recurse
# forever.
return Deadline.at(value, self.today)
# This was not a date, so return it as-is.
return value
class RoundPage(AugmentDeadlines, Page):
roundnumber = models.IntegerField()
pingnew = models.DateField("Date to start pinging new orgs")
pingold = models.DateField("Date to start pinging past orgs")
orgreminder = models.DateField("Date to remind orgs to submit their home pages")
landingdue = models.DateField("Date community landing pages are due")
coupon_code = models.CharField(blank=True, max_length=255, verbose_name='Coupon code for the book "Forge Your Future with Open Source"')
minimum_days_free_for_students = models.IntegerField(
validators=[validators.MinValueValidator(0)],
default=49,
)
minimum_days_free_for_non_students = models.IntegerField(
validators=[validators.MinValueValidator(0)],
default=49,
)
initial_applications_open = models.DateField("Date initial applications open")
outreachy_chat = models.DateTimeField("Date and time of the Outreachy Twitter chat")
outreachy_chat_timezone_url = models.URLField(blank=True, verbose_name="URL to display timezone conversions for the Outreachy Twitter chat time")
initial_applications_close = models.DateField("Date initial applications close")
pick_a_project_blog_url = models.URLField(blank=True, verbose_name="URL of the blog on how to pick a project")
contributions_open = models.DateField("Date contributions open")
contributions_close = models.DateField("Date contributions close")
lateorgs = models.DateField("Last date to add community landing pages")
lateprojects = models.DateField("Last date to add projects")
mentor_intern_selection_deadline = models.DateField("Date mentors must select their intern by")
coordinator_funding_deadline = models.DateField("Date coordinators must mark funding sources for interns by")
internapproval = models.DateField("Date interns are approved by the Outreachy organizers")
internannounce = models.DateField("Date interns are announced")
internstarts = models.DateField("Date internships start")
week_two_chat_text_date = models.DateTimeField("Date and time of outreachy week two chat (text only)")
week_two_chat_text_url = models.URLField(blank=True, verbose_name="URL of the real-time text chat")
week_four_chat_text_date = models.DateTimeField("Date and time of Outreachy week four chat about what we're stuck on")
week_four_stuck_chat_url = models.URLField(blank=True, verbose_name="URL of the week four chat on what we're stuck on")
week_six_chat_text_date = models.DateTimeField("Date and time of Outreachy week six chat to explain why your project passion to a newcomer")
week_six_audience_chat_url = models.URLField(blank=True, verbose_name="URL of the week six chat to explain your project to a newcomer")
week_eight_chat_text_date = models.DateTimeField("Date and time of Outreachy week eight chat to talk about difficulties scoping project tasks")
week_eight_timeline_chat_url = models.URLField(blank=True, verbose_name="URL of the week eight chat to talk about project timeline modifications")
week_ten_chat_text_date = models.DateTimeField("Date and time of Outreachy week ten chat to talk about career opportunities")
week_ten_career_chat_url = models.URLField(blank=True, verbose_name="URL of the week ten chat to talk about career opportunities")
week_twelve_chat_text_date = models.DateTimeField("Date and time of Outreachy week twelve chat on interviewing")
week_twelve_interviewing_chat_url = models.URLField(blank=True, verbose_name="URL of the week twelve chat on interviewing")
resume_reviewer_name = models.CharField(blank=True, max_length=255, verbose_name='Name of the person doing resume review during week 12')
resume_reviewer_email = models.EmailField(blank=True, verbose_name='Email address of the person doing resume review during week 12')
week_fourteen_chat_text_date = models.DateTimeField(verbose_name="Date and time of Outreachy week fourteen chat to wrap up the Outreachy internship")
week_fourteen_wrapup_chat_url = models.URLField(blank=True, verbose_name="URL of the week fourteen chat to wrap up the Outreachy internship")
tax_form_deadline = models.DateField("Date tax forms must be received by in order to have on-time initial payments")
initialfeedback = models.DateField("Date initial feedback is due")
initial_payment_date = models.DateField("Date initial payment will be received by")
initialpayment = models.IntegerField(default=1000)
midfeedback = models.DateField("Date mid-point feedback is due")
midpoint_payment_date = models.DateField("Date mid-point payment will be received by")
midpayment = models.IntegerField(default=2000)
internends = models.DateField("Date internships end")
finalfeedback = models.DateField("Date final feedback is due")
final_payment_date = models.DateField("Date final payment will be received by")
finalpayment = models.IntegerField(default=2500)
sponsordetails = RichTextField(default='<p>Outreachy is hosted by the <a href="https://sfconservancy.org/">Software Freedom Conservancy</a> with special support from Red Hat, GNOME, and <a href="http://otter.technology">Otter Tech</a>. We invite companies and free and open source communities to sponsor internships in the next round.</p>')
content_panels = Page.content_panels + [
FieldPanel('roundnumber'),
FieldPanel('pingnew'),
FieldPanel('pingold'),
FieldPanel('orgreminder'),
FieldPanel('landingdue'),
FieldPanel('initial_applications_open'),
FieldPanel('initial_applications_close'),
FieldPanel('pick_a_project_blog_url'),
FieldPanel('contributions_open'),
FieldPanel('contributions_close'),
FieldPanel('lateorgs'),
FieldPanel('lateprojects'),
FieldPanel('mentor_intern_selection_deadline'),
FieldPanel('coordinator_funding_deadline'),
FieldPanel('internapproval'),
FieldPanel('internannounce'),
FieldPanel('internstarts'),
FieldPanel('initialfeedback'),
FieldPanel('midfeedback'),
FieldPanel('internends'),
FieldPanel('finalfeedback'),
FieldPanel('sponsordetails', classname="full"),
FieldPanel('initialpayment'),
FieldPanel('midpayment'),
FieldPanel('finalpayment'),
FieldPanel('week_two_chat_text_date'),
FieldPanel('week_two_chat_text_url'),
FieldPanel('week_four_chat_text_date'),
FieldPanel('week_four_stuck_chat_url'),
FieldPanel('week_six_chat_text_date'),
FieldPanel('week_six_audience_chat_url'),
FieldPanel('week_eight_chat_text_date'),
FieldPanel('week_eight_timeline_chat_url'),
FieldPanel('week_ten_chat_text_date'),
FieldPanel('week_ten_career_chat_url'),
FieldPanel('week_twelve_chat_text_date'),
FieldPanel('week_twelve_interviewing_chat_url'),
FieldPanel('week_fourteen_chat_text_date'),
FieldPanel('week_fourteen_wrapup_chat_url'),
]
def official_name(self):
return(self.internstarts.strftime("%B %Y") + " to " + self.internends.strftime("%B %Y") + " Outreachy internships")
def project_soft_deadline(self):
return self.lateprojects - datetime.timedelta(days=7)
def internship_week_1_email_date(self):
return(self.internstarts)
def internship_week_3_email_date(self):
return self.internstarts + datetime.timedelta(days=7*(3-1))
def internship_week_5_email_date(self):
return self.internstarts + datetime.timedelta(days=7*(5-1))
def internship_week_7_email_date(self):
return self.internstarts + datetime.timedelta(days=7*(7-1))
def internship_week_9_email_date(self):
return self.internstarts + datetime.timedelta(days=7*(9-1))
def internship_week_11_email_date(self):
return self.internstarts + datetime.timedelta(days=7*(11-1))
def internship_week_13_email_date(self):
return self.internstarts + datetime.timedelta(days=7*(13-1))
def intern_agreement_deadline(self):
return(self.internannounce + datetime.timedelta(days=5))
def intern_initial_feedback_opens(self):
return(self.initialfeedback - datetime.timedelta(days=7))
def intern_midpoint_feedback_opens(self):
return(self.midfeedback - datetime.timedelta(days=7))
def intern_not_started_deadline(self):
return(self.initialfeedback - datetime.timedelta(days=1))
def intern_sfc_initial_payment_notification_deadline(self):
return(self.initialfeedback)
def initial_stipend_payment_deadline(self):
return self.initial_payment_date
def midpoint_stipend_payment_deadline(self):
return self.midpoint_payment_date
def final_stipend_payment_deadline(self):
return self.final_payment_date
# There is a concern about paying interns who are in a country
# where they are not eligible to work in (usually due to visa restrictions).
# We need to ask interns whether they will be traveling after their internship
# when they would normally be paid. Internships may be extended by up to five weeks.
# Payment isn't instantaneous, but this is a little better than just saying
# "Are you eligible to work in all the countries you are residing in
# during the internship period?"
def sfc_payment_last_date(self):
return self.internends + datetime.timedelta(days=7*5)
# Interns get a five week extension at most.
def has_internship_ended(self):
return (self.internends + datetime.timedelta(days=7 * 5)).has_passed()
# Outreachy internships can be extended for up to five weeks past the official end date.
# In some cases, we've changed or added an intern after the official announcement date.
# The very latest we could do that would be five weeks after the official start date.
def has_last_day_to_add_intern_passed(self):
return (self.internstarts + datetime.timedelta(days=5 * 7)).has_passed()
def gsoc_round(self):
# The internships would start before August
# for rounds aligned with GSoC
# GSoC traditionally starts either in May or June
return(self.internstarts.month < 8)
def number_approved_communities_with_projects(self):
return self.participation_set.approved().filter(project__isnull=False).distinct().count()
def number_approved_projects(self):
return Project.objects.filter(project_round__participating_round=self,
approval_status=ApprovalStatus.APPROVED,
project_round__approval_status=ApprovalStatus.APPROVED).distinct().count()
def get_new_projects(self):
# Find all approved projects
projects = Project.objects.filter(project_round__participating_round=self,
approval_status=ApprovalStatus.APPROVED,
project_round__approval_status=ApprovalStatus.APPROVED).order_by('project_round__community__name').distinct()
new_projects = []
now = datetime.datetime.now(datetime.timezone.utc)
week_ago = now - datetime.timedelta(weeks=1)
# Find all projects that were in the submitted state within the last week
for p in projects:
versions = Version.objects.get_for_object(p)
for v in versions:
if v.revision.date_created < week_ago:
break
if v.field_dict['approval_status'] == ApprovalStatus.PENDING:
new_projects.append(p)
break
return new_projects
# for p in new_projects:
# print(p.project_round.community.name, '"' + p.short_title + '" - ', ', '.join([s.skill for s in p.projectskill_set.all()]))
# print("New @outreachy internship project:", p.project_round.community.name, '"' + p.short_title + '" - ', ', '.join([s.skill for s in p.projectskill_set.all()]), 'https://www.outreachy.org/apply/project-selection/#' + p.project_round.community.slug + '-' + p.slug)
def number_funded_interns(self):
participations = self.participation_set.approved().filter(project__isnull=False).distinct()
funded = 0
for p in participations:
funded += p.interns_funded()
return funded
def is_coordinator(self, user):
return CoordinatorApproval.objects.filter(
coordinator__account=user,
approval_status=ApprovalStatus.APPROVED,
community__participation__approval_status=ApprovalStatus.APPROVED,
community__participation__participating_round=self,
).exists()
def is_mentor(self, user):
try:
return user.comrade.get_mentored_projects().approved().filter(
project_round__participating_round=self,
project_round__approval_status=ApprovalStatus.APPROVED,
).exists()
except Comrade.DoesNotExist:
return False
def is_reviewer(self, user):
return self.applicationreviewer_set.approved().filter(
comrade__account=user,
).exists()
def print_approved_project_list(self):
projects = Project.objects.filter(project_round__participating_round=self, approval_status=ApprovalStatus.APPROVED, project_round__approval_status=ApprovalStatus.APPROVED).order_by('project_round__community__name').distinct()
for p in projects:
skills = p.required_skills() | p.preferred_skills()
print("<p><a href='https://www.outreachy.org/{}'>{}</a>, ".format(p.get_landing_url(), p.short_title), end='')
for s in skills:
print("{} ({}), ".format(s.skill, s.get_requirement_short_code()), end='')
print("</p>")
def get_intern_selections(self):
return InternSelection.objects.filter(
project__project_round__participating_round=self,
project__approval_status=Project.APPROVED,
project__project_round__approval_status=Participation.APPROVED).exclude(
funding_source=InternSelection.NOT_FUNDED).order_by('project__project_round__community__name', 'project__short_title')
def get_general_funding_intern_selections(self):
return self.get_intern_selections().filter(
funding_source=InternSelection.GENERAL_FUNDED)
def get_pending_intern_selections(self):
return self.get_intern_selections().filter(
organizer_approved=None)
def get_approved_intern_selections(self):
return self.get_intern_selections().filter(
organizer_approved=True)
def get_rejected_intern_selections(self):
return self.get_intern_selections().filter(
organizer_approved=False)
def get_approved_interns_with_unsigned_contracts(self):
return self.get_approved_intern_selections().filter(
intern_contract=None)
def get_in_good_standing_intern_selections(self):
return self.get_approved_intern_selections().filter(
in_good_standing=True)
def get_interns_with_open_initial_feedback(self):
interns = []
# interns may not give feedback, but we only want to send a reminder email
# if their mentor hasn't given feedback yet.
for i in self.get_in_good_standing_intern_selections():
if i.is_initial_feedback_on_intern_open():
interns.append(i)
return interns
def get_interns_with_open_midpoint_feedback(self):
interns = []
for i in self.get_in_good_standing_intern_selections():
if i.is_midpoint_feedback_on_intern_open():
interns.append(i)
return interns
def get_interns_with_open_final_feedback(self):
interns = []
for i in self.get_in_good_standing_intern_selections():
if i.is_final_feedback_on_intern_open():
interns.append(i)
return interns
def get_communities_with_unused_funding(self):
participations = self.participation_set.approved()
communities = []
for p in participations:
funded = p.interns_funded()
if funded < 1:
continue
intern_count = InternSelection.objects.filter(
project__project_round=p,
project__approval_status=Project.APPROVED,
funding_source=InternSelection.ORG_FUNDED).count()
if intern_count < funded:
communities.append((p.community, intern_count, funded))
communities.sort(key=lambda x: x[0].name)
return communities
def get_common_skills_counter(self):
approved_projects = Project.objects.filter(project_round__participating_round=self, approval_status=Project.APPROVED)
skills = []
for p in approved_projects:
for s in p.projectskill_set.all():
if 'python' in s.skill.lower():
skills.append('Python')
elif 'javascript' in s.skill.lower() or 'JS' in s.skill:
skills.append('JavaScript')
elif 'html' in s.skill.lower() or 'css' in s.skill.lower():
skills.append('HTML/CSS')
elif 'java' in s.skill.lower():
skills.append('Java')
elif 'django' in s.skill.lower():
skills.append('Django')
elif 'c program' in s.skill.lower() or 'c language' in s.skill.lower() or 'c code' in s.skill.lower() or 'programming in c' in s.skill.lower() or s.skill == 'C':
skills.append('C programming')
elif 'c++' in s.skill.lower():
skills.append('C++')
elif 'rust' in s.skill.lower():
skills.append('Rust')
elif 'ruby on rails' in s.skill.lower():
skills.append('Ruby on Rails')
elif 'ruby' in s.skill.lower():
skills.append('Ruby')
elif 'operating systems' in s.skill.lower() or 'kernel' in s.skill.lower():
skills.append('Operating Systems knowledge')
elif 'linux' in s.skill.lower():
skills.append('Linux')
elif 'web development' in s.skill.lower():
skills.append('Web development')
elif 'gtk' in s.skill.lower() or 'gobject' in s.skill.lower():
skills.append('GTK programming')
elif 'git' in s.skill.lower():
skills.append('Git')
elif 'writing' in s.skill.lower() or 'documentation' in s.skill.lower():
skills.append('Documentation')
else:
skills.append(s.skill)
# A lot of projects list Android in conjunction with another skill
if 'android' in s.skill.lower():
skills.append('Android')
# Some projects list both Git or mercurial
if 'mercurial' in s.skill.lower():
skills.append('Mercurial')
# Some projects list both JavaScipt and node.js
if 'node.js' in s.skill.lower():
skills.append('node.js')
return Counter(skills)
# Statistics functions
def get_common_skills(self):
skill_counter = self.get_common_skills_counter()
return skill_counter.most_common(20)
def number_accepted_initial_applications(self):
return self.applicantapproval_set.approved().count()
def number_contributors(self):
return self.applicantapproval_set.approved().filter(
contribution__isnull=False,
).distinct().count()
def number_final_applicants(self):
return self.applicantapproval_set.approved().filter(
finalapplication__isnull=False,
).distinct().count()
def get_statistics_on_eligibility_check(self):
applicants = self.applicantapproval_set
count_all = applicants.count()
count_approved = applicants.approved().count()
count_rejected_all = applicants.rejected().count()
count_rejected_time = applicants.rejected().filter(reason_denied="TIME").count()
count_rejected_general = applicants.rejected().filter(reason_denied="GENERAL").count()
count_rejected_essay = applicants.rejected().filter(reason_denied__contains="ALIGNMENT").count()
if count_rejected_all == 0:
return (count_all, count_approved, 0, 0, 0)
return (count_all, count_approved, count_rejected_essay * 100 / count_rejected_all, count_rejected_time * 100 / count_rejected_all, count_rejected_general * 100 / count_rejected_all)
def get_countries_stats(self):
all_apps = self.applicantapproval_set.approved()
countries = []
cities = []
for a in all_apps:
city, country = a.applicant.get_city_country()
if city != '':
cities.append(city)
if country != '':
countries.append(country)
return Counter(countries).most_common(25)
def get_contributor_demographics(self):
contributors = self.applicantapproval_set.approved().filter(contribution__isnull=False).distinct()
applicants = contributors.count()
us_apps = contributors.filter(
models.Q(
paymenteligibility__us_national_or_permanent_resident=True
) | models.Q(
paymenteligibility__living_in_us=True
)
).count()
us_people_of_color_apps = contributors.filter(
applicantraceethnicityinformation__us_resident_demographics=True,
).count()
if us_apps == 0:
return (applicants, 0, 0)
return (applicants, (us_apps - us_people_of_color_apps) * 100 / us_apps, us_people_of_color_apps * 100 / us_apps)
def get_contributor_gender_stats(self):
all_apps = self.applicantapproval_set.approved().filter(contribution__isnull=False).distinct().count()
if all_apps == 0:
return (0, 0, 0)
cis_apps = ApplicantGenderIdentity.objects.filter(
transgender=False,
genderqueer=False,
demi_boy=False,
demi_girl=False,
trans_masculine=False,
trans_feminine=False,
non_binary=False,
demi_non_binary=False,
genderflux=False,
genderfluid=False,
demi_genderfluid=False,
demi_gender=False,
bi_gender=False,
tri_gender=False,
multigender=False,
pangender=False,
maxigender=False,
aporagender=False,
intergender=False,
mavrique=False,
gender_confusion=False,
gender_indifferent=False,
graygender=False,
agender=False,
genderless=False,
gender_neutral=False,
neutrois=False,
androgynous=False,
androgyne=False,
applicant__application_round=self,
applicant__approval_status=ApprovalStatus.APPROVED,
applicant__contribution__isnull=False).distinct().count()
trans_folks_apps = ApplicantGenderIdentity.objects.filter(
transgender=True,
applicant__application_round=self,
applicant__approval_status=ApprovalStatus.APPROVED,
applicant__contribution__isnull=False).distinct().count()
genderqueer_folks_apps = ApplicantGenderIdentity.objects.filter(
genderqueer=True,
applicant__application_round=self,
applicant__approval_status=ApprovalStatus.APPROVED,
applicant__contribution__isnull=False).distinct().count()
return (cis_apps * 100 / all_apps, trans_folks_apps * 100 / all_apps, genderqueer_folks_apps * 100 / all_apps)
def get_contributor_applicant_funding_status(self):
eligible = self.applicantapproval_set.approved().count()
contributed = self.applicantapproval_set.approved().filter(contribution__isnull=False).distinct().count()
applied = self.applicantapproval_set.approved().filter(finalapplication__isnull=False).distinct().count()
funded = 0
participations = self.participation_set.approved()
for p in participations:
funded = funded + p.interns_funded()
return (eligible, contributed, applied, funded)
def serve(self, request, *args, **kwargs):
# If the project selection page (views.current_round_page) would
# consider this a current_round, redirect there.
if self.pingnew.has_passed() and not self.internannounce.has_passed():
return redirect('project-selection')
# Only show this page if we shouldn't be showing the project selection page.
return super(RoundPage, self).serve(request, *args, **kwargs)
def get_context(self, request, *args, **kwargs):
context = super(RoundPage, self).get_context(request, *args, **kwargs)
context['role'] = Role(request.user, self)
return context
class StatisticTotalApplied(models.Model):
internship_round = models.OneToOneField(RoundPage, on_delete=models.CASCADE, primary_key=True)
total_applicants = models.IntegerField(
validators=[validators.MinValueValidator(0)],
default=0,
)
total_approved = models.IntegerField(
validators=[validators.MinValueValidator(0)],
default=0,
)
total_pending = models.IntegerField(
validators=[validators.MinValueValidator(0)],
default=0,
)
total_rejected = models.IntegerField(
validators=[validators.MinValueValidator(0)],
default=0,
)
total_withdrawn = models.IntegerField(
validators=[validators.MinValueValidator(0)],
default=0,
)
def clean(self):
if self.total_applicants != (self.total_approved + self.total_pending + self.total_rejected + self.total_withdrawn):
error_string = 'Total applicants != approved + pending + rejected + withdrawn'
raise ValidationError({'total_applicants': error_string})
class StatisticApplicantCountry(models.Model):
internship_round = models.ForeignKey(RoundPage, on_delete=models.CASCADE)
country_living_in_during_internship = models.CharField(
verbose_name='Country interns are living in during the internship',
max_length=PARAGRAPH_LENGTH,
)
country_living_in_during_internship_code = models.CharField(
verbose_name='ISO 3166-1 alpha-2 country code',
max_length=2,
)
total_applicants = models.IntegerField(
validators=[validators.MinValueValidator(0)],
default=0,
)
class StatisticAmericanDemographics(models.Model):
internship_round = models.OneToOneField(RoundPage, on_delete=models.CASCADE, primary_key=True)
# total accepted applicants who are U.S. nationals or permanent residents
total_approved_american_applicants = models.IntegerField(
validators=[validators.MinValueValidator(0)],
default=0,
)
# total accepted applicants who are U.S. nationals or permanent residents and
# Black/African American, Hispanic/Latinx, Native American,
# Alaska Native, Native Hawaiian, or Pacific Islander
total_approved_american_bipoc = models.IntegerField(
validators=[validators.MinValueValidator(0)],
default=0,
)
def total_approved_american_not_bipoc(self):
return self.total_approved_american_applicants - self.total_approved_american_bipoc
def percentage_americans_accepted_who_are_bipoc(self):
return int(round(100 * (self.total_approved_american_bipoc / self.total_approved_american_applicants)))
def percentage_americans_accepted_who_are_not_bipoc(self):
return int(round(100 * (self.total_approved_american_not_bipoc() / self.total_approved_american_applicants)))
class StatisticGenderDemographics(models.Model):
internship_round = models.OneToOneField(RoundPage, on_delete=models.CASCADE, primary_key=True)
# Note: These could be overlapping gender identities
# For example, someone could be a non-binary woman, or a trans masculine agender person.
# In short, these totals will not add up to the total number of accepted applicants.
# total accepted applicants who answered 'yes' to "Are you transgender?"
total_transgender_people = models.IntegerField(
validators=[validators.MinValueValidator(0)],
default=0,
)
# total accepted applicants who answered 'yes' to "Are you genderqueer?"
total_genderqueer_people = models.IntegerField(
validators=[validators.MinValueValidator(0)],
default=0,
)
# total accepted applicants checked the 'man' gender box
total_men = models.IntegerField(
validators=[validators.MinValueValidator(0)],
default=0,
)
# Note: Trans men are men!
#
# If an applicant identifies as a man, they would have checked the 'man' gender box.
# However, some non-binary people may identify as trans masculine, but don't identify as men.
#
# Don't assume that trans masculine and trans feminine people are binary.
# total accepted applicants checked the 'trans masculine' gender box
total_trans_masculine_people = models.IntegerField(
validators=[validators.MinValueValidator(0)],
default=0,
)
# total accepted applicants checked the 'woman' gender box
total_women = models.IntegerField(
validators=[validators.MinValueValidator(0)],
default=0,
)
# total accepted applicants checked the 'trans feminine' gender box
total_trans_feminine_people = models.IntegerField(
validators=[validators.MinValueValidator(0)],
default=0,
)
# total accepted applicants checked a gender identity other
# than 'man', 'woman', 'trans masculine', or 'trans feminine'
total_non_binary_people = models.IntegerField(
validators=[validators.MinValueValidator(0)],
default=0,
)
# total accepted applicants who self identified their gender
# We get a lot of people who self-identify as "girl" instead of "woman"
# So self-identification is not an indication of whether they are non-binary
total_who_self_identified_gender = models.IntegerField(
validators=[validators.MinValueValidator(0)],
default=0,
)
def percentage_accepted_who_are_women(self):
return int(round(100 * (self.total_women / self.internship_round.statistictotalapplied.total_approved)))
def percentage_accepted_who_are_men(self):
return int(round(100 * (self.total_men / self.internship_round.statistictotalapplied.total_approved)))
def percentage_accepted_who_are_transgender(self):
return int(round(100 * (self.total_transgender_people / self.internship_round.statistictotalapplied.total_approved)))
def percentage_accepted_who_are_non_binary(self):
return int(round(100 * (self.total_non_binary_people / self.internship_round.statistictotalapplied.total_approved)))
class CohortPage(Page):
round_start = models.DateField("Round start date")
round_end = models.DateField("Round end date")
content_panels = Page.content_panels + [
FieldPanel('round_start'),
FieldPanel('round_end'),
InlinePanel('participant', label="Intern or alumns information", help_text="Please add information about the alumn or intern"),
]
class AlumInfo(Orderable):
page = ParentalKey(CohortPage, related_name='participant')
name = models.CharField(max_length=255, verbose_name="Name")
email = models.EmailField(verbose_name="Email")
picture = models.ForeignKey(
'wagtailimages.Image',
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name='+'
)
gravitar = models.BooleanField(max_length=255, verbose_name="Use gravitar image associated with email?")
location = models.CharField(max_length=255, blank=True, verbose_name="Location (optional)")
nick = models.CharField(max_length=255, blank=True, verbose_name="Chat/Forum/IRC username (optional)")
blog = models.URLField(blank=True, verbose_name="Blog URL (optional)")
rss = models.URLField(blank=True, verbose_name="RSS URL (optional)")
community = models.CharField(max_length=255, verbose_name="Community name")
project = models.CharField(max_length=255, verbose_name="Project description")
mentors = models.CharField(max_length=255, verbose_name="Mentor name(s)")
survey_opt_out = models.BooleanField(default=False)
panels = [
FieldPanel('name'),
FieldPanel('email'),
ImageChooserPanel('picture'),
FieldPanel('gravitar'),
FieldPanel('location'),
FieldPanel('nick'),
FieldPanel('blog'),
FieldPanel('rss'),
FieldPanel('community'),
FieldPanel('project'),
FieldPanel('mentors'),
FieldPanel('survey_opt_out'),
]
def round_string(self):
return '{start:%b %Y} to {end:%b %Y}'.format(
start=self.page.round_start,
end=self.page.round_end)
def __str__(self):
return '{start:%b %Y} to {end:%b %Y}: {name}'.format(
start=self.page.round_start,
end=self.page.round_end,
name=self.name)
# We can't remove this old function because the default value
# for the token field used mentor_id and so an old migration
# refers to mentor_id
# FIXME - squash migrations after applied to server
def mentor_id():
# should be a multiple of three
return urlsafe_b64encode(urandom(9))
def make_comrade_photo_filename(instance, original_name):
# Use the underlying User object's primary key rather than any
# human-readable name, because if the person changes any of their
# names, we don't want to be revealing their old names in these
# URLs. It's usually considered bad style to include database IDs in
# URLs, for a variety of good reasons, but it seems like the best we
# can do here.
base = instance.account.id
# Incorporate a pseudo-random number to make it harder to guess the
# URL to somebody's old photo once they've replaced it.
randbase = 100000000
unique = random.randrange(randbase, 10 * randbase)
# Preserve the original filename's extension as that usually signals
# the file's type.
extension = os.path.splitext(original_name)[1]
return "comrade/{pk}/{unique}{ext}".format(pk=base, unique=unique, ext=extension)
# From Wordnik:
# comrade: A person who shares one's interests or activities; a friend or companion.
# user: One who uses addictive drugs.
class Comrade(models.Model):
account = models.OneToOneField(User, primary_key=True, on_delete=models.CASCADE)
public_name = models.CharField(max_length=LONG_LEGAL_NAME, verbose_name="Name (public)", help_text="Your full name, which will be publicly displayed on the Outreachy website. This is typically your given name, followed by your family name. You may use a pseudonym or abbreviate your given or family names if you have concerns about privacy.")
legal_name = models.CharField(max_length=LONG_LEGAL_NAME, verbose_name="Legal name (private)", help_text="Your name on your government identification. This is the name that you would use to sign a legal document. This will be used only by Outreachy organizers on any private legal contracts. Other applicants, coordinators, mentors, and volunteers will not see this name.")
photo = models.ImageField(blank=True, upload_to=make_comrade_photo_filename,
help_text="File limit size is 1MB. For best display, use a square photo at least 200x200 pixels.")
# Reference: https://uwm.edu/lgbtrc/support/gender-pronouns/
PRONOUN_RAW = (
['she', 'her', 'her', 'hers', 'herself', 'http://pronoun.is/she'],
['he', 'him', 'his', 'his', 'himself', 'http://pronoun.is/he'],
['they', 'them', 'their', 'theirs', 'themself', 'http://pronoun.is/they'],
['fae', 'faer', 'faer', 'faers', 'faerself', 'http://pronoun.is/fae'],
['ey', 'em', 'eir', 'eirs', 'eirself', 'http://pronoun.is/ey'],
['per', 'per', 'pers', 'pers', 'perself', 'http://pronoun.is/per'],
['ve', 'ver', 'vis', 'vis', 'verself', 'http://pronoun.is/ve'],
['xe', 'xem', 'xyr', 'xyrs', 'xemself', 'http://pronoun.is/xe'],
['ze', 'hir', 'hir', 'hirs', 'hirself', 'http://pronoun.is/ze'],
)
PRONOUN_CHOICES = [
(raw[0], '{subject}/{Object}/{possessive_pronoun}'.format(subject=raw[0], Object=raw[1], possessive_pronoun=raw[3]))
for raw in PRONOUN_RAW
]
pronouns = models.CharField(
max_length=4,
choices=PRONOUN_CHOICES,
default='they',
help_text="Common pronouns include she/her, he/him, or they/them. Neopronouns are also welcome! Your pronouns may be (optionally) displayed to Outreachy organizers, applicants, mentors, and (optionally) displayed on the public Outreachy alums page. See the pronoun privacy options below.",
)
pronouns_to_participants = models.BooleanField(
verbose_name = "Share pronouns with Outreachy participants",
help_text = "If this box is checked, applicant pronouns will be shared with coordinators, mentors and volunteers. If the box is checked, coordinator and mentor pronouns will be shared with applicants.<br>If the box is unchecked, no pronouns will be displayed.<br>If you don't want to share your pronouns, all Outreachy organizer email that Cc's another participant will use they/them/their pronouns for you.",
default=True,
)
pronouns_public = models.BooleanField(
verbose_name = "Share pronouns publicly",
help_text = "Mentor, coordinator, and accepted interns' pronouns will be displayed publicly on the Outreachy website to anyone who is not logged in. Sharing pronouns can be a way for people to proudly display their gender identity and connect with other Outreachy participants, but other people may prefer to keep their pronouns private.<br>If this box is unchecked, Outreachy participants will be instructed to use they/them pronouns on public community channels. They will still know what your pronouns are if you check the previous box.",
default=False,
)
timezone = TimeZoneField(blank=True, verbose_name="(Optional) Your timezone", help_text="The timezone in your current location. Shared with other Outreachy participants to help facilitate communication.")
location = models.CharField(
max_length=SENTENCE_LENGTH,
blank=True,
help_text="(Optional) Location - city, state/province, and country.<br>This field is unused for mentors and coordinators. Applicant's location will be shared with their mentors. If selected as an intern, this location will be publicly displayed on the Outreachy website.<br>If you are concerned about keeping your location private, you can share less information, such as just the country, or a larger town nearby.")
nick = models.CharField(
max_length=SENTENCE_LENGTH,
blank=True,
verbose_name="Forum, chat, or IRC username",
help_text="(Optional) The username or 'nick' you typically use when communicating on professional channels. If you don't have one yet, leave this blank and update it later.<br>For mentors and coordinators, this will be displayed to applicants. Applicants' username/nick will be shared with their mentors and coordinators. Accepted interns' username/nick will be displayed on the Outreachy website.")
github_url = models.URLField(blank=True,
verbose_name="GitHub profile URL",
help_text="(Optional) The full URL to your profile on GitHub.<br>For mentors and coordinators, this will be displayed to applicants. Applicants' GitHub URLs will be shared with their mentors and coordinators. Accepted interns' GitHub URLs will be displayed on the Outreachy website.")
gitlab_url = models.URLField(blank=True,
verbose_name="GitLab profile URL",
help_text="(Optional) The full URL to your profile on GitLab.<br>For mentors and coordinators, this will be displayed to applicants. Applicants' GitLab URLs will be shared with their mentors and coordinators. Accepted interns' GitLab URLs will be displayed on the Outreachy website.")
blog_url = models.URLField(blank=True,
verbose_name="Blog URL",
help_text="(Optional) The full URL to your blog.<br>For mentors and coordinators, this will be displayed to applicants. Applicants' blog URLs will be shared with their mentors and coordinators. Accepted interns' blog URLs will be displayed on the Outreachy website.")
blog_rss_url = models.URLField(blank=True,
verbose_name="Blog RSS URL",
help_text="(Optional) The full URL to the RSS or ATOM feed for your blog.<br>For mentors and coordinators, this is unused. Applicants' blog RSS URLs will be unused. Accepted interns' blog RSS URLs will be used to create an aggregated feed of all Outreachy intern blogs, which will be displayed on the Outreachy website or Outreachy planetaria.")
twitter_url = models.URLField(blank=True,
verbose_name="Twitter profile URL",
help_text="(Optional) The full URL to your Twitter profile.<br>For mentors and coordinators, this will be displayed to applicants, who may try to contact you via Twitter. Applicants' Twitter URLs will be shared with their mentors and coordinators. Accepted interns' Twitter URLs will be used to create an Outreachy Twitter list for accepted interns for that round. Accepted interns' Twitter URLs will not be displayed on the Outreachy website.")
agreed_to_code_of_conduct = models.CharField(
max_length=LONG_LEGAL_NAME,
verbose_name = "Type your legal name to indicate you agree to the Code of Conduct")
def __str__(self):
return self.public_name + ' <' + self.account.email + '> (' + self.legal_name + ')'
def email_address(self):
if self.account.email:
return Address(self.public_name, addr_spec=self.account.email)
return ''
def username(self):
return self.account.username
def get_pronouns_html(self):
return "<a href=http://pronoun.is/{short_name}>{pronouns}</a>".format(
short_name=self.pronouns,
pronouns=self.get_pronouns_display(),
)
# Having a text location field was a disaster.
def get_city_country(self):
us_state_abbrevs = ['AL', 'AK', 'AZ', 'AR', 'CA', 'CO', 'CT', 'DE', 'FL', 'GA', 'HI', 'ID', 'IL', 'IN', 'IA', 'KS', 'KY', 'LA', 'ME', 'MD', 'MA', 'MI', 'MN', 'MS', 'MO', 'MT', 'NE', 'NV', 'NH', 'NJ', 'NM', 'NY', 'NC', 'ND', 'OH', 'OK', 'OR', 'PA', 'RI', 'SC', 'SD', 'TN', 'TX', 'UT', 'VT', 'VA', 'WA', 'WV', 'WI', 'WY', 'AS', 'DC', 'FM', 'GU', 'MH', 'MP', 'PW', 'PR', 'VI', ]
us_states = [ 'alabama', 'alaska', 'arizona', 'arkansas', 'california', 'colorado', 'connecticut', 'delaware', 'florida', 'georgia', 'hawaii', 'idaho', 'illinois', 'indiana', 'iowa', 'kansas', 'kentucky', 'louisiana', 'maine', 'maryland', 'massachusetts', 'michigan', 'minnesota', 'mississippi', 'missouri', 'montana', 'nebraska', 'nevada', 'new hampshire', 'new jersey', 'new mexico', 'new york', 'north carolina', 'north dakota', 'ohiooH', 'oklahoma', 'oregon', 'pennsylvania', 'rhode island', 'south carolina', 'south dakota', 'tennessee', 'texas', 'utah', 'vermont', 'virginia', 'washington', 'west virginia', 'wisconsin', 'wyoming', 'american samoa', 'district of columbia', 'federated states of micronesia', 'guam', 'marshall islands', 'northern mariana islands', 'palau', 'puerto rico', 'virgin islands', ]
us_cities = [
'boston',
'los angeles',
'san francisco',
'new york city',
'united states',
'philadelphia',
'madison',
]
us_timezones = [
'America/Los_Angeles',
'America/Chicago',
'America/New_York',
'US/Eastern',
'US/Central',
'US/Pacific',
]
indian_cities = [
'india',
'india.',
'new delhi',
'hyderabad',
'bangalore',
'delhi',
'mumbai',
'hyderabad',
'chennai',
'noida',
'kerala',
'pune',
'jaipur',
'maharashtra',
'new delhi india',
'bengaluru',
]
location = self.location.split(',')
if location == '':
city = ''
else:
city = location[0].strip().lower()
country = ''
if len(location) >= 3:
country = location[-1].strip().lower()
elif len(location) == 2:
country = location[-1].strip().lower()
if country.upper() in us_state_abbrevs or country in us_states:
country = 'usa'
scrubbed_city = ''
if country:
if country == 'usa' or country == 'united states' or country == 'united states of america' or country == 'us' or country in us_states:
country = 'usa'
if country == 'india.' or country == 'delhi and india':
country = 'india'
elif city == 'buenos aires' or city.startswith('argentina'):
country = 'argentina'
# Brazilians like to use dashes instead of commas??
elif city.startswith('são paulo') or city.startswith('curitiba') or city == 'brazil' or city == 'brasil':
country = 'brazil'
elif city == 'yaounde':
country = 'cameroon'
# There's a Vancouver, WA, but it's more likely to be Canada
elif city == 'vancouver' or city == 'canada':
country = 'canada'
elif city == 'egypt':
country = 'egypt'
elif city == 'berlin':
country = 'germany'
elif city in indian_cities:
country = 'india'
elif city == 'israel':
country = 'israel'
elif city == 'mombasa' or city == 'nairobi' or city == 'kenya':
country = 'kenya'
elif city == 'mexico city' or city == 'mexico':
country = 'mexico'
elif city.startswith('lagos') or city == 'port harcourt' or city == 'ibadan' or city == 'nigeria':
country = 'nigeria'
# technically there's a saint petersberg FL, but it's more likely to be Russia
elif city == 'moscow' or city == 'saint petersburg' or city == 'saint-petersburg' or city == 'russia':
country = 'russia'
elif city == 'istanbul' or city == 'turkey':
country = 'turkey'
elif city == 'kazakhstan' or city == 'united arab emirates':
country = 'united arab emirates'
elif city in us_cities or city in us_states:
country = 'usa'
elif self.timezone:
timezone = self.timezone.zone
if timezone == 'America/Argentina/Buenos_Aires':
country = 'argentina'
if 'Australia' in timezone:
country = 'australia'
elif timezone == 'America/Sao_Paulo':
country = 'brazil'
elif timezone.startswith('Canada') or timezone == 'America/Toronto':
country = 'canada'
elif timezone == 'Africa/Cairo':
country = 'egypt'
elif timezone == 'Europe/Berlin':
country = 'germany'
elif timezone == 'Africa/Nairobi' or timezone == 'Africa/Lagos':
country = 'kenya'
elif timezone == 'Asia/Kolkata' or timezone == 'Indian/Mayotte':
country = 'india'
elif timezone == 'Europe/Rome':
country = 'italy'
elif timezone == 'Europe/Dublin':
country = 'ireland'
elif timezone == 'Indian/Antananarivo':
country = 'madagascar'
elif timezone == 'Europe/Bucharest':
country = 'romania'
elif timezone == 'Europe/Moscow':
country = 'russia'
elif timezone == 'Europe/London':
country = 'uk'
elif timezone == 'Europe/Kiev':
country = 'ukraine'
elif timezone in us_timezones:
country = 'usa'
return (city.title(), country.title())
def get_mentored_projects(self):
"""
Returns all projects for which this person has ever been approved as a
mentor, regardless of whether the project itself or its community were
approved. You can apply additional filters to the return value if you
want to be more specific.
"""
return Project.objects.filter(
mentorapproval__mentor=self,
mentorapproval__approval_status=ApprovalStatus.APPROVED,
)
def get_intern_selection(self):
try:
return InternSelection.objects.get(
applicant__applicant=self,
funding_source__in=(InternSelection.ORG_FUNDED, InternSelection.GENERAL_FUNDED),
organizer_approved=True)
except InternSelection.DoesNotExist:
return None
class ApprovalStatusQuerySet(models.QuerySet):
def approved(self):
return self.filter(approval_status=ApprovalStatus.APPROVED)
def pending(self):