Beispiel #1
0
 def form_valid(self, form):
     if local_now() < itinerary_available_at(self.trip.trip_date):
         form.errors['__all__'] = ErrorList(["Form not yet available!"])
         return self.form_invalid(form)
     self.trip.info = form.save()
     self.trip.save()
     return super().form_valid(form)
Beispiel #2
0
    def after_lottery(self):
        """ True if it's after the lottery, but takes place before the next one. """
        next_lottery = dateutils.next_lottery()
        past_lottery = dateutils.lottery_time(next_lottery - timedelta(days=7))

        return (dateutils.local_now() > past_lottery and
                self.midnight_before < next_lottery)
Beispiel #3
0
 def get_context_data(self, **kwargs):
     context = super().get_context_data(**kwargs)
     context['itinerary_available_at'] = itinerary_available_at(
         self.trip.trip_date)
     context['info_form_available'] = local_now(
     ) >= context['itinerary_available_at']
     return context
Beispiel #4
0
def ws_lectures_complete():
    """ Return if the mandatory first two lectures have occurred.

    Because IAP varies every year, use traits of the first week of Winter
    School in order to determine if lectures have completed.

    If there are completed Winter School trips this year, then it's no longer
    the first week, and lectures have completed. If it's at least Thursday
    night and there are future trips, we can deduce that lectures have ended.
    """
    now = dateutils.local_now()
    today = now.date()
    dow = now.weekday()
    jan_1 = dateutils.jan_1()
    trips_this_ws = models.Trip.objects.filter(trip_date__gte=jan_1,
                                               activity='winter_school')

    after_thursday = dow > 3 or dow == 3 and now.hour >= 21

    if trips_this_ws.filter(trip_date__lt=today):
        return True
    elif trips_this_ws.filter(trip_date__gte=today) and after_thursday:
        return True
    else:  # It's Winter School, but it's not late enough in the first week
        return False
Beispiel #5
0
    def _post_login_update_password_validity(self, times_password_seen):
        """ After form.login has been invoked, handle password being breached or not.

        This method exists to serve two types of users:
        - those who have a corresponding Participant model (most active users)
        - those who have only a user record (signed up, never finished registration)
        """
        user = self.request.user
        assert user, "Method should be invoked *after* user login."

        # The user may or may not have a participant record linked!
        # If they do have a participant, set the password as insecure (or not)
        participant = models.Participant.from_user(user)
        if participant:
            participant.insecure_password = bool(times_password_seen)
            if times_password_seen is not None:  # (None indicates an API failure)
                participant.password_last_checked = local_now()
            participant.save()

        if times_password_seen:
            subject = (
                f"Participant {participant.pk}" if participant else f"User {user.pk}"
            )
            logger.info("%s logged in with a breached password", subject)

            if not participant:
                # For non-new users lacking a participant (very rare), a message + redirect is fine.
                # If they ignore the reset, they'll be locked out once creating a Participant record.
                messages.error(
                    self.request,
                    'This password has been compromised! Please choose a new password. '
                    'If you use this password on any other sites, we recommend changing it immediately.',
                )
Beispiel #6
0
 def configure_logger(self):
     """ Configure a stream to save the log to the trip. """
     datestring = datetime.strftime(local_now(), "%Y-%m-%dT:%H:%M:%S")
     filename = Path(settings.WS_LOTTERY_LOG_DIR, f"ws_{datestring}.log")
     self.handler = logging.FileHandler(filename)
     self.handler.setLevel(logging.DEBUG)
     self.logger.addHandler(self.handler)
Beispiel #7
0
def lapsed_participants():
    """Return all participants who've not used the system in a long time.

    We exclude anybody who's signed a waiver, paid dues, been on trips, or
    updated their profile recently. We consider a number of factors for
    "activity" so as to minimize the chance that we accidentally consider
    somebody to have lapsed.
    """
    now = date_utils.local_now()
    one_year_ago = now - timedelta(days=365)

    # Participants are required to update their info periodically
    # If a sufficient period of time has lapsed since they last updated,
    # we can deduce that they're likely not going on trips anymore
    multiple_update_periods = timedelta(days=(2.5 * settings.MUST_UPDATE_AFTER_DAYS))
    lapsed_update = Q(profile_last_updated__lt=(now - multiple_update_periods))

    today = now.date()
    active_members = (
        # Anybody with a current membership/waiver is active
        Q(membership__membership_expires__gte=today)
        | Q(membership__waiver_expires__gte=today)
        |
        # Anybody who led or participated in a trip during the last year
        Q(trips_led__trip_date__gte=one_year_ago)
        | Q(trip__trip_date__gte=one_year_ago)
        |
        # Anybody signed up for a trip in the future
        # (Should be disallowed without a current waiver, but might as well check)
        Q(signup__trip__trip_date__gte=today)
    )

    return models.Participant.objects.filter(lapsed_update).exclude(active_members)
