from betterforms.multiform import MultiModelForm from datetime import datetime, timedelta, timezone, date from django.conf import settings from django.contrib.auth.models import User from django.contrib.auth.decorators import login_required from django.contrib.admin.views.decorators import staff_member_required from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin from django.core import mail from django.core.exceptions import PermissionDenied from django.core.signing import TimestampSigner, SignatureExpired, BadSignature from django.db import models from django.forms import inlineformset_factory, ModelForm, modelform_factory, modelformset_factory, ValidationError from django.forms.models import BaseInlineFormSet, BaseModelFormSet from django.http import JsonResponse, HttpResponse, Http404 from django.shortcuts import get_list_or_404 from django.shortcuts import get_object_or_404 from django.shortcuts import redirect from django.shortcuts import render from django.urls import reverse from django.utils.http import urlencode from django.utils.safestring import mark_safe from django.utils.text import slugify from django.views.decorators.http import require_POST from django.views.generic import FormView, View, DetailView, ListView, TemplateView from django.views.generic.edit import CreateView, DeleteView, UpdateView from django.views.generic.detail import SingleObjectMixin from formtools.wizard.views import SessionWizardView from itertools import chain, groupby from markdownx.utils import markdownify from registration.forms import RegistrationForm from registration.backends.hmac import views as hmac_views import reversion from . import email from .dashboard import get_dashboard_sections from .forms import InviteForm from .forms import RadioBooleanField from .mixins import ApprovalStatusAction from .mixins import ComradeRequiredMixin from .mixins import EligibleApplicantRequiredMixin from .mixins import Preview from .models import AlumInfo from .models import AlumSurvey from .models import AlumSurveyTracker from .models import ApplicantApproval from .models import ApplicantGenderIdentity from .models import ApplicantRaceEthnicityInformation from .models import ApplicationReviewer from .models import ApprovalStatus from .models import BarriersToParticipation from .models import CohortPage from .models import CommunicationChannel from .models import Community from .models import Comrade from .models import ContractorInformation from .models import Contribution from .models import CoordinatorApproval from .models import create_time_commitment_calendar from .models import EmploymentTimeCommitment from .models import FinalApplication from .models import get_deadline_date_for from .models import InternSelection from .models import InitialApplicationReview from .models import InitialMentorFeedback from .models import InitialInternFeedback from .models import MidpointMentorFeedback from .models import MidpointInternFeedback from .models import FinalMentorFeedback from .models import FinalInternFeedback from .models import MentorApproval from .models import MentorRelationship from .models import NewCommunity from .models import NonCollegeSchoolTimeCommitment from .models import Notification from .models import Participation from .models import PaymentEligibility from .models import PriorFOSSExperience from .models import Project from .models import ProjectSkill from .models import PromotionTracking from .models import Role from .models import RoundPage from .models import SchoolInformation from .models import SchoolTimeCommitment from .models import TimeCommitmentSummary from .models import SignedContract from .models import Sponsorship from .models import VolunteerTimeCommitment from .models import WorkEligibility from os import path class RegisterUserForm(RegistrationForm): def clean(self): email = self.cleaned_data.get('email') if email and User.objects.filter(email=email).exists(): self.add_error('email', mark_safe('This email address is already associated with an account. If you have forgotten your password, you can <a href="{}">reset it</a>.'.format(reverse('password_reset')))) super(RegisterUserForm, self).clean() class RegisterUser(hmac_views.RegistrationView): form_class = RegisterUserForm # The RegistrationView that django-registration provides # doesn't respect the next query parameter, so we have to # add it to the context of the template. def get_context_data(self, **kwargs): context = super(RegisterUser, self).get_context_data(**kwargs) context['next'] = self.request.GET.get('next', '/') return context def get_activation_key(self, user): # The superclass implementation of get_activation_key will # serialize arbitrary data in JSON format, so we can save more # data than just the username, which is good! Unfortunately it # expects to get that data from the field named after whatever # string is in USERNAME_FIELD, which only works for actual # Django user models. So first we construct a fake user model # for it to take apart, containing only the data we want. self.USERNAME_FIELD = 'activation_data' self.activation_data = {'u': user.username} # Now, if we have someplace the user is supposed to go after # registering, then we save that as well. next_url = self.request.POST.get('next') if next_url: self.activation_data['n'] = next_url return super(RegisterUser, self).get_activation_key(self) def get_email_context(self, activation_key): return { 'activation_key': activation_key, 'expiration_days': settings.ACCOUNT_ACTIVATION_DAYS, 'request': self.request, } class PendingRegisterUser(TemplateView): template_name = 'registration/registration_complete.html' class ActivationView(hmac_views.ActivationView): def get_user(self, data): # In the above RegistrationView, we dumped extra data into the # activation key, but the superclass implementation of get_user # expects the key to contain only a username string. So we save # our extra data and then pass the superclass the part it # expected. self.next_url = data.get('n') return super(ActivationView, self).get_user(data['u']) def get_success_url(self, user): # Ugh, we need to chain together TWO next-page URLs so we can # confirm that the person who posesses this activation token # also knows the corresponding password, then make a stop at the # ComradeUpdate form, before finally going where the user # actually wanted to go. Sorry folks. if self.next_url: query = '?' + urlencode({'next': self.next_url}) else: query = '' query = '?' + urlencode({'next': reverse('account') + query}) return reverse('registration_activation_complete') + query class ActivationCompleteView(TemplateView): template_name = 'registration/activation_complete.html' class ComradeUpdate(LoginRequiredMixin, UpdateView): # FIXME - we need a way for comrades to change their passwords # and update and re-verify their email address. def get_form_class(self): # We want to have different fields for different comrades, but # self.fields is shared across all instances of this view, so we can't # use that. There's no get_fields() method we can override, either, so # the only hook we can use is overriding this method of ModelFormMixin. fields = [ 'public_name', 'nick_name', 'legal_name', 'pronouns', 'pronouns_to_participants', 'pronouns_public', ] comrade = self.object # was an approved coordinator for a community that had at least one approved participation coordinatored = comrade.coordinatorapproval_set.approved().filter( community__participation__approval_status=ApprovalStatus.APPROVED, ) # was an approved mentor for some approved project in an approved community mentored = comrade.get_mentored_projects().approved().filter( project_round__approval_status=ApprovalStatus.APPROVED, ) # was an approved application reviewer at some point reviewered = comrade.applicationreviewer_set.approved() # people who participated in some way at some time can set a photo of themselves if comrade.account.is_staff or comrade.get_intern_selection() is not None or coordinatored.exists() or mentored.exists() or reviewered.exists(): fields.append('photo') fields.extend([ 'timezone', 'location', 'nick', 'github_url', 'gitlab_url', 'blog_url', 'blog_rss_url', 'twitter_url', 'agreed_to_code_of_conduct', ]) return modelform_factory(comrade.__class__, fields=fields) def get_object(self): # Either grab the current comrade to update, or make a new one try: return self.request.user.comrade except Comrade.DoesNotExist: return Comrade(account=self.request.user) def get_context_data(self, **kwargs): context = super(ComradeUpdate, self).get_context_data(**kwargs) context['next'] = self.request.GET.get('next', '/') with open(path.join(settings.BASE_DIR, 'CODE-OF-CONDUCT.md')) as coc_file: context['codeofconduct'] = markdownify(coc_file.read()) return context # FIXME - not sure where we should redirect people back to? # Take them back to the home page right now. def get_success_url(self): return self.request.POST.get('next', '/') class EmptyModelFormSet(BaseModelFormSet): def get_queryset(self): return self.model._default_manager.none() class SchoolTimeCommitmentModelFormSet(EmptyModelFormSet): def clean(self): super(SchoolTimeCommitmentModelFormSet, self).clean() if any(self.errors): # Don't validate if the individual term fields already have errors return end = None last_term = None number_filled_terms = 0 for index, form in enumerate(self.forms): # This checks if one of the forms was left blank if index >= self.initial_form_count() and not form.has_changed(): continue number_filled_terms += 1 # Ensure that only one term has last_term set end_term = form.cleaned_data['last_term'] if end_term: if last_term: raise ValidationError("You cannot have more than one term be the last term in your degree.") else: last_term = form # Ensure terms are in consecutive order start_date = form.cleaned_data['start_date'] if end and end > start_date: raise ValidationError("Terms must be in chronological order.") end = form.cleaned_data['end_date'] # Ensure that all three terms are filled out, unless one term has the last_term set if not last_term and number_filled_terms < 3: raise ValidationError("Please provide information for your next three terms of classes.") # We can't confirm there are no terms after the term where last_term is set # because someone might be ending their bachelor's degree and starting a master's. def work_eligibility_is_approved(wizard): cleaned_data = wizard.get_cleaned_data_for_step('Work Eligibility') if not cleaned_data: return True if not cleaned_data['over_18']: return False # If they have student visa restrictions, we don't follow up # until they're actually selected as an intern, at which point we # need to send them a CPT letter and have them get it approved. if not cleaned_data['eligible_to_work']: return False if cleaned_data['under_export_control']: return False # If they're in a us_sanctioned_country, go ahead and collect the # rest of the information on the forms, but we'll mark them as # PENDING later. return True def prior_foss_experience_is_approved(wizard): if not work_eligibility_is_approved(wizard): return False cleaned_data = wizard.get_cleaned_data_for_step('Prior FOSS Experience') if not cleaned_data: return True if cleaned_data['gsoc_or_outreachy_internship']: return False return True def show_us_demographics(wizard): if not prior_foss_experience_is_approved(wizard): return False cleaned_data = wizard.get_cleaned_data_for_step('Payment Eligibility') or {} if not cleaned_data: return True us_resident = cleaned_data.get('us_national_or_permanent_resident', True) return us_resident def show_noncollege_school_info(wizard): if not prior_foss_experience_is_approved(wizard): return False cleaned_data = wizard.get_cleaned_data_for_step('Time Commitments') or {} return cleaned_data.get('enrolled_as_noncollege_student', True) def show_school_info(wizard): if not prior_foss_experience_is_approved(wizard): return False cleaned_data = wizard.get_cleaned_data_for_step('Time Commitments') or {} return cleaned_data.get('enrolled_as_student', True) def show_contractor_info(wizard): if not prior_foss_experience_is_approved(wizard): return False cleaned_data = wizard.get_cleaned_data_for_step('Time Commitments') or {} if cleaned_data == None: return False return cleaned_data.get('contractor', True) def show_employment_info(wizard): if not prior_foss_experience_is_approved(wizard): return False cleaned_data = wizard.get_cleaned_data_for_step('Time Commitments') or {} if cleaned_data.get('employed', True): return True if cleaned_data.get('contractor', True): cleaned_data = wizard.get_cleaned_data_for_step('Contractor Info') if cleaned_data and cleaned_data[0].get('continuing_contract_work', True): return True return False def show_time_commitment_info(wizard): if not prior_foss_experience_is_approved(wizard): return False cleaned_data = wizard.get_cleaned_data_for_step('Time Commitments') or {} return cleaned_data.get('volunteer_time_commitments', True) def time_commitment(cleaned_data, hours): return { 'start_date': cleaned_data['start_date'], 'end_date': cleaned_data['end_date'], 'hours': hours, } def time_commitments_are_approved(wizard, application_round): tcs = [ time_commitment(d, d['hours_per_week']) for d in wizard.get_cleaned_data_for_step('Volunteer Time Commitment Info') or [] if d ] ctcs = [ time_commitment(d, 0 if d['quit_on_acceptance'] else d['hours_per_week']) for d in wizard.get_cleaned_data_for_step('Coding School or Online Courses Time Commitment Info') or [] if d ] etcs = [ time_commitment(d, 0 if d['quit_on_acceptance'] else d['hours_per_week']) for d in wizard.get_cleaned_data_for_step('Employment Info') or [] if d ] stcs = [ time_commitment(d, 40) for d in wizard.get_cleaned_data_for_step('School Term Info') or [] if d ] required_free_days = 7*7 calendar = create_time_commitment_calendar(chain(tcs, ctcs, etcs, stcs), application_round) for key, group in groupby(calendar, lambda hours: hours <= 20): if key is True and len(list(group)) >= required_free_days: return True return False def determine_eligibility(wizard, application_round): if not (work_eligibility_is_approved(wizard)): return (ApprovalStatus.REJECTED, 'GENERAL') if not (prior_foss_experience_is_approved(wizard)): return (ApprovalStatus.REJECTED, 'GENERAL') if not time_commitments_are_approved(wizard, application_round): return (ApprovalStatus.REJECTED, 'TIME') general_data = wizard.get_cleaned_data_for_step('Work Eligibility') if general_data['us_sanctioned_country']: return (ApprovalStatus.PENDING, 'SANCTIONED') return (ApprovalStatus.PENDING, 'ESSAY') def get_current_round_for_initial_application(): """ People can only submit new initial applications or edit initial applications when the application period is open. """ now = datetime.now(timezone.utc) today = get_deadline_date_for(now) try: current_round = RoundPage.objects.get( initial_applications_open__lte=today, initial_applications_close__gt=today, ) current_round.today = today except RoundPage.DoesNotExist: raise PermissionDenied('The Outreachy application period is closed. If you are an applicant who has submitted an application for an internship project and your time commitments have increased, please contact the Outreachy organizers (see contact link above). Eligibility checking will become available when the next application period opens. Please sign up for the announcements mailing list for an email when the next application period opens: https://lists.outreachy.org/cgi-bin/mailman/listinfo/announce') return current_round def get_current_round_for_initial_application_review(): """ Application reviewers need to have finished their work before the contribution period begins. """ now = datetime.now(timezone.utc) today = get_deadline_date_for(now) try: current_round = RoundPage.objects.get( initial_applications_open__lte=today, contributions_close__gt=today, ) current_round.today = today except RoundPage.DoesNotExist: raise PermissionDenied('It is too late to review applications.') return current_round class EligibilityUpdateView(LoginRequiredMixin, ComradeRequiredMixin, reversion.views.RevisionMixin, SessionWizardView): template_name = 'home/wizard_form.html' condition_dict = { 'Payment Eligibility': work_eligibility_is_approved, 'Prior FOSS Experience': work_eligibility_is_approved, 'USA demographics': show_us_demographics, 'Gender Identity': prior_foss_experience_is_approved, 'Time Commitments': prior_foss_experience_is_approved, 'School Info': show_school_info, 'School Term Info': show_school_info, 'Coding School or Online Courses Time Commitment Info': show_noncollege_school_info, 'Contractor Info': show_contractor_info, 'Employment Info': show_employment_info, 'Volunteer Time Commitment Info': show_time_commitment_info, } form_list = [ ('Work Eligibility', modelform_factory(WorkEligibility, fields=( 'over_18', 'student_visa_restrictions', 'eligible_to_work', 'under_export_control', 'us_sanctioned_country', ), field_classes={ 'over_18': RadioBooleanField, 'student_visa_restrictions': RadioBooleanField, 'eligible_to_work': RadioBooleanField, 'under_export_control': RadioBooleanField, 'us_sanctioned_country': RadioBooleanField, }, )), ('Payment Eligibility', modelform_factory(PaymentEligibility, fields=( 'us_national_or_permanent_resident', 'living_in_us', ), field_classes={ 'us_national_or_permanent_resident': RadioBooleanField, 'living_in_us': RadioBooleanField, }, )), ('Prior FOSS Experience', modelform_factory(PriorFOSSExperience, fields=( 'gsoc_or_outreachy_internship', 'prior_contributor', 'prior_paid_contributor', 'prior_contrib_coding', 'prior_contrib_forums', 'prior_contrib_events', 'prior_contrib_issues', 'prior_contrib_devops', 'prior_contrib_docs', 'prior_contrib_data', 'prior_contrib_translate', 'prior_contrib_illustration', 'prior_contrib_ux', 'prior_contrib_short_talk', 'prior_contrib_testing', 'prior_contrib_security', 'prior_contrib_marketing', 'prior_contrib_reviewer', 'prior_contrib_mentor', 'prior_contrib_accessibility', 'prior_contrib_self_identify', ), field_classes={ 'gsoc_or_outreachy_internship': RadioBooleanField, 'prior_contributor': RadioBooleanField, 'prior_paid_contributor': RadioBooleanField, }, )), ('USA demographics', modelform_factory(ApplicantRaceEthnicityInformation, fields=( 'us_resident_demographics', ), field_classes={ 'us_resident_demographics': RadioBooleanField, }, )), ('Gender Identity', modelform_factory(ApplicantGenderIdentity, fields=( 'transgender', 'genderqueer', 'man', 'woman', 'demi_boy', 'demi_girl', 'trans_masculine', 'trans_feminine', 'non_binary', 'demi_non_binary', 'genderqueer', 'genderflux', 'genderfluid', 'demi_genderfluid', 'demi_gender', 'bi_gender', 'tri_gender', 'multigender', 'pangender', 'maxigender', 'aporagender', 'intergender', 'mavrique', 'gender_confusion', 'gender_indifferent', 'graygender', 'agender', 'genderless', 'gender_neutral', 'neutrois', 'androgynous', 'androgyne', 'prefer_not_to_say', 'self_identify', ), field_classes={ 'transgender': RadioBooleanField, 'genderqueer': RadioBooleanField, }, )), ('Barriers to Participation', modelform_factory(BarriersToParticipation, fields=( 'lacking_representation', 'systemic_bias', 'employment_bias', 'barriers_to_contribution', ), )), ('Time Commitments', modelform_factory(TimeCommitmentSummary, fields=( 'enrolled_as_student', 'enrolled_as_noncollege_student', 'employed', 'contractor', 'volunteer_time_commitments', ), field_classes={ 'enrolled_as_student': RadioBooleanField, 'enrolled_as_noncollege_student': RadioBooleanField, 'employed': RadioBooleanField, 'contractor': RadioBooleanField, 'volunteer_time_commitments': RadioBooleanField, }, )), ('School Info', modelform_factory(SchoolInformation, fields=( 'university_name', 'university_website', 'current_academic_calendar', 'next_academic_calendar', 'degree_name', ), )), ('School Term Info', modelformset_factory(SchoolTimeCommitment, formset=SchoolTimeCommitmentModelFormSet, min_num=1, validate_min=True, extra=2, can_delete=False, fields=( 'term_name', 'start_date', 'end_date', 'last_term', ), )), ('Coding School or Online Courses Time Commitment Info', modelformset_factory(NonCollegeSchoolTimeCommitment, formset=EmptyModelFormSet, min_num=1, validate_min=True, extra=4, can_delete=False, fields=( 'start_date', 'end_date', 'hours_per_week', 'description', 'quit_on_acceptance', ), )), ('Contractor Info', modelformset_factory(ContractorInformation, formset=EmptyModelFormSet, min_num=1, max_num=1, validate_min=True, validate_max=True, can_delete=False, fields=( 'typical_hours', 'continuing_contract_work', ), field_classes={ 'continuing_contract_work': RadioBooleanField, }, )), ('Employment Info', modelformset_factory(EmploymentTimeCommitment, formset=EmptyModelFormSet, min_num=1, validate_min=True, extra=2, can_delete=False, fields=( 'start_date', 'end_date', 'hours_per_week', 'job_title', 'job_description', 'quit_on_acceptance', ), )), ('Volunteer Time Commitment Info', modelformset_factory(VolunteerTimeCommitment, formset=EmptyModelFormSet, min_num=1, validate_min=True, extra=2, can_delete=False, fields=( 'start_date', 'end_date', 'hours_per_week', 'description', ), )), ('Outreachy Promotional Information', modelform_factory(PromotionTracking, fields=( 'spread_the_word', ), )), ] TEMPLATES = { 'Work Eligibility': 'home/eligibility_wizard_general.html', 'Payment Eligibility': 'home/eligibility_wizard_tax_forms.html', 'Prior FOSS Experience': 'home/eligibility_wizard_foss_experience.html', 'USA demographics': 'home/eligibility_wizard_us_demographics.html', 'Gender Identity': 'home/eligibility_wizard_gender.html', 'Barriers to Participation': 'home/eligibility_wizard_essay_questions.html', 'Time Commitments': 'home/eligibility_wizard_time_commitments.html', 'School Info': 'home/eligibility_wizard_school_info.html', 'School Term Info': 'home/eligibility_wizard_school_terms.html', 'Coding School or Online Courses Time Commitment Info': 'home/eligibility_wizard_noncollege_school_info.html', 'Contractor Info': 'home/eligibility_wizard_contractor_info.html', 'Employment Info': 'home/eligibility_wizard_employment_info.html', 'Volunteer Time Commitment Info': 'home/eligibility_wizard_time_commitment_info.html', 'Outreachy Promotional Information': 'home/eligibility_wizard_promotional.html', } def get_template_names(self): return [self.TEMPLATES[self.steps.current]] def show_results_if_any(self): # get_context_data() and done() both need a round; save it for them. self.current_round = get_current_round_for_initial_application() already_submitted = ApplicantApproval.objects.filter( applicant=self.request.user.comrade, application_round=self.current_round, ).exists() if not already_submitted: # Continue with the default get or post behavior. return None return redirect(self.request.GET.get('next', reverse('eligibility-results'))) def get(self, *args, **kwargs): # Using `or` this way returns the redirect from show_results_if_any, # unless that function returns None. Only in that case does it call the # superclass implementation of this method and return _that_. return self.show_results_if_any() or super(EligibilityUpdateView, self).get(*args, **kwargs) def post(self, *args, **kwargs): # See self.get(), above. return self.show_results_if_any() or super(EligibilityUpdateView, self).post(*args, **kwargs) def get_context_data(self, **kwargs): context = super(EligibilityUpdateView, self).get_context_data(**kwargs) context['current_round'] = self.current_round return context def done(self, form_list, **kwargs): self.object = ApplicantApproval( applicant=self.request.user.comrade, application_round=self.current_round, ) self.object.ip_address = self.request.META.get('REMOTE_ADDR') # It's okay that the other objects aren't saved, # because determine_eligibility get the cleaned data from the form wizard, # not the database objects. self.object.approval_status, self.object.reason_denied = determine_eligibility(self, self.object.application_round) # Make sure to commit the object to the database before saving # any of the related objects, so they can set their foreign keys # to point to this ApplicantApproval object. self.object.save() for form in form_list: results = form.save(commit=False) # result might be a single value because it's a modelform # (WorkEligibility and TimeCommitmentSummary) # or a list because it's a modelformsets # (VolunteerTimeCommitment, EmploymentTimeCommitment, etc) # The next line is magic to check if it's a list if not isinstance(results, list): results = [ results ] # For each object which contains data from the modelform # or modelformsets, we save that database object, # after setting the parent pointer. for r in results: r.applicant = self.object r.save() return redirect(self.request.GET.get('next', reverse('eligibility-results'))) class EligibilityResults(LoginRequiredMixin, ComradeRequiredMixin, DetailView): template_name = 'home/eligibility_results.html' context_object_name = 'role' def get_object(self): now = datetime.now(timezone.utc) today = get_deadline_date_for(now) # We want to let people know why they can't make contributions, right # up until all contributions are closed; but we don't want to confuse # people who come back in a future round by showing them old results. try: current_round = RoundPage.objects.get( initial_applications_open__lte=today, contributions_close__gt=today, ) current_round.today = today except RoundPage.DoesNotExist: raise PermissionDenied('The Outreachy application period is closed. Eligibility checking will become available when the next application period opens. Please sign up for the announcements mailing list for an email when the next application period opens: https://lists.outreachy.org/cgi-bin/mailman/listinfo/announce') role = Role(self.request.user, current_round) if not role.is_applicant: raise Http404("No initial application in this round.") return role class ViewInitialApplication(LoginRequiredMixin, ComradeRequiredMixin, DetailView): template_name = 'home/applicant_review_detail.html' context_object_name = 'application' def get_context_data(self, **kwargs): context = super(ViewInitialApplication, self).get_context_data(**kwargs) context['current_round'] = self.object.application_round context['role'] = self.role return context def get_object(self): current_round = get_current_round_for_initial_application_review() self.role = Role(self.request.user, current_round) if not self.role.is_organizer and not self.role.is_reviewer: raise PermissionDenied("You are not authorized to review applications.") return get_object_or_404(ApplicantApproval, applicant__account__username=self.kwargs['applicant_username'], application_round=current_round) def promote_page(request): now = datetime.now(timezone.utc) today = get_deadline_date_for(now) # For the purposes of this view, a round is current until # initial application period ends. After that, there's # no point in getting people to apply to that round. try: current_round = RoundPage.objects.get( pingnew__lte=today, initial_applications_close__gt=today, ) current_round.today = today except RoundPage.DoesNotExist: current_round = None return render(request, 'home/promote.html', { 'current_round' : current_round, }, ) def past_rounds_page(request): return render(request, 'home/past_rounds.html', { 'rounds' : RoundPage.objects.all().order_by('internstarts'), }, ) def current_round_page(request): closed_approved_projects = [] ontime_approved_projects = [] example_skill = ProjectSkill now = datetime.now(timezone.utc) today = get_deadline_date_for(now) # For the purposes of this view, a round is current until its # intern selections are announced, and then it becomes one of # the "previous" rounds. try: previous_round = RoundPage.objects.filter( internannounce__lte=today, ).latest('internstarts') previous_round.today = today except RoundPage.DoesNotExist: previous_round = None try: # Keep RoundPage.serve() in sync with this. current_round = RoundPage.objects.get( pingnew__lte=today, internannounce__gt=today, ) current_round.today = today except RoundPage.DoesNotExist: current_round = None role = Role(request.user, current_round) if current_round is not None: approved_participations = current_round.participation_set.approved().order_by('community__name') for p in approved_participations: if not p.approved_to_see_project_overview(request.user): continue projects = p.project_set.approved().filter(new_contributors_welcome=False) if projects: closed_approved_projects.append((p, projects)) projects = p.project_set.approved().filter(new_contributors_welcome=True) if projects: ontime_approved_projects.append((p, projects)) return render(request, 'home/round_page_with_communities.html', { 'current_round' : current_round, 'previous_round' : previous_round, 'closed_projects': closed_approved_projects, 'ontime_projects': ontime_approved_projects, 'example_skill': example_skill, 'role': role, }, ) # Call for communities, mentors, and volunteers page # # This is complex, so class-based views don't help us here. # # We want to display four sections: # * Blurb about what Outreachy is # * Timeline for the round # * Communities that are participating and are open to mentors and volunteers # * Communities that have participated and need to be claimed by coordinators # # We need to end up with: # * The most current round (by round number) # * The communities participating in the current round (which have their CFP open) # * The communities which aren't participating in the current round # # We need to do some database calls in order to get this info: # * Grab all the rounds, sort by round number (descending), hand us back one round # * For the current round, grab all Participations (communities participating) # * Grab all the communities # # To get the communities which aren't participating: # * Make a set of the community IDs from the communities # participating in the current round (participating IDs) # * Walk through all communities, seeing if the community ID is # in participating IDs. # * If so, put it in a participating communities set # * If not, put it in a not participating communities set def community_cfp_view(request): # Cheap trick for case-insensitive sorting: the slug is always lower-cased. all_communities = Community.objects.all().order_by('slug') approved_communities = [] pending_communities = [] rejected_communities = [] not_participating_communities = [] # The problem here is the CFP page serves four (or more) purposes: # - to provide mentors a way to submit new projects # - to provide coordinators a way to submit new communities # - to allow mentors to sign up to co-mentor a project # - to allow mentors a way to edit their projects # # So, we close down the page after the interns are announced, # when (hopefully) all mentors have signed up to co-mentor. # Mentors can still be sent a manual link to sign up to co-mentor after that date, # but their community page just won't show their project. now = datetime.now(timezone.utc) today = get_deadline_date_for(now) try: previous_round = RoundPage.objects.filter( internstarts__lte=today, ).latest('internstarts') previous_round.today = today except RoundPage.DoesNotExist: previous_round = None try: current_round = RoundPage.objects.get( pingnew__lte=today, internstarts__gt=today, ) current_round.today = today except RoundPage.DoesNotExist: current_round = None not_participating_communities = all_communities.filter( participation__approval_status=ApprovalStatus.APPROVED, ).distinct() else: # No exception caught, so we have a current_round # Now grab the community IDs of all communities participating in the current round # https://docs.djangoproject.com/en/1.11/topics/db/queries/#following-relationships-backward # https://docs.djangoproject.com/en/1.11/ref/models/querysets/#values-list # https://docs.djangoproject.com/en/1.11/ref/models/querysets/#values participating_communities = { p.community_id: p for p in current_round.participation_set.all() } for c in all_communities: participation_info = participating_communities.get(c.id) if participation_info is not None: if participation_info.is_pending(): pending_communities.append(c) elif participation_info.is_approved(): approved_communities.append(c) else: # either withdrawn or rejected rejected_communities.append(c) else: not_participating_communities.append(c) # See https://docs.djangoproject.com/en/1.11/topics/http/shortcuts/ return render(request, 'home/community_cfp.html', { 'current_round' : current_round, 'previous_round' : previous_round, 'pending_communities': pending_communities, 'approved_communities': approved_communities, 'rejected_communities': rejected_communities, 'not_participating_communities': not_participating_communities, }, ) def community_read_only_view(request, community_slug): """ This is the page for volunteers, mentors, and coordinators. It's a read-only page that displays information about the community, what projects are accepted, and how volunteers can help. If the community isn't participating in this round, the page displays instructions for being notified or signing the community up to participate. """ community = get_object_or_404(Community, slug=community_slug) now = datetime.now(timezone.utc) today = get_deadline_date_for(now) participation_info = None # For the purposes of this view, a round is current until its interns start # their internships, and then it becomes one of the "previous" rounds. try: current_round = RoundPage.objects.get( pingnew__lte=today, internstarts__gt=today, ) current_round.today = today previous_round = None except RoundPage.DoesNotExist: current_round = None try: previous_round = community.rounds.filter( internstarts__lte=today, participation__approval_status=ApprovalStatus.APPROVED, ).latest('internstarts') previous_round.today = today except RoundPage.DoesNotExist: previous_round = None else: # Try to see if this community is participating in the current round # and get the Participation object if so. try: participation_info = community.participation_set.get(participating_round=current_round) except Participation.DoesNotExist: pass coordinator = None notification = None mentors_pending_projects = () if request.user.is_authenticated: try: # Although the current user is authenticated, don't assume # that they have a Comrade instance. Instead check that the # approval's coordinator is attached to a User that matches # this one. coordinator = community.coordinatorapproval_set.get(coordinator__account=request.user) except CoordinatorApproval.DoesNotExist: pass try: notification = community.notification_set.get(comrade__account=request.user) except Notification.DoesNotExist: pass if participation_info is not None and participation_info.approval_status in (ApprovalStatus.PENDING, ApprovalStatus.APPROVED): try: mentors_pending_projects = request.user.comrade.get_mentored_projects().pending().filter( project_round=participation_info, ) except Comrade.DoesNotExist: pass return render(request, 'home/community_read_only.html', { 'current_round' : current_round, 'previous_round' : previous_round, 'community': community, 'coordinator': coordinator, 'notification': notification, 'mentors_pending_projects': mentors_pending_projects, 'participation_info': participation_info, }) # A Comrade wants to sign up to be notified when a community coordinator # says this community is participating in a new round class CommunityNotificationUpdate(LoginRequiredMixin, ComradeRequiredMixin, UpdateView): fields = [] def get_object(self): community = get_object_or_404(Community, slug=self.kwargs['community_slug']) try: return Notification.objects.get(comrade=self.request.user.comrade, community=community) except Notification.DoesNotExist: return Notification(comrade=self.request.user.comrade, community=community) def get_success_url(self): return self.object.community.get_preview_url() def community_landing_view(request, round_slug, community_slug): # Try to see if this community is participating in that round # and if so, get the Participation object and related objects. participation_info = get_object_or_404( Participation.objects.select_related('community', 'participating_round'), community__slug=community_slug, participating_round__slug=round_slug, ) projects = participation_info.project_set.approved() ontime_projects = [p for p in projects if p.new_contributors_welcome] closed_projects = [p for p in projects if not p.new_contributors_welcome] example_skill = ProjectSkill current_round = participation_info.participating_round role = Role(request.user, current_round) approved_coordinator_list = CoordinatorApproval.objects.none() if request.user.is_authenticated: approved_coordinator_list = participation_info.community.coordinatorapproval_set.approved() approved_to_see_all_project_details = participation_info.approved_to_see_all_project_details(request.user) mentors_pending_projects = Project.objects.none() approved_coordinator = False if request.user.is_authenticated: # If a mentor has submitted a project, they should be able to see all # their project details and have the link to edit the project, even if # the community is pending or the project isn't approved. try: # XXX: Despite the name, this is not limited to pending projects. Should it be? mentors_pending_projects = request.user.comrade.get_mentored_projects().filter( project_round=participation_info, ) except Comrade.DoesNotExist: pass approved_coordinator = participation_info.community.is_coordinator(request.user) return render(request, 'home/community_landing.html', { 'participation_info': participation_info, 'ontime_projects': ontime_projects, 'closed_projects': closed_projects, 'role': role, # TODO: make the template get these off the participation_info instead of passing them in the context 'current_round' : current_round, 'community': participation_info.community, 'approved_coordinator_list': approved_coordinator_list, 'approved_to_see_all_project_details': approved_to_see_all_project_details, 'mentors_pending_projects': mentors_pending_projects, 'approved_coordinator': approved_coordinator, 'example_skill': example_skill, }, ) class CommunityCreate(LoginRequiredMixin, ComradeRequiredMixin, CreateView): model = NewCommunity fields = ['name', 'approved_license', 'no_proprietary_software', 'approved_advertising', 'community_size', 'longevity', 'participating_orgs', 'description', 'long_description', 'tutorial', 'website', 'governance', 'code_of_conduct', 'cla', 'dco', 'unapproved_license_description', 'proprietary_software_description', 'unapproved_advertising_description', ] def get_form(self): now = datetime.now(timezone.utc) today = get_deadline_date_for(now) try: self.current_round = RoundPage.objects.filter( lateorgs__gt=today, ).earliest('lateorgs') self.current_round.today = today except RoundPage.DoesNotExist: raise PermissionDenied("There is no round you can participate in right now.") return super(CommunityCreate, self).get_form() # We have to over-ride this method because we need to # create a community's slug from its name. def form_valid(self, form): self.object = form.save(commit=False) self.object.slug = slugify(self.object.name)[:self.object._meta.get_field('slug').max_length] self.object.save() # Whoever created this community is automatically approved as a # coordinator for it, even though the community itself isn't # approved yet. CoordinatorApproval.objects.create( coordinator=self.request.user.comrade, community=self.object, approval_status=ApprovalStatus.APPROVED) # Send email email.notify_organizers_of_new_community(self.object) # When a new community is created, immediately redirect the # coordinator to gather information about their participation in # this round. The Participation object doesn't have to be saved # to the database yet; that'll happen when they submit it in the # next step. return redirect(Participation( community=self.object, participating_round=self.current_round, ).get_action_url('submit')) class CommunityUpdate(LoginRequiredMixin, UpdateView): model = Community slug_url_kwarg = 'community_slug' fields = ['name', 'description', 'long_description', 'website', 'tutorial'] def get_object(self): community = super(CommunityUpdate, self).get_object() if not community.is_coordinator(self.request.user): raise PermissionDenied("You are not an approved coordinator for this community.") return community def get_success_url(self): return self.object.get_preview_url() class SponsorshipInlineFormSet(BaseInlineFormSet): def get_queryset(self): qs = super(SponsorshipInlineFormSet, self).get_queryset() return qs.filter(coordinator_can_update=True) def save_new(self, form, commit=True): # Ensure that new objects created by this form will still be # editable with it later. form.instance.coordinator_can_update = True return super(SponsorshipInlineFormSet, self).save_new(form, commit) class ParticipationAction(ApprovalStatusAction): form_class = inlineformset_factory(Participation, Sponsorship, formset=SponsorshipInlineFormSet, fields='__all__', exclude=['coordinator_can_update']) # Make sure that someone can't feed us a bad community URL by fetching the Community. def get_object(self): community = get_object_or_404(Community, slug=self.kwargs['community_slug']) # FIXME: probably should raise PermissionDenied, not Http404, outside of deadlines # For most purposes, this form is available right up to intern announcement... now = datetime.now(timezone.utc) today = get_deadline_date_for(now) participating_round = get_object_or_404( RoundPage, slug=self.kwargs['round_slug'], pingnew__lte=today, internannounce__gt=today, ) # ...except submitting new communities cuts off at the lateorgs deadline. if participating_round.lateorgs.has_passed(): return get_object_or_404( Participation, community=community, participating_round=participating_round, ) try: return Participation.objects.get( community=community, participating_round=participating_round) except Participation.DoesNotExist: return Participation( community=community, participating_round=participating_round) def get_context_data(self, **kwargs): context = super(ParticipationAction, self).get_context_data(**kwargs) context['readonly_sponsorships'] = self.object.sponsorship_set.filter(coordinator_can_update=False) return context def save_form(self, form): # We might be newly-creating the Participation or changing its # approval_status even though the form the user sees has no # fields off this object itself, so make sure to save it first # so it gets assigned a primary key. self.object.save() # InlineFormSet's save method returns the list of created or # changed Sponsorship objects, not the parent Participation. form.save() # Saving this form doesn't change which object is current. return self.object def notify(self): if self.prior_status == self.target_status: return email.approval_status_changed(self.object, self.request) if self.target_status == ApprovalStatus.PENDING: for notification in self.object.community.notification_set.all(): email.notify_mentor(self.object, notification, self.request) notification.delete() # This view is for mentors and coordinators to review project information and approve it def project_read_only_view(request, round_slug, community_slug, project_slug): project = get_object_or_404( Project.objects.select_related('project_round__participating_round', 'project_round__community'), slug=project_slug, project_round__participating_round__slug=round_slug, project_round__community__slug=community_slug, ) approved_mentors = project.mentorapproval_set.approved() unapproved_mentors = project.mentorapproval_set.filter(approval_status__in=(ApprovalStatus.PENDING, ApprovalStatus.REJECTED)) mentor_request = None coordinator = None if request.user.is_authenticated: # For both of the following queries, although the current user # is authenticated, don't assume that they have a Comrade # instance. Instead check that the approval is attached to a # User that matches this one. try: # Grab the current user's mentor request regardless of its # status so we can tell them what their status is. mentor_request = project.mentorapproval_set.get(mentor__account=request.user) except MentorApproval.DoesNotExist: pass try: # Grab the current user's coordinator request for the # community this project is part of, but only if they've # been approved, because otherwise we don't treat them any # differently than anyone else. coordinator = project.project_round.community.coordinatorapproval_set.approved().get(coordinator__account=request.user) except CoordinatorApproval.DoesNotExist: pass return render(request, 'home/project_read_only.html', { 'current_round': project.round(), 'community': project.project_round.community, 'project' : project, 'approved_mentors': approved_mentors, 'unapproved_mentors': unapproved_mentors, 'mentor_request': mentor_request, 'coordinator': coordinator, }, ) class CoordinatorApprovalAction(ApprovalStatusAction): # We don't collect any information about coordinators beyond what's # in the Comrade model already. fields = [] def get_object(self): community = get_object_or_404(Community, slug=self.kwargs['community_slug']) username = self.kwargs.get('username') if username: comrade = get_object_or_404(Comrade, account__username=username) else: comrade = self.request.user.comrade try: return CoordinatorApproval.objects.get(coordinator=comrade, community=community) except CoordinatorApproval.DoesNotExist: return CoordinatorApproval(coordinator=comrade, community=community) def get_success_url(self): if self.kwargs['action'] == 'submit': # There's nothing useful to see on the coordinator preview # page, so go to the community preview after submit instead. return self.object.community.get_preview_url() return self.object.get_preview_url() def notify(self): if self.prior_status != self.target_status: email.approval_status_changed(self.object, self.request) class InviteMentor(LoginRequiredMixin, ComradeRequiredMixin, FormView, SingleObjectMixin): template_name = 'home/invite-mentor.html' form_class = InviteForm def get_form(self, *args, **kwargs): # This method is called during both GET and POST, before # get_context_data or form_valid, but after the login checks have run. # So it's a semi-convenient common place to set self.object. self.object = project = get_object_or_404( Project, project_round__community__slug=self.kwargs['community_slug'], project_round__participating_round__slug=self.kwargs['round_slug'], slug=self.kwargs['project_slug'], ) user = self.request.user if not project.is_mentor(user) and not project.project_round.community.is_coordinator(user): raise PermissionDenied("Only approved project mentors or community coordinators can invite additional mentors.") return super(InviteMentor, self).get_form(*args, **kwargs) def form_valid(self, form): email.invite_mentor(self.object, form.get_address(), self.request) return redirect('dashboard') class MentorApprovalAction(ApprovalStatusAction): fields = [ 'instructions_read', 'understands_intern_time_commitment', 'understands_applicant_time_commitment', 'understands_mentor_contract', 'mentored_before', 'mentorship_style', 'longevity', 'mentor_foss_contributions', 'communication_channel_username', ] def get_object(self): project = get_object_or_404(Project, project_round__community__slug=self.kwargs['community_slug'], project_round__participating_round__slug=self.kwargs['round_slug'], slug=self.kwargs['project_slug']) username = self.kwargs.get('username') if username: mentor = get_object_or_404(Comrade, account__username=username) else: mentor = self.request.user.comrade try: return MentorApproval.objects.get(mentor=mentor, project=project) except MentorApproval.DoesNotExist: return MentorApproval(mentor=mentor, project=project) def get_success_url(self): # BaseProjectEditPage doesn't allow people who aren't # "submitters" for the project to edit skills/channels. So # we want initial project submission to go # 1. 'project-action' # 2. 'mentorapproval-action' # 3. 'project-skills-edit' # 4. 'communication-channels-edit' # but if this is a co-mentor signup the co-mentor can't follow # that route because they haven't been approved to edit the # project yet when they first sign up. if self.kwargs['action'] == 'submit' and self.object.project.is_submitter(self.request.user): return self.object.project.reverse('project-skills-edit') return self.object.project.get_preview_url() def notify(self): if self.prior_status != self.target_status: email.approval_status_changed(self.object, self.request) if self.target_status == MentorApproval.APPROVED: interns = self.object.project.internselection_set.exclude(funding_source=InternSelection.NOT_FUNDED) # If we're adding a co-mentor after Outreachy organizers have # approved intern selections, then only tell the new co-mentor # about the approved interns. current_round = self.object.project.round() if current_round.internapproval.has_passed(): interns = interns.filter(organizer_approved=True) for intern_selection in interns: email.co_mentor_intern_selection_notification( intern_selection, [self.object.mentor.email_address()], self.request, ) class ProjectAction(ApprovalStatusAction): fields = ['approved_license', 'no_proprietary_software', 'longevity', 'community_size', 'short_title', 'long_description', 'minimum_system_requirements', 'contribution_tasks', 'repository', 'issue_tracker', 'newcomer_issue_tag', 'intern_tasks', 'intern_benefits', 'community_benefits', 'unapproved_license_description', 'proprietary_software_description', 'new_contributors_welcome'] # Make sure that someone can't feed us a bad community URL by fetching the Community. def get_object(self): participation = get_object_or_404(Participation, community__slug=self.kwargs['community_slug'], participating_round__slug=self.kwargs['round_slug']) project_slug = self.kwargs.get('project_slug') if project_slug: return get_object_or_404(Project, project_round=participation, slug=project_slug) else: return Project(project_round=participation) def save_form(self, form): project = form.save(commit=False) if not project.slug: project.slug = slugify(project.short_title)[:project._meta.get_field('slug').max_length] project.save() return project def get_success_url(self): if not self.kwargs.get('project_slug'): # If this is a new Project, associate an approved mentor with it mentor = MentorApproval.objects.create( mentor=self.request.user.comrade, project=self.object, approval_status=ApprovalStatus.APPROVED) return mentor.get_action_url('submit', self.request.user) elif self.kwargs['action'] == 'submit': return self.object.reverse('project-skills-edit') return self.object.get_preview_url() def notify(self): if self.prior_status == self.target_status: return email.approval_status_changed(self.object, self.request) if self.target_status == ApprovalStatus.PENDING: if not self.object.approved_license or not self.object.no_proprietary_software: email.project_nonfree_warning(self.object, self.request) class BaseProjectEditPage(LoginRequiredMixin, ComradeRequiredMixin, UpdateView): def get_object(self): project = get_object_or_404(Project, slug=self.kwargs['project_slug'], project_round__community__slug=self.kwargs['community_slug'], project_round__participating_round__slug=self.kwargs['round_slug']) if not project.is_submitter(self.request.user): raise PermissionDenied("You are not an approved mentor for this project") # Only allow adding new project communication channels or skills # for approved projects after the deadline has passed. deadline = project.submission_and_approval_deadline() if not project.is_approved() and deadline.has_passed(): raise PermissionDenied("The project submission and approval deadline ({date}) has passed. Please sign up for the announcement mailing list for a call for mentors for the next Outreachy internship round. https://lists.outreachy.org/cgi-bin/mailman/listinfo/announce".format(date=deadline)) return project def get_success_url(self): return reverse(self.next_view, kwargs=self.kwargs) class ProjectSkillsEditPage(BaseProjectEditPage): template_name_suffix = '_skills_form' form_class = inlineformset_factory(Project, ProjectSkill, fields='__all__') next_view = 'communication-channels-edit' class CommunicationChannelsEditPage(BaseProjectEditPage): template_name_suffix = '_channels_form' form_class = inlineformset_factory(Project, CommunicationChannel, fields='__all__') next_view = 'project-read-only' class MentorApprovalPreview(Preview): def get_object(self): return get_object_or_404( MentorApproval, project__slug=self.kwargs['project_slug'], project__project_round__participating_round__slug=self.kwargs['round_slug'], project__project_round__community__slug=self.kwargs['community_slug'], mentor__account__username=self.kwargs['username']) class CoordinatorApprovalPreview(Preview): def get_object(self): return get_object_or_404( CoordinatorApproval, community__slug=self.kwargs['community_slug'], coordinator__account__username=self.kwargs['username']) class ProjectContributions(LoginRequiredMixin, ComradeRequiredMixin, EligibleApplicantRequiredMixin, TemplateView): template_name = 'home/project_contributions.html' def get_context_data(self, **kwargs): # Make sure both the Community and Project are approved project = get_object_or_404(Project, slug=self.kwargs['project_slug'], approval_status=ApprovalStatus.APPROVED, project_round__community__slug=self.kwargs['community_slug'], project_round__participating_round__slug=self.kwargs['round_slug'], project_round__approval_status=ApprovalStatus.APPROVED) current_round = project.round() role = Role(self.request.user, current_round) contributions = role.application.contribution_set.filter( project=project) try: final_application = role.application.finalapplication_set.get( project=project) except FinalApplication.DoesNotExist: final_application = None context = super(ProjectContributions, self).get_context_data(**kwargs) context.update({ 'current_round' : current_round, 'community': project.project_round.community, 'project': project, 'role': role, 'contributions': contributions, 'final_application': final_application, }) return context # Only submit one contribution at a time class ContributionUpdate(LoginRequiredMixin, ComradeRequiredMixin, EligibleApplicantRequiredMixin, UpdateView): fields = [ 'date_started', 'date_merged', 'url', 'description', ] def get_object(self): # Make sure both the Community and Project are approved project = get_object_or_404(Project, slug=self.kwargs['project_slug'], approval_status=ApprovalStatus.APPROVED, project_round__community__slug=self.kwargs['community_slug'], project_round__participating_round__slug=self.kwargs['round_slug'], project_round__approval_status=ApprovalStatus.APPROVED) current_round = project.round() role = Role(self.request.user, current_round) try: application = role.application.finalapplication_set.get( project=project) except FinalApplication.DoesNotExist: application = None if not current_round.contributions_open.has_passed(): raise PermissionDenied("You cannot record a contribution until the Outreachy application period opens.") if current_round.contributions_close.has_passed() and application == None: raise PermissionDenied("Editing or recording new contributions is closed at this time to applicants who have not created a final application.") if current_round.internannounce.has_passed(): raise PermissionDenied("Editing or recording new contributions is closed at this time.") if 'contribution_id' not in self.kwargs: return Contribution(applicant=role.application, project=project) return get_object_or_404( Contribution, applicant=role.application, project=project, pk=self.kwargs['contribution_id'], ) def get_success_url(self): return self.object.project.get_contributions_url() class FinalApplicationRate(LoginRequiredMixin, ComradeRequiredMixin, View): def post(self, request, *args, **kwargs): # Make sure both the Community and Project are approved project = get_object_or_404(Project, slug=kwargs['project_slug'], approval_status=ApprovalStatus.APPROVED, project_round__community__slug=kwargs['community_slug'], project_round__participating_round__slug=kwargs['round_slug'], project_round__approval_status=ApprovalStatus.APPROVED) # Only allow approved mentors to rank applicants approved_mentors = project.get_approved_mentors() if request.user.comrade not in [m.mentor for m in approved_mentors]: raise PermissionDenied("You are not an approved mentor for this project.") current_round = project.round() if not current_round.contributions_open.has_passed(): raise PermissionDenied("You cannot rate an applicant until the Outreachy application period opens.") if current_round.has_last_day_to_add_intern_passed(): raise PermissionDenied("Outreachy interns cannot be rated at this time.") applicant = get_object_or_404( current_round.applicantapproval_set.approved(), applicant__account__username=kwargs['username'], ) application = get_object_or_404(FinalApplication, applicant=applicant, project=project) rating = kwargs['rating'] if rating in [c[0] for c in application.RATING_CHOICES]: application.rating = kwargs['rating'] application.save() return redirect(project.get_applicants_url() + "#rating") class FinalApplicationAction(ApprovalStatusAction): fields = [ 'time_correct', 'time_updates', 'experience', 'foss_experience', 'relevant_projects', 'applying_to_gsoc', 'community_specific_questions', 'timeline', ] def get_object(self): username = self.kwargs.get('username') if username: account = get_object_or_404(User, username=username) else: account = self.request.user # Make sure both the Community and Project are approved project = get_object_or_404(Project, slug=self.kwargs['project_slug'], approval_status=ApprovalStatus.APPROVED, project_round__community__slug=self.kwargs['community_slug'], project_round__participating_round__slug=self.kwargs['round_slug'], project_round__approval_status=ApprovalStatus.APPROVED) current_round = project.round() role = Role(account, current_round, self.request.user) if not current_round.contributions_open.has_passed(): raise PermissionDenied("You can't submit a final application until the Outreachy application period opens.") if current_round.contributions_close.has_passed(): raise PermissionDenied("This project is closed to final applications.") # Only allow eligible applicants to apply if not role.is_approved_applicant: raise PermissionDenied("You are not an eligible applicant or you have not filled out the eligibility check.") try: return FinalApplication.objects.get(applicant=role.application, project=project) except FinalApplication.DoesNotExist: return FinalApplication(applicant=role.application, project=project) def get_success_url(self): return self.object.project.get_contributions_url() class ProjectApplicants(LoginRequiredMixin, ComradeRequiredMixin, TemplateView): template_name = 'home/project_applicants.html' def get_context_data(self, **kwargs): # Make sure both the Community, Project, and mentor are approved # Note that accessing URL parameters like project_slug off kwargs only # works because this is a TemplateView. For the various kinds of # DetailViews, you have to use self.kwargs instead. project = get_object_or_404(Project, slug=kwargs['project_slug'], approval_status=ApprovalStatus.APPROVED, project_round__community__slug=kwargs['community_slug'], project_round__participating_round__slug=kwargs['round_slug'], project_round__approval_status=ApprovalStatus.APPROVED) current_round = project.round() # Note that there's no reason to ever keep someone who was a # coordinator or mentor in a past round from looking at who applied in # that round. if not self.request.user.is_staff and not project.project_round.community.is_coordinator(self.request.user) and not project.round().is_mentor(self.request.user): raise PermissionDenied("You are not an approved mentor for this project.") contributions = project.contribution_set.filter( applicant__approval_status=ApprovalStatus.APPROVED).order_by( "applicant__applicant__public_name", "date_started") internship_total_days = current_round.internends - current_round.internstarts try: mentor_approval = project.mentorapproval_set.approved().get( mentor__account=self.request.user, ) except MentorApproval.DoesNotExist: mentor_approval = None context = super(ProjectApplicants, self).get_context_data(**kwargs) context.update({ 'current_round': current_round, 'community': project.project_round.community, 'project': project, 'contributions': contributions, 'internship_total_days': internship_total_days, 'approved_mentor': project.is_submitter(self.request.user), 'is_staff': self.request.user.is_staff, 'mentor_approval': mentor_approval, }) return context @login_required def community_applicants(request, round_slug, community_slug): # Make sure both the Community and mentor are approved participation = get_object_or_404(Participation, community__slug=community_slug, participating_round__slug=round_slug, approval_status=ApprovalStatus.APPROVED) current_round = participation.participating_round # Note that there's no reason to ever keep someone who was a coordinator or # mentor in a past round from looking at who applied in that round. user_is_coordinator = participation.community.is_coordinator(request.user) user_is_staff = request.user.is_staff if not user_is_staff and not user_is_coordinator and not participation.is_mentor(request.user): raise PermissionDenied("You are not an approved mentor for this community.") return render(request, 'home/community_applicants.html', { 'current_round': current_round, 'community': participation.community, 'participation': participation, 'is_coordinator': user_is_coordinator, 'is_staff': user_is_staff, }) def eligibility_information(request): now = datetime.now(timezone.utc) today = get_deadline_date_for(now) try: # The most relevant dates come from the soonest round where internships # haven't started yet... current_round = RoundPage.objects.filter( internstarts__gt=today, ).earliest('internstarts') except RoundPage.DoesNotExist: try: # ...but if there aren't any, use the round that started most # recently, so people get some idea of what the timeline looks like # even when the next round isn't announced yet. current_round = RoundPage.objects.latest('internstarts') except RoundPage.DoesNotExist: raise Http404("No internship rounds configured yet!") current_round.today = today return render(request, 'home/eligibility.html', { 'current_round': current_round, }) class TrustedVolunteersListView(UserPassesTestMixin, ListView): template_name = 'home/trusted_volunteers.html' def get_queryset(self): now = datetime.now(timezone.utc) today = get_deadline_date_for(now) # Find all mentors and coordinators who are active in any round that is # currently running (anywhere from pingnew to internends). # Mentors get annoyed if they're re-subscribed after the internship ends. return Comrade.objects.filter( models.Q( mentorapproval__approval_status=ApprovalStatus.APPROVED, mentorapproval__project__approval_status=ApprovalStatus.APPROVED, mentorapproval__project__project_round__approval_status=ApprovalStatus.APPROVED, mentorapproval__project__project_round__participating_round__pingnew__lte=today, mentorapproval__project__project_round__participating_round__internends__gt=today, ) | models.Q( coordinatorapproval__approval_status=ApprovalStatus.APPROVED, coordinatorapproval__community__participation__approval_status=ApprovalStatus.APPROVED, coordinatorapproval__community__participation__participating_round__pingnew__lte=today, coordinatorapproval__community__participation__participating_round__internends__gt=today, ) ).order_by('public_name').distinct() def test_func(self): return self.request.user.is_staff class ActiveTrustedVolunteersListView(UserPassesTestMixin, ListView): template_name = 'home/trusted_volunteers.html' def get_queryset(self): now = datetime.now(timezone.utc) today = get_deadline_date_for(now) # For all interns with active internships, who are approved by Outreachy organizers, interns = InternSelection.objects.filter( organizer_approved=True, intern_starts__lte=today, intern_ends__gte=today) # Find all mentors who signed up to mentor this intern, # and all approved coordinators for the intern's community. # # This means mentors who didn't select an intern, # or coordiantors from communities who didn't select any interns don't # get re-subscribed to the mentors' mailing list or invited to the chat. return Comrade.objects.filter( models.Q( mentorapproval__approval_status=ApprovalStatus.APPROVED, mentorapproval__mentorrelationship__intern_selection__in=interns, ) | models.Q( coordinatorapproval__approval_status=ApprovalStatus.APPROVED, coordinatorapproval__community__in=[i.project.project_round.community for i in interns], ) ).order_by('public_name').distinct() def test_func(self): return self.request.user.is_staff class ActiveInternshipContactsView(UserPassesTestMixin, TemplateView): template_name = 'home/active_internship_contacts.html' def get_context_data(self, **kwargs): now = datetime.now(timezone.utc) today = get_deadline_date_for(now) # For all interns with active internships, who are approved by Outreachy organizers, interns = InternSelection.objects.filter( organizer_approved=True, project__project_round__participating_round__internannounce__lte=today, intern_ends__gte=today).order_by('applicant__applicant__public_name').order_by('project__project_round__community__name') mentors_and_coordinators = Comrade.objects.filter( models.Q( mentorapproval__approval_status=ApprovalStatus.APPROVED, mentorapproval__mentorrelationship__intern_selection__in=interns, ) | models.Q( coordinatorapproval__approval_status=ApprovalStatus.APPROVED, coordinatorapproval__community__in=[i.project.project_round.community for i in interns], ) ).order_by('public_name').distinct() context = super(ActiveInternshipContactsView, self).get_context_data(**kwargs) context.update({ 'interns': interns, 'mentors_and_coordinators': mentors_and_coordinators, }) return context def test_func(self): return self.request.user.is_staff # Is this a current or past intern in good standing? # This will return None if the internship hasn't been announced yet. def intern_in_good_standing(user): if not user.is_authenticated: return None try: internship = InternSelection.objects.get( applicant__applicant__account = user, project__approval_status = ApprovalStatus.APPROVED, project__project_round__approval_status = ApprovalStatus.APPROVED, organizer_approved = True, in_good_standing = True, ) if not internship.round().internannounce.has_passed(): internship = None except InternSelection.DoesNotExist: internship = None return internship @login_required def intern_contract_export_view(request): internship = intern_in_good_standing(request.user) if not internship: raise PermissionDenied("You are not an Outreachy intern.") if not internship.intern_contract: raise PermissionDenied("You have not signed your Outreachy internship contract.") response = HttpResponse(internship.intern_contract.text, content_type="text/plain") response['Content-Disposition'] = 'attachment; filename="intern-contract-' + internship.intern_contract.legal_name + '-' + internship.intern_contract.date_signed.strftime("%Y-%m-%d") + '.md"' return response def generic_intern_contract_export_view(request): with open(path.join(settings.BASE_DIR, 'docs', 'intern-agreement.md')) as iafile: intern_agreement = iafile.read() response = HttpResponse(intern_agreement, content_type="text/plain") response['Content-Disposition'] = 'attachment; filename="intern-contract-generic-unsigned.md"' return response # Passed round_slug, community_slug, project_slug, applicant_username # Even if someone resigns as a mentor, we still want to keep their signed mentor agreement class MentorContractExport(LoginRequiredMixin, ComradeRequiredMixin, View): def get(self, request, round_slug, community_slug, project_slug, applicant_username): try: mentor_relationship = MentorRelationship.objects.get( mentor__mentor=request.user.comrade, intern_selection__project__slug=self.kwargs['project_slug'], intern_selection__project__project_round__community__slug=self.kwargs['community_slug'], intern_selection__project__project_round__participating_round__slug=self.kwargs['round_slug'], intern_selection__applicant__applicant__account__username=self.kwargs['applicant_username'], ) except MentorRelationship.DoesNotExist: raise PermissionDenied("Cannot export mentor contract. You have not signed a contract for this internship.") response = HttpResponse(mentor_relationship.contract.text, content_type="text/plain") response['Content-Disposition'] = 'attachment; filename="mentor-contract-' + mentor_relationship.contract.legal_name + '-' + mentor_relationship.contract.date_signed.strftime("%Y-%m-%d") + '.md"' return response def generic_mentor_contract_export_view(request): with open(path.join(settings.BASE_DIR, 'docs', 'mentor-agreement.md')) as mafile: mentor_agreement = mafile.read() response = HttpResponse(mentor_agreement, content_type="text/plain") response['Content-Disposition'] = 'attachment; filename="mentor-contract-generic-unsigned.md"' return response @login_required @staff_member_required def contract_export_view(request, round_slug): def export_comrade_with_contract(comrade, contract): return { 'public name': comrade.public_name, 'legal name': comrade.legal_name, 'blog URL': comrade.blog_url, 'email address': comrade.account.email, 'contract signed by': contract.legal_name, 'contract signed on': str(contract.date_signed), 'contract signed from': contract.ip_address, 'contract text': contract.text, } this_round = get_object_or_404(RoundPage, slug=round_slug) interns = this_round.get_approved_intern_selections().exclude(intern_contract=None) dictionary_list = [] for sel in interns: intern_export = export_comrade_with_contract(sel.applicant.applicant, sel.intern_contract) intern_export['community'] = sel.community_name() intern_export['mentors'] = [ export_comrade_with_contract(mr.mentor.mentor, mr.contract) for mr in sel.mentorrelationship_set.all() ] dictionary_list.append(intern_export) response = JsonResponse(dictionary_list, safe=False) response['Content-Disposition'] = 'attachment; filename="' + round_slug + '-contracts.json"' return response class SignedContractForm(ModelForm): class Meta: model = SignedContract fields = ('legal_name',) class FinalApplicationForm(ModelForm): class Meta: model = FinalApplication fields = ('rating',) def clean_rating(self): rating = self.cleaned_data['rating'] if rating == FinalApplication.UNRATED: raise ValidationError("You must provide a rating for the selected applicant.") return rating class InternSelectionForm(MultiModelForm): form_classes = { 'rating': FinalApplicationForm, 'contract': SignedContractForm, } def set_project_and_applicant(self, current_round): self.project = get_object_or_404(Project, slug=self.kwargs['project_slug'], approval_status=ApprovalStatus.APPROVED, project_round__community__slug=self.kwargs['community_slug'], project_round__participating_round=current_round, project_round__approval_status=ApprovalStatus.APPROVED) self.applicant = get_object_or_404( current_round.applicantapproval_set.approved(), applicant__account__username=self.kwargs['applicant_username'], ) # Passed round_slug, community_slug, project_slug, applicant_username class InternSelectionUpdate(LoginRequiredMixin, ComradeRequiredMixin, reversion.views.RevisionMixin, FormView): form_class = InternSelectionForm template_name = 'home/internselection_form.html' def get_form_kwargs(self): kwargs = super(InternSelectionUpdate, self).get_form_kwargs() current_round = get_object_or_404(RoundPage, slug=self.kwargs['round_slug']) if not current_round.contributions_open.has_passed(): raise PermissionDenied("You can't select an intern until the Outreachy application period opens.") set_project_and_applicant(self, current_round) application = get_object_or_404(FinalApplication, applicant=self.applicant, project=self.project) # Usually, we want mentors to only be able to select new interns # until the deadline. However, sometimes an Outreachy mentor # needs to stop participating in the middle of the internship. # This function is the path for them to sign up to co-mentor # (so that they can submit internship feedback). if current_round.has_last_day_to_add_intern_passed(): try: intern_selection = InternSelection.objects.get( applicant=self.applicant, project=self.project, ) except InternSelection.DoesNotExist: raise PermissionDenied("Intern selection is closed for this round.") # Allow mentors to sign up to co-mentor, up until the internship ends if current_round.has_internship_ended(): raise PermissionDenied("You cannot sign up to mentor after an internship has ended.") # Only allow approved mentors to select interns try: self.mentor_approval = self.project.mentorapproval_set.approved().get( mentor__account=self.request.user, ) except MentorApproval.DoesNotExist: raise PermissionDenied("Only approved mentors can select an applicant as an intern") # Don't allow mentors to sign the contract twice if MentorRelationship.objects.filter( mentor = self.mentor_approval, intern_selection__applicant = self.applicant).exists(): raise PermissionDenied("This intern has already been selected for this project. You cannot sign the mentor agreement twice.") with open(path.join(settings.BASE_DIR, 'docs', 'mentor-agreement.md')) as mafile: self.mentor_agreement = mafile.read() # We pass in all object instances that already exist, # and the form will create new object instances in memory that aren't referenced. kwargs.update(instance={ 'rating': application, }) return kwargs def get_context_data(self, **kwargs): context = super(InternSelectionUpdate, self).get_context_data(**kwargs) try: intern_selection = InternSelection.objects.get( applicant=self.applicant, project=self.project, ) except InternSelection.DoesNotExist: intern_selection = None context['mentor_agreement_html'] = markdownify(self.mentor_agreement) context['project'] = self.project context['community'] = self.project.project_round.community context['applicant'] = self.applicant context['intern_selection'] = intern_selection context['current_round'] = self.project.round() return context def form_valid(self, form): form['rating'].save() # We can't use get or create here, in case the payment dates have changed # between when the intern was initially selected and when a co-mentor signed up was_intern_selection_created = False try: intern_selection = InternSelection.objects.get( applicant=self.applicant, project=self.project, ) except InternSelection.DoesNotExist: was_intern_selection_created = True current_round = self.project.round() intern_selection = InternSelection.objects.create( applicant=self.applicant, project=self.project, intern_starts=current_round.internstarts, initial_feedback_opens=current_round.initialfeedback - timedelta(days=7), initial_feedback_due=current_round.initialfeedback, midpoint_feedback_opens=current_round.midfeedback - timedelta(days=7), midpoint_feedback_due=current_round.midfeedback, intern_ends=current_round.internends, final_feedback_opens=current_round.finalfeedback - timedelta(days=7), final_feedback_due=current_round.finalfeedback, ) # Fill in the date and IP address of the signed contract signed_contract = form['contract'].save(commit=False) signed_contract.date_signed = datetime.now(timezone.utc) signed_contract.ip_address = self.request.META.get('REMOTE_ADDR') signed_contract.text = self.mentor_agreement signed_contract.save() mentor_relationship = MentorRelationship( intern_selection=intern_selection, mentor=self.mentor_approval, contract=signed_contract).save() # If we just created this intern selection, email all co-mentors and # encourage them to sign the mentor agreement. if was_intern_selection_created: email.co_mentor_intern_selection_notification( intern_selection, [ mentor_approval.mentor.email_address() for mentor_approval in self.project.mentorapproval_set.approved() # skip the current visitor, who just signed if mentor_approval != self.mentor_approval ], self.request, ) # Send emails about any project conflicts email.intern_selection_conflict_notification(intern_selection, self.request) return redirect(self.project.get_applicants_url() + "#rating") # Passed round_slug, community_slug, project_slug, applicant_username class InternRemoval(LoginRequiredMixin, ComradeRequiredMixin, reversion.views.RevisionMixin, DeleteView): model = InternSelection template_name = 'home/intern_removal_form.html' def get_object(self): current_round = get_object_or_404(RoundPage, slug=self.kwargs['round_slug']) # If the internship is currently active, # then only Outreachy organizers should remove interns # by setting them not in good standing on the alums page. if current_round.is_internship_active(): raise PermissionDenied("Only Outreachy organizers can remove an intern after the internship starts.") # Mentors shouldn't be able to delete interns after the internship ends. if current_round.has_internship_ended(): raise PermissionDenied("Outreachy interns cannot be removed after the internship ends.") set_project_and_applicant(self, current_round) self.intern_selection = get_object_or_404(InternSelection, applicant=self.applicant, project=self.project) self.mentor_relationships = self.intern_selection.mentorrelationship_set.all() # Only allow approved mentors to remove interns # Coordinators can set the funding to 'Not funded' # Organizers can set the InternSelection.organizer_approved to False try: self.mentor_approval = self.project.mentorapproval_set.approved().get( mentor__account=self.request.user, ) except MentorApproval.DoesNotExist: raise PermissionDenied("Only approved mentors can select an applicant as an intern") return self.intern_selection def get_context_data(self, **kwargs): context = super(InternRemoval, self).get_context_data(**kwargs) context['project'] = self.project context['community'] = self.project.project_round.community context['applicant'] = self.applicant context['current_round'] = self.project.round() return context # get_success_url is called before the object is deleted in DeleteView.delete() # so the objects are still in the database. def get_success_url(self): # Delete all the associated signed contracts # The MentorRelationships will be deleted automatically # However, if a mentor resigned from the internship, # the contract will still be around, and that's ok. for relationship in self.mentor_relationships: relationship.contract.delete() return self.project.get_applicants_url() + "#rating" @login_required def project_timeline(request, round_slug, community_slug, project_slug, applicant_username): intern_selection = get_object_or_404(InternSelection, applicant__applicant__account__username=applicant_username, project__slug=project_slug, project__project_round__community__slug=community_slug, project__project_round__participating_round__slug=round_slug) final_application = intern_selection.get_application() # Verify that this is either: # the intern, # staff, # an approved mentor for the project, or # an approved coordinator for the community. # The last two cases are covered by is_submitter() if not request.user.is_staff and not request.user == intern_selection.applicant.applicant.account and not intern_selection.is_submitter(request.user): raise PermissionDenied("You are not authorized to view this intern project timeline.") return render(request, 'home/intern_timeline.html', { 'intern_selection': intern_selection, 'project': intern_selection.project, 'community': intern_selection.project.project_round.community, 'current_round': intern_selection.project.round(), 'final_application': final_application, }) # Passed round_slug, community_slug, project_slug, applicant_username # Even if someone resigns as a mentor, we still want to keep their signed mentor agreement class MentorResignation(LoginRequiredMixin, ComradeRequiredMixin, DeleteView): model = MentorRelationship template_name = 'home/mentor_resignation_form.html' def get_object(self): self.current_round = RoundPage.objects.get(slug=self.kwargs['round_slug']) if self.current_round.has_internship_ended(): raise PermissionDenied("You cannot resign as a mentor from an internship from a past Outreachy round.") set_project_and_applicant(self, self.current_round) self.intern_selection = get_object_or_404(InternSelection, applicant=self.applicant, project=self.project) # Only allow approved mentors to resign from the internship try: self.mentor_approval = self.project.mentorapproval_set.approved().get( mentor__account=self.request.user, ) except MentorApproval.DoesNotExist: raise PermissionDenied("Only approved mentors can resign from an internship.") return get_object_or_404(self.intern_selection.mentorrelationship_set, mentor=self.mentor_approval) def get_context_data(self, **kwargs): context = super(MentorResignation, self).get_context_data(**kwargs) context['project'] = self.project context['community'] = self.project.project_round.community context['applicant'] = self.applicant context['current_round'] = self.current_round return context def get_success_url(self): # Store the signed mentor contract for resigned mentors return self.project.get_applicants_url() + "#rating" class InternFund(LoginRequiredMixin, ComradeRequiredMixin, reversion.views.RevisionMixin, View): def post(self, request, *args, **kwargs): username = kwargs['applicant_username'] current_round = get_object_or_404(RoundPage, slug=kwargs['round_slug']) if not current_round.contributions_open.has_passed(): raise PermissionDenied("You cannot set a funding source for an Outreachy intern until the application period opens.") if current_round.has_last_day_to_add_intern_passed(): raise PermissionDenied("Funding sources for Outreachy interns cannot be changed at this time.") set_project_and_applicant(self, current_round) self.intern_selection = get_object_or_404(InternSelection, applicant=self.applicant, project=self.project) # Only allow approved coordinators and organizers to select intern funding user_is_coordinator = self.project.project_round.community.is_coordinator(request.user) user_is_staff = request.user.is_staff if not user_is_staff and not user_is_coordinator: raise PermissionDenied("Only approved coordinators and organizers can set intern funding sources.") funding = kwargs['funding'] if funding in [c[0] for c in self.intern_selection.FUNDING_CHOICES]: if funding == InternSelection.ORG_FUNDED: # 'project_round' is the Participation (community and round) # Look for which are org-funded interns in this round for this community org_funded_intern_count = InternSelection.objects.filter( project__project_round=self.intern_selection.project.project_round, funding_source=InternSelection.ORG_FUNDED).count() if org_funded_intern_count + 1 > self.intern_selection.project.project_round.interns_funded(): raise PermissionDenied("You've selected more interns for organization funding than you have sponsored funds available. Please use your web browser back button and choose another funding source.") past_funding = self.intern_selection.funding_source self.intern_selection.funding_source = kwargs['funding'] self.intern_selection.save() # If the coordinator or organizer is moving this intern from NOT_FUNDED # to any other state, send emails about any project conflicts if past_funding == InternSelection.NOT_FUNDED and funding != InternSelection.NOT_FUNDED: email.intern_selection_conflict_notification(self.intern_selection, self.request) return redirect(self.project.project_round.reverse('community-applicants') + "#interns") class InternApprove(LoginRequiredMixin, ComradeRequiredMixin, reversion.views.RevisionMixin, View): def post(self, request, *args, **kwargs): username = kwargs['applicant_username'] current_round = get_object_or_404(RoundPage, slug=kwargs['round_slug']) if not current_round.contributions_open.has_passed(): raise PermissionDenied("You cannot approve an Outreachy intern until the application period opens.") if current_round.has_last_day_to_add_intern_passed(): raise PermissionDenied("Approval status for Outreachy interns cannot be changed at this time.") set_project_and_applicant(self, current_round) self.intern_selection = get_object_or_404(InternSelection, applicant=self.applicant, project=self.project) if self.intern_selection.funding_source not in (InternSelection.GENERAL_FUNDED, InternSelection.ORG_FUNDED): raise PermissionDenied("Outreachy interns cannot be approvde until they have a funding source selected.") # Only allow approved organizers to approve interns if not request.user.is_staff: raise PermissionDenied("Only organizers can approve interns.") approval = kwargs['approval'] if approval == "Approved": self.intern_selection.organizer_approved = True elif approval == "Rejected": self.intern_selection.organizer_approved = False elif approval == "Undecided": self.intern_selection.organizer_approved = None self.intern_selection.save() return redirect(reverse('dashboard') + "#intern-{project}-{applicant}".format( project=self.intern_selection.project.slug, applicant=self.intern_selection.applicant.applicant.pk)) class AlumStanding(LoginRequiredMixin, ComradeRequiredMixin, reversion.views.RevisionMixin, View): def post(self, request, *args, **kwargs): # Only allow approved organizers to approve interns if not request.user.is_staff: raise PermissionDenied("Only organizers can approve interns.") username = kwargs['applicant_username'] that_round = get_object_or_404(RoundPage, slug=kwargs['round_slug']) # FIXME - also need a method to hide AlumInfo set_project_and_applicant(self, that_round) self.intern_selection = get_object_or_404(InternSelection, applicant=self.applicant, project=self.project) standing = kwargs['standing'] if kwargs['standing'] == "Good": self.intern_selection.in_good_standing = True elif kwargs['standing'] == "Failed": self.intern_selection.in_good_standing = False self.intern_selection.save() return redirect('alums') # Passed round_slug, community_slug, project_slug, (get applicant from request.user) class InternAgreementSign(LoginRequiredMixin, ComradeRequiredMixin, CreateView): model = SignedContract template_name = 'home/internrelationship_form.html' fields = ('legal_name',) def set_project_and_intern_selection(self): self.current_round = get_object_or_404(RoundPage, slug=self.kwargs['round_slug']) if not self.current_round.internannounce.has_passed(): raise PermissionDenied("Intern agreements cannot be signed before the interns are announced.") # Since interns can't sign the contract twice, the only people who # could sign a contract for a past round are people who never signed it # when they were supposed to. If somehow that happens (it shouldn't!), # let's not limit which round an intern can sign a contract for. self.project = get_object_or_404(Project, slug=self.kwargs['project_slug'], approval_status=ApprovalStatus.APPROVED, project_round__community__slug=self.kwargs['community_slug'], project_round__participating_round=self.current_round, project_round__approval_status=ApprovalStatus.APPROVED) try: self.intern_selection = InternSelection.objects.get( applicant__applicant=self.request.user.comrade, funding_source__in=(InternSelection.ORG_FUNDED, InternSelection.GENERAL_FUNDED), organizer_approved=True, applicant__application_round=self.current_round ) except InternSelection.DoesNotExist: raise PermissionDenied("You are not an intern in this round.") # Don't allow interns to sign the contract twice if self.intern_selection.intern_contract != None: raise PermissionDenied("You have already signed the intern agreement.") with open(path.join(settings.BASE_DIR, 'docs', 'intern-agreement.md')) as iafile: self.intern_agreement = iafile.read() def get_context_data(self, **kwargs): context = super(InternAgreementSign, self).get_context_data(**kwargs) self.set_project_and_intern_selection() context['intern_agreement_html'] = markdownify(self.intern_agreement) context['project'] = self.project context['community'] = self.project.project_round.community context['intern_selection'] = self.intern_selection context['applicant'] = self.intern_selection.applicant context['current_round'] = self.current_round return context def form_valid(self, form): self.set_project_and_intern_selection() intern_contract = form.save(commit=False) intern_contract.date_signed = datetime.now(timezone.utc) intern_contract.ip_address = self.request.META.get('REMOTE_ADDR') intern_contract.text = self.intern_agreement intern_contract.save() self.intern_selection.intern_contract = intern_contract self.intern_selection.save() return redirect(self.get_success_url()) def get_success_url(self): return reverse('dashboard') def round_statistics(request, round_slug): current_round = get_object_or_404(RoundPage, slug=round_slug) return render(request, 'home/blog/round-statistics.html', { 'current_round': current_round, }) def blog_schedule_changes(request): try: changed_round = RoundPage.objects.get( contributions_open__gte='2019-07-23', contributions_open__lte='2019-12-01', ) except RoundPage.DoesNotExist: changed_round = None return render(request, 'home/blog/2019-07-23-outreachy-schedule-changes.html', { 'changed_round': changed_round, }) def blog_2019_pick_projects(request): try: changed_round = RoundPage.objects.get( contributions_open__gte='2019-07-23', contributions_open__lte='2019-12-01', ) except RoundPage.DoesNotExist: changed_round = None return render(request, 'home/blog/2019-10-01-picking-a-project.html', { 'changed_round': changed_round, }) def blog_2019_10_18_open_projects(request): return render(request, 'home/blog/2019-10-18-open-projects.html') def blog_2020_03_covid(request): return render(request, 'home/blog/2020-03-27-outreachy-response-to-covid-19.html') class InitialMentorFeedbackUpdate(LoginRequiredMixin, reversion.views.RevisionMixin, UpdateView): form_class = modelform_factory(InitialMentorFeedback, fields=( 'provided_onboarding', 'checkin_frequency', 'mentor_response_time', 'mentors_report', 'in_contact', 'asking_questions', 'active_in_public', 'last_contact', 'intern_response_time', 'progress_report', 'full_time_effort', 'payment_approved', 'request_extension', 'extension_date', 'request_termination', 'termination_reason', ), field_classes = { 'in_contact': RadioBooleanField, 'asking_questions': RadioBooleanField, 'active_in_public': RadioBooleanField, 'provided_onboarding': RadioBooleanField, 'share_mentor_feedback_with_community_coordinator': RadioBooleanField, 'full_time_effort': RadioBooleanField, 'payment_approved': RadioBooleanField, 'request_extension': RadioBooleanField, 'request_termination': RadioBooleanField, }, ) def get_object(self): applicant = get_object_or_404(User, username=self.kwargs['username']) mentor_relationship = MentorRelationship.objects.filter( mentor__mentor__account=self.request.user, intern_selection__applicant__applicant__account=applicant, ) if not mentor_relationship.exists(): raise PermissionDenied("You are not a mentor for {}.".format(self.kwargs['username'])) internship = intern_in_good_standing(applicant) if not internship: raise PermissionDenied("{} is not an intern in good standing".format(self.kwargs['username'])) try: feedback = InitialMentorFeedback.objects.get(intern_selection=internship) if not feedback.can_edit(): raise PermissionDenied("This feedback is already submitted and can't be updated right now.") return feedback except InitialMentorFeedback.DoesNotExist: return InitialMentorFeedback(intern_selection=internship) def form_valid(self, form): feedback = form.save(commit=False) feedback.allow_edits = False feedback.ip_address = self.request.META.get('REMOTE_ADDR') feedback.save() return redirect(reverse('dashboard') + '#feedback') class InitialInternFeedbackUpdate(LoginRequiredMixin, reversion.views.RevisionMixin, UpdateView): form_class = modelform_factory(InitialInternFeedback, fields=( 'in_contact', 'asking_questions', 'active_in_public', 'provided_onboarding', 'checkin_frequency', 'last_contact', 'intern_response_time', 'mentor_response_time', 'mentor_support', 'share_mentor_feedback_with_community_coordinator', 'hours_worked', 'time_comments', 'progress_report', ), field_classes = { 'in_contact': RadioBooleanField, 'asking_questions': RadioBooleanField, 'active_in_public': RadioBooleanField, 'provided_onboarding': RadioBooleanField, 'share_mentor_feedback_with_community_coordinator': RadioBooleanField, }, ) def get_object(self): self.internship = intern_in_good_standing(self.request.user) if not self.internship: raise PermissionDenied("The account for {} is not associated with an intern in good standing".format(self.request.user.username)) try: feedback = InitialInternFeedback.objects.get(intern_selection=self.internship) if not feedback.can_edit(): raise PermissionDenied("This feedback is already submitted and can't be updated right now.") return feedback except InitialInternFeedback.DoesNotExist: return InitialInternFeedback(intern_selection=self.internship) def get_context_data(self, **kwargs): context = super(InitialInternFeedbackUpdate, self).get_context_data(**kwargs) context['internship'] = self.internship return context def form_valid(self, form): feedback = form.save(commit=False) feedback.allow_edits = False feedback.ip_address = self.request.META.get('REMOTE_ADDR') feedback.save() return redirect(reverse('dashboard') + '#feedback') def export_feedback(feedback): return { 'intern public name': feedback.intern_selection.applicant.applicant.public_name, 'intern legal name': feedback.intern_selection.applicant.applicant.legal_name, 'intern email address': feedback.intern_selection.applicant.applicant.account.email, 'community': feedback.intern_selection.community_name(), 'mentor public name': feedback.get_mentor_public_name(), 'mentor legal name': feedback.get_mentor_legal_name(), 'mentor email address': feedback.get_mentor_email(), 'feedback submitted on': str(feedback.get_date_submitted()), 'feedback submitted from': feedback.ip_address, 'payment approved': feedback.payment_approved, 'progress report': feedback.progress_report, 'extension requested': feedback.request_extension, 'extension date': str(feedback.extension_date), 'termination requested': feedback.request_termination, 'termination reason': feedback.termination_reason, } @login_required @staff_member_required def initial_mentor_feedback_export_view(request, round_slug): this_round = get_object_or_404(RoundPage, slug=round_slug) interns = this_round.get_approved_intern_selections() dictionary_list = [] for i in interns: try: dictionary_list.append(export_feedback(i.initialmentorfeedback)) except InitialMentorFeedback.DoesNotExist: continue response = JsonResponse(dictionary_list, safe=False) response['Content-Disposition'] = 'attachment; filename="' + round_slug + '-initial-feedback.json"' return response @login_required @staff_member_required def initial_feedback_summary(request, round_slug): current_round = get_object_or_404(RoundPage, slug=round_slug) return render(request, 'home/initial_feedback.html', { 'current_round' : current_round, }, ) class MidpointMentorFeedbackUpdate(LoginRequiredMixin, reversion.views.RevisionMixin, UpdateView): form_class = modelform_factory(MidpointMentorFeedback, fields=( 'mentor_help_response_time', 'mentor_review_response_time', 'mentors_report', 'intern_help_requests_frequency', 'last_contact', 'intern_contribution_frequency', 'intern_contribution_revision_time', 'progress_report', 'full_time_effort', 'payment_approved', 'request_extension', 'extension_date', 'request_termination', 'termination_reason', ), field_classes = { 'in_contact': RadioBooleanField, 'full_time_effort': RadioBooleanField, 'payment_approved': RadioBooleanField, 'request_extension': RadioBooleanField, 'request_termination': RadioBooleanField, }, ) def get_object(self): applicant = get_object_or_404(User, username=self.kwargs['username']) mentor_relationship = MentorRelationship.objects.filter( mentor__mentor__account=self.request.user, intern_selection__applicant__applicant__account=applicant, ) if not mentor_relationship.exists(): raise PermissionDenied("You are not a mentor for {}.".format(self.kwargs['username'])) internship = intern_in_good_standing(applicant) if not internship: raise PermissionDenied("{} is not an intern in good standing".format(self.kwargs['username'])) try: feedback = MidpointMentorFeedback.objects.get(intern_selection=internship) if not feedback.can_edit(): raise PermissionDenied("This feedback is already submitted and can't be updated right now.") return feedback except MidpointMentorFeedback.DoesNotExist: return MidpointMentorFeedback(intern_selection=internship) def form_valid(self, form): feedback = form.save(commit=False) feedback.allow_edits = False feedback.ip_address = self.request.META.get('REMOTE_ADDR') feedback.save() return redirect(reverse('dashboard') + '#feedback') class MidpointInternFeedbackUpdate(LoginRequiredMixin, reversion.views.RevisionMixin, UpdateView): form_class = modelform_factory(MidpointInternFeedback, fields=( 'mentor_help_response_time', 'mentor_review_response_time', 'last_contact', 'mentor_support', 'share_mentor_feedback_with_community_coordinator', 'intern_help_requests_frequency', 'intern_contribution_frequency', 'intern_contribution_revision_time', 'hours_worked', 'time_comments', 'progress_report', ), field_classes = { 'share_mentor_feedback_with_community_coordinator': RadioBooleanField, }, ) def get_object(self): self.internship = intern_in_good_standing(self.request.user) if not self.internship: raise PermissionDenied("The account for {} is not associated with an intern in good standing".format(self.request.user.username)) try: feedback = MidpointInternFeedback.objects.get(intern_selection=self.internship) if not feedback.can_edit(): raise PermissionDenied("This feedback is already submitted and can't be updated right now.") return feedback except MidpointInternFeedback.DoesNotExist: return MidpointInternFeedback(intern_selection=self.internship) def get_context_data(self, **kwargs): context = super(MidpointInternFeedbackUpdate, self).get_context_data(**kwargs) context['internship'] = self.internship return context def form_valid(self, form): feedback = form.save(commit=False) feedback.allow_edits = False feedback.ip_address = self.request.META.get('REMOTE_ADDR') feedback.save() return redirect(reverse('dashboard') + '#feedback') @login_required @staff_member_required def midpoint_mentor_feedback_export_view(request, round_slug): this_round = get_object_or_404(RoundPage, slug=round_slug) interns = this_round.get_approved_intern_selections() dictionary_list = [] for i in interns: try: dictionary_list.append(export_feedback(i.midpointmentorfeedback)) except MidpointMentorFeedback.DoesNotExist: continue response = JsonResponse(dictionary_list, safe=False) response['Content-Disposition'] = 'attachment; filename="' + round_slug + '-midpoint-feedback.json"' return response @login_required @staff_member_required def midpoint_feedback_summary(request, round_slug): current_round = get_object_or_404(RoundPage, slug=round_slug) return render(request, 'home/midpoint_feedback.html', { 'current_round' : current_round, }, ) class FinalMentorFeedbackUpdate(LoginRequiredMixin, reversion.views.RevisionMixin, UpdateView): form_class = modelform_factory(FinalMentorFeedback, fields=( 'mentor_help_response_time', 'mentor_review_response_time', 'mentors_report', 'intern_help_requests_frequency', 'last_contact', 'intern_contribution_frequency', 'intern_contribution_revision_time', 'progress_report', 'full_time_effort', 'payment_approved', 'request_extension', 'extension_date', 'request_termination', 'termination_reason', 'mentoring_recommended', 'blog_frequency', 'blog_prompts_caused_writing', 'blog_prompts_caused_overhead', 'recommend_blog_prompts', 'zulip_caused_intern_discussion', 'zulip_caused_mentor_discussion', 'recommend_zulip', 'feedback_for_organizers', ), field_classes = { 'in_contact': RadioBooleanField, 'full_time_effort': RadioBooleanField, 'payment_approved': RadioBooleanField, 'request_extension': RadioBooleanField, 'request_termination': RadioBooleanField, }, ) def get_object(self): applicant = get_object_or_404(User, username=self.kwargs['username']) mentor_relationship = MentorRelationship.objects.filter( mentor__mentor__account=self.request.user, intern_selection__applicant__applicant__account=applicant, ) if not mentor_relationship.exists(): raise PermissionDenied("You are not a mentor for {}.".format(self.kwargs['username'])) internship = intern_in_good_standing(applicant) if not internship: raise PermissionDenied("{} is not an intern in good standing".format(self.kwargs['username'])) try: feedback = FinalMentorFeedback.objects.get(intern_selection=internship) if not feedback.can_edit(): raise PermissionDenied("This feedback is already submitted and can't be updated right now.") return feedback except FinalMentorFeedback.DoesNotExist: return FinalMentorFeedback(intern_selection=internship) def form_valid(self, form): feedback = form.save(commit=False) feedback.allow_edits = False feedback.ip_address = self.request.META.get('REMOTE_ADDR') feedback.save() return redirect(reverse('dashboard') + '#feedback') class FinalInternFeedbackUpdate(LoginRequiredMixin, reversion.views.RevisionMixin, UpdateView): form_class = modelform_factory(FinalInternFeedback, fields=( 'intern_help_requests_frequency', 'mentor_help_response_time', 'intern_contribution_frequency', 'mentor_review_response_time', 'intern_contribution_revision_time', 'last_contact', 'mentor_support', 'share_mentor_feedback_with_community_coordinator', 'hours_worked', 'time_comments', 'progress_report', 'interning_recommended', 'tech_industry_prep', 'foss_confidence', 'recommend_intern_chat', 'chat_frequency', 'blog_prompts_caused_writing', 'blog_prompts_caused_overhead', 'blog_frequency', 'recommend_blog_prompts', 'zulip_caused_intern_discussion', 'zulip_caused_mentor_discussion', 'recommend_zulip', 'feedback_for_organizers', ), ) def get_object(self): self.internship = intern_in_good_standing(self.request.user) if not self.internship: raise PermissionDenied("The account for {} is not associated with an intern in good standing".format(self.request.user.username)) try: feedback = FinalInternFeedback.objects.get(intern_selection=self.internship) if not feedback.can_edit(): raise PermissionDenied("This feedback is already submitted and can't be updated right now.") return feedback except FinalInternFeedback.DoesNotExist: return FinalInternFeedback(intern_selection=self.internship) def get_context_data(self, **kwargs): context = super(FinalInternFeedbackUpdate, self).get_context_data(**kwargs) context['internship'] = self.internship return context def form_valid(self, form): feedback = form.save(commit=False) feedback.allow_edits = False feedback.ip_address = self.request.META.get('REMOTE_ADDR') feedback.save() return redirect(reverse('dashboard') + '#feedback') @login_required @staff_member_required def final_mentor_feedback_export_view(request, round_slug): this_round = get_object_or_404(RoundPage, slug=round_slug) interns = this_round.get_approved_intern_selections() dictionary_list = [] for i in interns: try: dictionary_list.append(export_feedback(i.finalmentorfeedback)) except FinalMentorFeedback.DoesNotExist: continue response = JsonResponse(dictionary_list, safe=False) response['Content-Disposition'] = 'attachment; filename="' + round_slug + '-final-feedback.json"' return response @login_required @staff_member_required def final_feedback_summary(request, round_slug): current_round = get_object_or_404(RoundPage, slug=round_slug) return render(request, 'home/final_feedback.html', { 'current_round' : current_round, }, ) def alums_page(request): # Get all the older AlumInfo models (before we had round pages) pages = CohortPage.objects.all() old_cohorts = [] for p in pages: old_cohorts.append((p.round_start, p.round_end, AlumInfo.objects.filter(page=p).order_by('community', 'name'))) now = datetime.now(timezone.utc) today = get_deadline_date_for(now) rounds = list(RoundPage.objects.filter( internannounce__lte=today, ).order_by('-internstarts')) current_round = None num_in_good_standing = 0 if rounds: current_round = rounds[0] num_in_good_standing = current_round.get_in_good_standing_intern_selections().count() return render(request, 'home/alums.html', { 'current_round': current_round, 'num_in_good_standing': num_in_good_standing, 'old_cohorts': old_cohorts, 'rounds': [Role(request.user, past_round) for past_round in rounds], }) def privacy_policy(request): with open(path.join(settings.BASE_DIR, 'docs', 'privacy-policy.md')) as policy_file: policy = policy_file.read() return render(request, 'home/privacy_policy.html', { 'privacy_policy': markdownify(policy), }) def survey_opt_out(request, survey_slug): signer = TimestampSigner() try: this_pk = signer.unsign(survey_slug) except BadSignature: raise PermissionDenied("Bad survey opt-out link.") try: survey_tracker = AlumSurveyTracker.objects.get(pk=this_pk) except AlumSurveyTracker.DoesNotExist: raise PermissionDenied("Bad survey opt-out link.") if survey_tracker.alumni_info != None: survey_tracker.alumni_info.survey_opt_out = True survey_tracker.alumni_info.save() elif survey_tracker.intern_info != None: survey_tracker.intern_info.survey_opt_out = True survey_tracker.intern_info.save() return render(request, 'home/survey_opt_out_confirmation.html') class AlumSurveyUpdate(UpdateView): form_class = modelform_factory(AlumSurvey, exclude=( 'survey_tracker', 'survey_date', ), field_classes={ 'community_contact': RadioBooleanField, }, ) def get_object(self): # Decode the timestamped data: # - the PK of the AlumSurveyTracker # # If the timestamp is older than 1 month, display an error message. # # Figure out which model is not null (alumni_info or intern_info) to use. # See if we already have an AlumSurvey that points to this survey tracker. # If not, create it. signer = TimestampSigner() try: this_pk = signer.unsign(self.kwargs['survey_slug'], max_age=timedelta(days=30)) except SignatureExpired: raise PermissionDenied("The survey link has expired.") except BadSignature: raise PermissionDenied("Bad survey link.") try: return AlumSurvey.objects.get(survey_tracker__pk=this_pk) except AlumSurvey.DoesNotExist: tracker = get_object_or_404(AlumSurveyTracker, pk=this_pk) return AlumSurvey(survey_tracker=tracker, survey_date=datetime.now()) # No need to override get_context because we can get everything from # form.instance.survey_tracker def get_success_url(self): return reverse('longitudinal-survey-2018-completed') class Survey2018Notification(LoginRequiredMixin, ComradeRequiredMixin, TemplateView): template_name = 'home/survey_notification.html' def get_alums_and_opt_outs(self): alums = AlumInfo.objects.all() alums_opt_out = [p for p in alums if p.survey_opt_out == True] alums = [p for p in alums if p.survey_opt_out == False] # Only send the survey to interns who have completed their internship past_interns = InternSelection.objects.filter(organizer_approved=True, project__project_round__participating_round__internends__lte=date.today()) past_interns_opt_out = [p for p in past_interns if p.survey_opt_out or p.project.round().internends >= date.today()] past_interns = [p for p in past_interns if not p.survey_opt_out and p.project.round().internends >= date.today()] return alums, alums_opt_out, past_interns, past_interns_opt_out def get_context_data(self, **kwargs): if not self.request.user.is_staff: raise PermissionDenied("You are not authorized to send survey emails.") alums, alums_opt_out, past_interns, past_interns_opt_out = self.get_alums_and_opt_outs() len_queued_surveys = AlumSurveyTracker.objects.filter(survey_date__isnull=True).count() context = super(Survey2018Notification, self).get_context_data(**kwargs) context.update({ 'alums': alums, 'alums_opt_out': alums_opt_out, 'past_interns': past_interns, 'past_interns_opt_out': past_interns_opt_out, 'len_queued_surveys': len_queued_surveys, }) return context def post(self, request, *args, **kwargs): if not self.request.user.is_staff: raise PermissionDenied("You are not authorized to send reminder emails.") alums, alums_opt_out, past_interns, past_interns_opt_out = self.get_alums_and_opt_outs() for a in alums: AlumSurveyTracker.objects.create(alumni_info=a) for p in past_interns: AlumSurveyTracker.objects.create(intern_info=i) return redirect('dashboard') @login_required def applicant_review_summary(request, status): """ For applicant reviewers and staff, show the status of applications that have the specified approval status. """ # Update dashboard.application_summary too if you change anything here. current_round = get_current_round_for_initial_application_review() if not request.user.is_staff and not current_round.is_reviewer(request.user): raise PermissionDenied("You are not authorized to review applications.") applications = ApplicantApproval.objects.filter( application_round=current_round, approval_status=status, ).order_by('pk') if status == ApprovalStatus.PENDING: context_name = 'pending_applications' elif status == ApprovalStatus.REJECTED: context_name = 'rejected_applications' elif status == ApprovalStatus.APPROVED: context_name = 'approved_applications' return render(request, 'home/applicant_review_summary.html', { context_name: applications, }) # Passed action, applicant_username class ApplicantApprovalUpdate(ApprovalStatusAction): model = ApplicantApproval def get_object(self): current_round = get_current_round_for_initial_application_review() return get_object_or_404(ApplicantApproval, applicant__account__username=self.kwargs['applicant_username'], application_round=current_round) class DeleteApplication(LoginRequiredMixin, ComradeRequiredMixin, View): def post(self, request, *args, **kwargs): # Only allow staff to delete initial applications if not request.user.is_staff: raise PermissionDenied("Only Outreachy organizers can delete initial applications.") # This only happens during review, but only makes sense to do while the # applicant still has an opportunity to submit the initial application # again, so we don't use the longer review period here. current_round = get_current_round_for_initial_application() application = get_object_or_404(ApplicantApproval, applicant__account__username=self.kwargs['applicant_username'], application_round=current_round) application.delete() # We need to delete both pending and rejected applications, # so I'm not sure which to redirect to. return redirect('dashboard') class NotifyEssayNeedsUpdating(LoginRequiredMixin, ComradeRequiredMixin, View): def post(self, request, *args, **kwargs): current_round = get_current_round_for_initial_application_review() # Allow staff to ask applicants to revise their essays if not request.user.is_staff: raise PermissionDenied("Only Outreachy organizers can ask applicants to revise their essays.") essay = get_object_or_404(BarriersToParticipation, applicant__application_round=current_round, applicant__applicant__account__username=self.kwargs['applicant_username'], ) essay.applicant_should_update = True essay.save() # Notify applicant their essay needs review email.applicant_essay_needs_updated(essay.applicant.applicant, request) return redirect(essay.applicant.get_preview_url()) class BarriersToParticipationUpdate(LoginRequiredMixin, ComradeRequiredMixin, reversion.views.RevisionMixin, UpdateView): model = BarriersToParticipation fields = [ 'lacking_representation', 'systemic_bias', 'employment_bias', 'barriers_to_contribution', ] def get_object(self): current_round = get_current_round_for_initial_application_review() # Only allow applicants to revise their own essays if self.request.user.comrade.account.username != self.kwargs['applicant_username']: raise PermissionDenied('You can only edit your own essay.') essay = get_object_or_404(BarriersToParticipation, applicant__application_round=current_round, applicant__applicant__account__username=self.kwargs['applicant_username'], ) # Only allow people to edit their essays if the flag has been set. if not essay.applicant_should_update: raise PermissionDenied('You cannot edit your essay at this time.') return essay def get_success_url(self): self.object.applicant_should_update = False self.object.save() return reverse('eligibility-results') class NotifySchoolInformationUpdating(LoginRequiredMixin, ComradeRequiredMixin, View): def post(self, request, *args, **kwargs): current_round = get_current_round_for_initial_application_review() if not request.user.is_staff: raise PermissionDenied("Only Outreachy organizers can ask applicants to revise their school information.") school_info = get_object_or_404(SchoolInformation, applicant__application_round=current_round, applicant__applicant__account__username=self.kwargs['applicant_username'], ) school_info.applicant_should_update = True school_info.save() # Notify applicant their essay needs review email.applicant_school_info_needs_updated(school_info.applicant.applicant, request) return redirect(school_info.applicant.get_preview_url()) class SchoolInformationUpdate(LoginRequiredMixin, ComradeRequiredMixin, reversion.views.RevisionMixin, UpdateView): model = SchoolInformation fields = [ 'current_academic_calendar', 'next_academic_calendar', 'school_term_updates', ] def get_object(self): current_round = get_current_round_for_initial_application_review() # Only allow applicants to revise their own essays if self.request.user.comrade.account.username != self.kwargs['applicant_username']: raise PermissionDenied('You can only edit your own school information.') school_info = get_object_or_404(SchoolInformation, applicant__application_round=current_round, applicant__applicant__account__username=self.kwargs['applicant_username'], ) return school_info def get_context_data(self, **kwargs): current_round = self.object.applicant.application_round school_terms = self.object.applicant.schooltimecommitment_set.all() context = super(SchoolInformationUpdate, self).get_context_data(**kwargs) context.update({ 'current_round': current_round, 'school_terms': school_terms, }) return context def get_success_url(self): self.object.applicant_should_update = False self.object.save() return reverse('eligibility-results') def get_or_create_application_reviewer_and_review(self): # Only allow approved reviewers to rate applications for the current round current_round = get_current_round_for_initial_application_review() try: reviewer = current_round.applicationreviewer_set.approved().get( comrade__account=self.request.user, ) except ApplicationReviewer.DoesNotExist: raise PermissionDenied("You are not currently an approved application reviewer.") application = get_object_or_404(ApplicantApproval, applicant__account__username=self.kwargs['applicant_username'], application_round=current_round) # If the reviewer gave an essay review, update it. Otherwise create a new review. try: review = InitialApplicationReview.objects.get( application=application, reviewer=reviewer) except InitialApplicationReview.DoesNotExist: review = InitialApplicationReview(application=application, reviewer=reviewer) return (application, reviewer, review) class SetReviewOwner(LoginRequiredMixin, ComradeRequiredMixin, View): def post(self, request, *args, **kwargs): application, requester, review = get_or_create_application_reviewer_and_review(self) # Only allow approved reviewers to change review owners if self.kwargs['owner'] == 'None': reviewer = None else: reviewer = get_object_or_404( application.application_round.applicationreviewer_set.approved(), comrade__account__username=self.kwargs['owner'], ) application.review_owner = reviewer application.save() return redirect(application.get_preview_url()) class EssayRating(LoginRequiredMixin, ComradeRequiredMixin, View): def post(self, request, *args, **kwargs): application, reviewer, review = get_or_create_application_reviewer_and_review(self) rating = kwargs['rating'] if rating == "STRONG": review.essay_rating = review.STRONG elif rating == "GOOD": review.essay_rating = review.GOOD elif rating == "MAYBE": review.essay_rating = review.MAYBE elif rating == "UNCLEAR": review.essay_rating = review.UNCLEAR elif rating == "UNRATED": review.essay_rating = review.UNRATED elif rating == "NOBIAS": review.essay_rating = review.NOBIAS elif rating == "NOTUNDERSTOOD": review.essay_rating = review.NOTUNDERSTOOD elif rating == "SPAM": review.essay_rating = review.SPAM review.save() return redirect(application.get_preview_url()) # When reviewing the application's time commitments, there are several red flags # reviewers can set or unset. class ChangeRedFlag(LoginRequiredMixin, ComradeRequiredMixin, View): def post(self, request, *args, **kwargs): flags = [ 'review_school', 'missing_school', 'review_work', 'missing_work', 'incorrect_dates', ] # validate input flag_value = kwargs['flag_value'] flag = kwargs['flag'] if flag_value != 'True' and flag_value != 'False': raise PermissionDenied('Time commitment review flags must be True or False.') if flag not in flags: raise PermissionDenied('Unknown time commitment review flag.') application, reviewer, review = get_or_create_application_reviewer_and_review(self) if flag == "review_school": if flag_value == 'True': review.review_school = True elif flag_value == 'False': review.review_school = False elif flag == "missing_school": if flag_value == 'True': review.missing_school = True elif flag_value == 'False': review.missing_school = False elif flag == "review_work": if flag_value == 'True': review.review_work = True elif flag_value == 'False': review.review_work = False elif flag == "missing_work": if flag_value == 'True': review.missing_work = True elif flag_value == 'False': review.missing_work = False elif flag == "incorrect_dates": if flag_value == 'True': review.incorrect_dates = True elif flag_value == 'False': review.incorrect_dates = False review.save() return redirect(application.get_preview_url()) class ReviewCommentUpdate(LoginRequiredMixin, ComradeRequiredMixin, UpdateView): model = InitialApplicationReview fields = ['comments',] def get_object(self): application, reviewer, review = get_or_create_application_reviewer_and_review(self) return review def get_success_url(self): return self.object.application.get_preview_url() def docs_toc(request): return render(request, 'home/docs/toc.html') def docs_applicant(request): current_round = RoundPage.objects.latest('internannounce') now = datetime.now(timezone.utc) today = get_deadline_date_for(now) try: previous_round = RoundPage.objects.filter( contributions_open__lte=today, ).latest('internstarts') previous_round.today = today except RoundPage.DoesNotExist: previous_round = None return render(request, 'home/docs/applicant_guide.html', { 'current_round': current_round, 'previous_round': previous_round, }) def docs_internship(request): now = datetime.now(timezone.utc) today = get_deadline_date_for(now) five_weeks_ago = today - timedelta(days=7*5) try: applicant_round = RoundPage.objects.get( pingnew__lte=today, internannounce__gt=today, ) except RoundPage.DoesNotExist: applicant_round = None try: intern_round = RoundPage.objects.get( internannounce__lte=today, internends__gt=five_weeks_ago, ) except RoundPage.DoesNotExist: intern_round = None return render(request, 'home/docs/internship_guide.html', { 'applicant_round': applicant_round, 'intern_round': intern_round, }) def travel_stipend(request): rounds = RoundPage.objects.all().order_by('-internstarts') return render(request, 'home/travel_stipend.html', { 'rounds': rounds, }) def opportunities(request): return render(request, 'home/opportunities.html') @login_required def dashboard(request): return render(request, 'home/dashboard.html', { 'sections': get_dashboard_sections(request), })