Beispiel #8
0
def remind_participants_to_renew():
    """Identify all participants who requested membership reminders, email them.

    This method is designed to be idempotent (one email per participant).
    """
    now = date_utils.local_now()

    # Reminders should be sent ~40 days before the participant's membership has expired.
    # We should only send one reminder every ~365 days or so.
    most_recent_allowed_reminder_time = now - timedelta(days=300)

    participants_needing_reminder = models.Participant.objects.filter(
        # Crucially, we *only* send these reminders to those who've opted in.
        send_membership_reminder=True,
        # Anybody with soon-expiring membership is eligible to renew.
        membership__membership_expires__lte=(
            now + models.Membership.RENEWAL_WINDOW).date(),
        # While it should technically be okay to tell people to renew *after* expiry, don't.
        # We'll run this task daily, targeting each participant for ~40 days before expiry.
        # Accordingly, various edge cases should have been handled some time in those 40 days.
        # The language in emails will suggest that renewal is for an active membership, too.
        membership__membership_expires__gte=now.date(),
    ).exclude(
        # Don't attempt to remind anybody who has already been reminded.
        membershipreminder__reminder_sent_at__gte=
        most_recent_allowed_reminder_time)

    # Farm out the delivery of individual emails to separate workers.
    for pk in participants_needing_reminder.values_list('pk', flat=True):
        logger.info("Identified participant %d as needing renewal reminder",
                    pk)
        remind_lapsed_participant_to_renew.delay(pk)
Beispiel #9
0
def can_attend_trip(user, trip):
    """ Return whether the user's membership allows them to attend the trip.

    Their cached membership may be sufficient to show that the last
    membership/waiver stored allows them to go on the trip. Otherwise, we
    must consult the gear database to be sure whether or not they can go.
    """
    participant = models.Participant.from_user(user, True)
    if not participant:
        return False
    if participant.can_attend(trip):
        return True

    # The first check used the cache, but failed.
    # To be sure they can't attend, we must consult the gear database
    membership = participant.membership
    original_ts = membership.last_cached if membership else local_now()

    try:
        update_membership_cache(participant)
    except OperationalError:
        if sentry.client:
            sentry.client.captureException()
        return True  # Database is down! Just assume they can attend

    participant.membership.refresh_from_db()

    assert participant.membership.last_cached > original_ts

    return participant.can_attend(trip)
Beispiel #10
0
 def configure_logger(self):
     """ Configure a stream to save the log to the trip. """
     datestring = datetime.strftime(local_now(), "%Y-%m-%dT:%H:%M:%S")
     filename = Path(settings.WS_LOTTERY_LOG_DIR, f"ws_{datestring}.log")
     self.handler = logging.FileHandler(filename)
     self.handler.setLevel(logging.DEBUG)
     self.logger.addHandler(self.handler)
Beispiel #11
0
def can_attend_trip(user, trip):
    """ Return whether the user's membership allows them to attend the trip.

    Their cached membership may be sufficient to show that the last
    membership/waiver stored allows them to go on the trip. Otherwise, we
    must consult the gear database to be sure whether or not they can go.
    """
    participant = models.Participant.from_user(user, True)
    if not participant:
        return False
    if participant.can_attend(trip):
        return True

    # The first check used the cache, but failed.
    # To be sure they can't attend, we must consult the gear database
    membership = participant.membership
    original_ts = membership.last_cached if membership else local_now()

    try:
        update_membership_cache(participant)
    except OperationalError:
        if sentry.client:
            sentry.client.captureException()
        return True  # Database is down! Just assume they can attend

    participant.membership.refresh_from_db()

    assert participant.membership.last_cached > original_ts

    return participant.can_attend(trip)
 def get_context_data(self, **kwargs):
     context = super().get_context_data(**kwargs)
     trip = context['trip']
     context['itinerary_available_at'] = itinerary_available_at(trip.trip_date)
     context['info_form_editable'] = trip.info_editable
     context['waiting_to_open'] = local_now() < context['itinerary_available_at']
     return context
Beispiel #13
0
 def form_valid(self, form):
     if local_now() < itinerary_available_at(self.trip.trip_date):
         form.errors['__all__'] = ErrorList(["Form not yet available!"])
         return self.form_invalid(form)
     self.trip.info = form.save()
     self.trip.save()
     return super().form_valid(form)
Beispiel #14
0
 def get_context_data(self, **kwargs):
     context = super().get_context_data(**kwargs)
     context['itinerary_available_at'] = itinerary_available_at(self.trip.trip_date)
     context['info_form_available'] = (
         local_now() >= context['itinerary_available_at']
     )
     return context
Beispiel #15
0
 def __init__(self, execution_datetime=None):
     # It's important that we be able to simulate the future time with `execution_datetime`
     # If test-running the lottery in advance, we want the same ranking to be used later
     self.lottery_runtime = execution_datetime or local_now()
     today = self.lottery_runtime.date()
     self.today = today  # Silences a weird pylint error
     self.jan_1st = self.today.replace(month=1, day=1)
     self.lottery_key = f"ws-{today.isoformat()}"
Beispiel #16
0
 def __init__(self, execution_datetime=None):
     # It's important that we be able to simulate the future time with `execution_datetime`
     # If test-running the lottery in advance, we want the same ranking to be used later
     self.lottery_runtime = execution_datetime or local_now()
     today = self.lottery_runtime.date()
     self.today = today  # Silences a weird pylint error
     self.jan_1st = self.today.replace(month=1, day=1)
     self.lottery_key = f"ws-{today.isoformat()}"
Beispiel #17
0
def reasons_cannot_attend(
        user: Union[models.User, AnonymousUser],
        trip: models.Trip) -> Iterator[enums.TripIneligibilityReason]:
    """Yield reasons why the user is not allowed to attend the trip.

    Their cached membership may be sufficient to show that the last
    membership/waiver stored allows them to go on the trip. Otherwise, we
    must consult the gear database to be sure whether or not they can go.
    """
    if not user.is_authenticated:
        yield enums.TripIneligibilityReason.NOT_LOGGED_IN
        return

    participant = models.Participant.from_user(user, True)
    if not participant:
        yield enums.TripIneligibilityReason.NO_PROFILE_INFO
        return

    reasons = list(participant.reasons_cannot_attend(trip))
    if not any(reason.related_to_membership for reason in reasons):
        # There may be no reasons, or they may just not pertain to membership.
        # In either case, we don't need to refresh membership!
        yield from iter(reasons)
        return

    # The first check identified that the participant cannot attend due to membership problems.
    # It used the cache, so some reasons for failure may have been due to a stale cache.
    # To be sure they can't attend, we must consult the gear database.
    membership = participant.membership
    before_refreshing_ts = local_now()
    original_ts = membership.last_cached if membership else before_refreshing_ts

    try:
        latest_membership = get_latest_membership(participant)
    except requests.exceptions.RequestException:
        capture_exception()
        return

    # (When running tests we often mock away wall time so it's constant)
    if local_now() != before_refreshing_ts:
        assert latest_membership.last_cached > original_ts

    participant.refresh_from_db()
    assert participant.membership is not None, "Cache refresh failed!"
    yield from participant.reasons_cannot_attend(trip)
Beispiel #18
0
    def _leader_emails_missing_itinerary(trips):
        now = date_utils.local_now()
        no_itinerary_trips = (trip for trip in trips if not trip.info)

        for trip in no_itinerary_trips:
            if now < date_utils.itinerary_available_at(trip.trip_date):
                continue  # Not yet able to submit!
            for leader in trip.leaders.all():
                yield leader.email_addr
    def test_old_student_affiliation_dated(self):
        student = factories.ParticipantFactory.create(
            affiliation=
            'S',  # Ambiguous! Is it an MIT student? non-MIT? Undergrad/grad?
            last_updated=date_utils.local_now(),
        )

        self.assertCountEqual(student.problems_with_profile,
                              [enums.ProfileProblem.LEGACY_AFFILIATION])
Beispiel #20
0
def _eligible_trips():
    """Identify all trips that are open for signups, or will be."""
    now = date_utils.local_now()

    upcoming_trips = (models.Trip.objects.filter(
        trip_date__gte=now.date()).filter(signups_close_at__gt=now).order_by(
            'trip_date', 'time_created'))

    return annotated_for_trip_list(upcoming_trips)
Beispiel #21
0
    def form_valid(self, form):
        response = super().form_valid(form)

        if self.request.participant:
            self.request.participant.insecure_password = False
            # NOTE: Technically, we cannot know for sure if the API call to HIBP failed
            # (and thus, we might be updating this timestamp incorrectly)
            # However, we log API failures and could use that information to identify the last success
            self.request.participant.password_last_checked = local_now()
            self.request.participant.save()
        return response
Beispiel #22
0
def trip_edit_buttons(trip, participant, user, hide_approve=False):
    available_at = date_utils.itinerary_available_at(trip.trip_date)
    return {
        'trip': trip,
        'is_chair': perm_utils.chair_or_admin(user, trip.activity),
        'is_creator': trip.creator == participant,
        'is_trip_leader': perm_utils.leader_on_trip(participant, trip, False),
        'hide_approve': hide_approve,  # Hide approval even if user is a chair
        'itinerary_available_at': available_at,
        'available_today': available_at.date() == date_utils.local_date(),
        'info_form_available': date_utils.local_now() >= available_at,
    }
Beispiel #23
0
 def get_context_data(self, **kwargs):
     context = super().get_context_data(**kwargs)
     trip = context['trip']
     context['itinerary_available_at'] = itinerary_available_at(
         trip.trip_date)
     context['info_form_editable'] = trip.info_editable
     if local_now() < context['itinerary_available_at']:
         context['waiting_to_open'] = True
         context['form'].fields.pop('accurate')
         for field in context['form'].fields.values():
             field.disabled = True
     return context
Beispiel #24
0
def refresh_all_membership_cache():
    """ Refresh all membership caches in the system.

    After this is run, every participant in the system will have membership
    information that is no more than a week old.
    """
    last_week = local_now() - timedelta(weeks=1)
    needs_update = Q(membership__isnull=True) | Q(membership__last_cached__lt=last_week)

    all_participants = models.Participant.objects.select_related('membership')
    for par in all_participants.filter(needs_update):
        update_membership_cache(par)
Beispiel #25
0
def trip_edit_buttons(trip, participant, user, hide_approve=False):
    available_at = date_utils.itinerary_available_at(trip.trip_date)
    return {
        'trip': trip,
        'is_chair': perm_utils.chair_or_admin(user, trip.activity),
        'is_creator': trip.creator == participant,
        'is_trip_leader': perm_utils.leader_on_trip(participant, trip, False),
        'hide_approve': hide_approve,  # Hide approval even if user is a chair
        'itinerary_available_at': available_at,
        'available_today': available_at.date() == date_utils.local_date(),
        'info_form_available': date_utils.local_now() >= available_at
    }
Beispiel #26
0
    def __init__(self, *args, **kwargs):
        allowed_programs = kwargs.pop("allowed_programs", None)
        super().__init__(*args, **kwargs)
        trip = self.instance

        if trip.pk is None:  # New trips must have *all* dates/times in the future
            now = local_now()
            # (the `datetime-local` inputs don't take timezones at all)
            # We specify only to the minutes' place so that we don't display seconds
            naive_iso_now = now.replace(tzinfo=None).isoformat(
                timespec='minutes')

            self.fields['trip_date'].widget.attrs['min'] = now.date(
            ).isoformat()
            # There is *extra* dynamic logic that (open < close at <= trip date)
            # However, we can at minimum enforce that times occur in the future
            self.fields['signups_open_at'].widget.attrs['min'] = naive_iso_now
            self.fields['signups_close_at'].widget.attrs['min'] = naive_iso_now

        # Use the participant queryset to cover an edge case:
        # editing an old trip where one of the leaders is no longer a leader!
        self.fields[
            'leaders'].queryset = models.Participant.objects.get_queryset()
        self.fields['leaders'].help_text = None  # Disable "Hold command..."

        # We'll dynamically hide the level widget on GET if it's not a winter trip
        # On POST, we only want this field required for winter trips
        program = self.data.get('program')
        program_enum = enums.Program(program) if program else None
        self.fields['level'].required = (program_enum
                                         and program_enum.winter_rules_apply())

        initial_program: Optional[
            enums.Program] = trip.pk and trip.program_enum

        if allowed_programs is not None:
            self.fields['program'].choices = list(
                self._allowed_program_choices(allowed_programs))

            # If it's currently WS, the WS program is almost certainly what's desired for new trips.
            if (enums.Program.WINTER_SCHOOL in allowed_programs
                    and is_currently_iap() and not trip.pk):
                initial_program = enums.Program.WINTER_SCHOOL

        self._init_wimp()

        # (No need for `ng-init`, we have a custom directive)
        self.fields['leaders'].widget.attrs['data-ng-model'] = 'leaders'

        _bind_input(self,
                    'program',
                    initial=initial_program and initial_program.value)
        _bind_input(self, 'algorithm', initial=trip and trip.algorithm)
Beispiel #27
0
def refresh_all_membership_cache():
    """ Refresh all membership caches in the system.

    After this is run, every participant in the system will have membership
    information that is no more than a week old.
    """
    last_week = local_now() - timedelta(weeks=1)
    needs_update = (Q(membership__isnull=True)
                    | Q(membership__last_cached__lt=last_week))

    all_participants = models.Participant.objects.select_related('membership')
    for par in all_participants.filter(needs_update):
        update_membership_cache(par)
Beispiel #28
0
 def form_valid(self, form):
     response = super().form_valid(form)
     if self.request.participant:
         models.PasswordQuality.objects.update_or_create(
             participant=self.request.participant,
             defaults={
                 'is_insecure': False,
                 # NOTE: Technically, we cannot know for sure if the API call to HIBP failed
                 # (and thus, we might be updating this timestamp incorrectly)
                 # However, we log API failures and could use that information to identify the last success.
                 'last_checked': local_now(),
             },
         )
     return response
Beispiel #29
0
    def can_reapply(self, latest_application):
        """ Winter School allows one application per year.

        Other activities just impose a reasonable waiting time.
        """
        if not latest_application:
            return True  # Not "re-applying," just applying for first time

        if latest_application.activity == models.LeaderRating.WINTER_SCHOOL:
            return latest_application.year < self.application_year

        # Allow upgrades after 2 weeks, repeat applications after ~6 months
        waiting_period_days = 14 if latest_application.rating_given else 180
        time_passed = local_now() - latest_application.time_created
        return time_passed > timedelta(days=waiting_period_days)
Beispiel #30
0
    def can_reapply(self, latest_application):
        """ Winter School allows one application per year.

        Other activities just impose a reasonable waiting time.
        """
        if not latest_application:
            return True  # Not "re-applying," just applying for first time

        if latest_application.activity == models.LeaderRating.WINTER_SCHOOL:
            return latest_application.year < self.application_year

        # Allow upgrades after 2 weeks, repeat applications after ~6 months
        waiting_period_days = 14 if latest_application.rating_given else 180
        time_passed = local_now() - latest_application.time_created
        return time_passed > timedelta(days=waiting_period_days)
Beispiel #31
0
    def make_fcfs(self, signups_open_at=None):
        """ Set the algorithm to FCFS, adjust signup times appropriately. """
        self.algorithm = 'fcfs'
        now = dateutils.local_now()
        if signups_open_at:
            self.signups_open_at = signups_open_at
        elif dateutils.wed_morning() <= now < dateutils.closest_wed_at_noon():
            # If posted between lottery time and noon, make it open at noon
            self.signups_open_at = dateutils.closest_wed_at_noon()
        else:
            self.signups_open_at = now

        # If a Winter School trip is last-minute, occurring mid-week,
        # we end up with a close time in the past (preventing trip creation)
        if self.fcfs_close_time > now:
            self.signups_close_at = self.fcfs_close_time
Beispiel #32
0
class Car(models.Model):
    # As long as this module is reloaded once a year, this is fine
    # (First license plates were issued in Mass in 1903)
    year_min, year_max = 1903, dateutils.local_now().year + 2
    # Loosely validate - may wish to use international plates in the future
    license_plate = models.CharField(max_length=31, validators=[alphanum])
    state = USStateField()
    make = models.CharField(max_length=63)
    model = models.CharField(max_length=63)
    year = models.PositiveIntegerField(validators=[MaxValueValidator(year_max),
                                                   MinValueValidator(year_min)])
    color = models.CharField(max_length=63)

    def __unicode__(self):
        car_info = "{} {} {} {}".format(self.color, self.year, self.make, self.model)
        registration_info = "-".join([self.license_plate, self.state])
        return "{} ({})".format(car_info, registration_info)
Beispiel #33
0
def complain_if_missing_itineraries(request):
    """ Create messages if the leader needs to complete trip itineraries. """
    if not ws.utils.perms.is_leader(request.user):
        return

    now = dateutils.local_now()

    # Most trips require itineraries, but some (TRS, etc.) do not
    # All WS trips require itineraries, though
    future_trips_without_info = request.participant.trips_led.filter(
        trip_date__gte=now.date(), info__isnull=True,
        activity='winter_school').values_list('pk', 'trip_date', 'name')

    for trip_pk, trip_date, name in future_trips_without_info:
        if now > dateutils.itinerary_available_at(trip_date):
            trip_url = reverse('trip_itinerary', args=(trip_pk, ))
            msg = (f'Please <a href="{trip_url}">submit an itinerary for '
                   f'{escape(name)}</a> before departing!')
            messages.warning(request, msg, extra_tags='safe')
    def _save_forms(self, user, post_forms):
        """ Given completed, validated forms, handle saving all.

        If no CarForm is supplied, a participant's existing car will be removed.

        Returns the saved Participant object.

        :param post_forms: Dictionary of <template_name>: <form>
        """
        participant = post_forms['participant_form'].save(commit=False)
        if participant.pk:  # Existing participant! Lock for UPDATE now.
            # (we don't also lock other objects via JOIN since those can be NULL)
            models.Participant.objects.filter(
                pk=participant.pk).select_for_update()

        e_contact = post_forms['emergency_contact_form'].save()
        e_info = post_forms['emergency_info_form'].save(commit=False)
        e_info.emergency_contact = e_contact
        e_info = post_forms['emergency_info_form'].save()

        participant.user_id = user.id
        participant.emergency_info = e_info

        del_car = False
        try:
            car = post_forms['car_form'].save()
        except KeyError:  # No CarForm posted
            # If Participant existed and has a stored Car, mark it for deletion
            if participant.car:
                car = participant.car
                participant.car = None
                del_car = True
        else:
            participant.car = car

        if participant == self.request.participant:
            participant.profile_last_updated = date_utils.local_now()
        participant.save()
        if del_car:
            car.delete()
        return participant
def complain_if_missing_itineraries(request):
    """ Create messages if the leader needs to complete trip itineraries. """
    if not ws.utils.perms.is_leader(request.user):
        return

    now = dateutils.local_now()

    # Most trips require itineraries, but some (TRS, etc.) do not
    # All WS trips require itineraries, though
    future_trips_without_info = request.participant.trips_led.filter(
        trip_date__gte=now.date(), info__isnull=True, activity='winter_school'
    ).values_list('pk', 'trip_date', 'name')

    for trip_pk, trip_date, name in future_trips_without_info:
        if now > dateutils.itinerary_available_at(trip_date):
            trip_url = reverse('trip_itinerary', args=(trip_pk,))
            msg = (
                f'Please <a href="{trip_url}">submit an itinerary for '
                f'{escape(name)}</a> before departing!'
            )
            messages.warning(request, msg, extra_tags='safe')
Beispiel #36
0
def remind_lapsed_participant_to_renew(participant_id: int):
    """A task which should only be called by `remind_participants_to_renew'.

    Like its parent task, is designed to be idempotent (so we only notify
    participants once per year).
    """
    participant = models.Participant.objects.get(pk=participant_id)

    # It's technically possible that the participant opted out in between execution
    if not participant.send_membership_reminder:
        logger.info("Not reminding participant %s, who since opted out",
                    participant.pk)
        return

    now = date_utils.local_now()
    with transaction.atomic():
        (reminder, created) = (
            models.MembershipReminder.objects
            # Make sure we get an exclusive lock to prevent sending a redundant email.
            .select_for_update()
            # It's possible this is the first reminder! If so, create it now.
            # (the unique constraint on participant will ensure other queries can't insert
            .get_or_create(
                participant=participant,
                defaults={'reminder_sent_at': now},
            ))

        if not created:
            logger.info("Last reminded %s: %s", participant,
                        reminder.reminder_sent_at)
            # Reminders should be sent ~40 days before the participant's membership has expired.
            # We should only send one reminder every ~365 days or so.
            # Pick 300 days as a sanity check that we send one message yearly (+/- some days)
            if reminder.reminder_sent_at > (now - timedelta(days=300)):
                raise ValueError(
                    f"Mistakenly trying to notify {participant} to renew")

        # (Note that this method makes some final assertions before delivering)
        # If the email succeeds, we'll commit the reminder record (else rollback)
        renew.send_email_reminding_to_renew(participant)
Beispiel #37
0
    def _complain_if_missing_itineraries(self):
        """ Create messages if the leader needs to complete trip itineraries. """
        now = date_utils.local_now()

        # Most trips require itineraries, but some (TRS, etc.) do not
        # All WS trips require itineraries, though
        future_trips_without_info = (
            self.request.participant.trips_led.filter(
                trip_date__gte=now.date(),
                info__isnull=True,
                program=enums.Program.WINTER_SCHOOL.value,
            ).order_by('trip_date')  # Warn about closest trips first!
            .values_list('pk', 'trip_date', 'name'))

        for trip_pk, trip_date, name in future_trips_without_info:
            if now > date_utils.itinerary_available_at(trip_date):
                trip_url = reverse('trip_itinerary', args=(trip_pk, ))
                msg = (f'Please <a href="{trip_url}">submit an itinerary for '
                       f'{escape(name)}</a> before departing!')
                self.add_unique_message(messages.WARNING,
                                        msg,
                                        extra_tags='safe')
Beispiel #38
0
def outstanding_items(emails):
    if not emails:
        return []
    cursor = connections['geardb'].cursor()
    cursor.execute(
        """
        select g.id, gt.type_name, gt.rental_amount, r.checkedout
        from rentals  r
          join gear g on g.id = r.gear_id
          join gear_types gt on gt.id = g.type
        where returned is null
          and person_id in (select id from people where email in %s)
        """, [tuple(emails)])
    items = [{
        'id': gear_id,
        'name': name,
        'cost': cost,
        'checkedout': checkedout
    } for gear_id, name, cost, checkedout in cursor.fetchall()]
    for item in items:
        item['overdue'] = local_now() - item['checkedout'] > timedelta(
            weeks=10)
    return items
Beispiel #39
0
    def test_non_ws_trips_ignored(self):
        """ Participants with signups for non-WS trips are handled.

        Namely,
        - a participant's signup for a non-WS trip does not affect the lottery
        - The cleanup phase of the lottery does not modify any non-WS trips
        """
        outside_iap_trip = factories.TripFactory.create(
            name='non-WS trip',
            algorithm='lottery',
            program=enums.Program.WINTER_NON_IAP.value,
            trip_date=date(2020, 1, 18),
        )
        office_day = factories.TripFactory.create(
            name='Office Day',
            algorithm='fcfs',
            program=enums.Program.NONE.value,
            trip_date=date(2020, 1, 19),
        )

        # Sign up the hiker for all three trips!
        for trip in [self.hike, outside_iap_trip, office_day]:
            factories.SignUpFactory.create(participant=self.hiker, trip=trip)

        runner = run.WinterSchoolLotteryRunner()
        runner()

        # The participant was placed on their desired WS trip!
        ws_signup = models.SignUp.objects.get(trip=self.hike,
                                              participant=self.hiker)
        self.assertTrue(ws_signup.on_trip)

        # Neither of the other two trips had their algorithm or start time adjusted
        outside_iap_trip.refresh_from_db()
        self.assertEqual(outside_iap_trip.algorithm, 'lottery')
        office_day.refresh_from_db()
        self.assertEqual(office_day.signups_open_at, date_utils.local_now())
Beispiel #40
0
 def __init__(self, execution_datetime=None):
     self.execution_datetime = execution_datetime or local_now()
     self.ranker = WinterSchoolParticipantRanker(self.execution_datetime)
     super().__init__()
     self.configure_logger()