예제 #1
0
    def test_unrequited_requests(self):
        """We tell participants if others have requested to be paired with them."""
        par = factories.ParticipantFactory.create()
        factories.LotteryInfoFactory.create(paired_with=par,
                                            participant__name='Bob Li')
        soup = self._render(par, user_viewing=True)
        self.assertEqual(
            strip_whitespace(soup.text),
            'Bob Li has requested to be paired with you. Change your pairing preferences',
        )

        # Add one more request, note that they're alphabetized
        factories.LotteryInfoFactory.create(paired_with=par,
                                            participant__name='Ana Ng')
        soup = self._render(par, user_viewing=True)
        self.assertEqual(
            [strip_whitespace(tag.text) for tag in soup.find_all('li')],
            [
                'Ana Ng has requested to be paired with you.',
                'Bob Li has requested to be paired with you.',
            ],
        )
        # We don't link to any participant, since normal users cannot see that
        self.assertEqual(
            soup.find('a').text, 'Change your pairing preferences')
예제 #2
0
    def test_warn_if_missing_after_lectures(self):
        """If lectures have ended, we warn participants without attendance."""
        factories.LectureAttendanceFactory.create(
            participant=self.participant, year=2022
        )
        # Participants cannot set attendance; that time has passed.
        self._allow_setting_attendance(False)

        # A future WS trip exists, which is a strong clue: it must be first week of WS
        with freeze_time("2022-01-04 12:00:00 EST"):
            factories.TripFactory(
                trip_date=date(2022, 1, 8),
                program=enums.Program.WINTER_SCHOOL.value,
            )

        # It's after 9 pm on Thursday (with future WS trips). Must be lecture day.
        with freeze_time("2022-01-06 22:00:00 EST"):
            resp = self.client.get('/')

        # This participant did not record their attendance!
        soup = BeautifulSoup(resp.content, 'html.parser')
        attendance = soup.find('h3', string='Lecture Attendance')
        self.assertEqual(
            strip_whitespace(attendance.find_next('p').text),
            "Attended You have attended this year's lectures!",
        )
예제 #3
0
    def test_no_itinerary_but_show_participants(self):
        trip = factories.TripFactory.create(info=None)
        factories.SignUpFactory.create(
            trip=trip,
            on_trip=True,
            participant__name="Car Owner",
            participant__car=factories.CarFactory.create(make="Honda",
                                                         model="Odyssey"),
        )
        factories.SignUpFactory.create(
            trip=trip,
            on_trip=True,
            participant__name="Car Renter",
            participant__car=factories.CarFactory.create(make="Subaru",
                                                         model="Outback"),
        )
        soup = self._render(trip, show_participants_if_no_itinerary=True)

        # We have a table of all participants
        self.assertTrue(soup.find('h4', text='Participants'))
        self.assertTrue(soup.find('td', text='Car Renter'))
        self.assertTrue(soup.find('td', text='Car Owner'))

        # We show a table of all drivers out of caution
        self.assertTrue(soup.find('h4', text='Drivers'))
        warning = soup.find(class_='alert alert-warning')
        self.assertIn(
            "all trip-goers that submitted car information",
            strip_whitespace(warning.text),
        )
        self.assertTrue(soup.find('td', text='Subaru Outback'))
        self.assertTrue(soup.find('td', text='Honda Odyssey'))
예제 #4
0
    def test_signup_exists_but_no_longer_leader(self):
        """Test the edge case where the user *was* signed up as a leader, but no longer is."""
        trip = factories.TripFactory.create(
            allow_leader_signups=True,
            program=enums.Program.NONE.value,
            notes='',
        )
        self.participant.leaderrating_set.add(
            factories.LeaderRatingFactory.create(
                participant=self.participant,
                activity=enums.Activity.HIKING.value,
            ))

        self.client.post('/trips/signup/leader/', {'trip': trip.pk})
        self.assertTrue(models.LeaderSignUp.objects.filter(trip=trip).exists())

        trip.leaders.clear()
        resp = self.client.post('/trips/signup/leader/', {'trip': trip.pk})
        soup = BeautifulSoup(resp.content, 'html.parser')
        warning = soup.find(class_='alert-danger')
        # Note: This is a temporary solution.
        # In the future, leaders should be able to self-add themselves back.
        self.assertEqual(
            strip_whitespace(warning.text),
            'Already signed up as a leader on this trip! Contact the trip organizer to be re-added.',
        )
예제 #5
0
    def test_2_activity_chairs(self):
        Group.objects.get(name='hiking_chair').user_set.set([
            self.participant.user,
            factories.ParticipantFactory.create().user
        ])

        # Neither chair have recommended one application
        no_recs_application = factories.HikingLeaderApplicationFactory.create()

        response_1, soup_1 = self._get('/hiking/applications/')
        self.assertEqual(response_1.context_data['needs_rec'],
                         [no_recs_application])
        self.assertEqual(response_1.context_data['needs_rating'], [])
        self.assertEqual(
            strip_whitespace(
                soup_1.find('div', attrs={'class', 'alert-info'}).text),
            'Recommend some leader ratings for other applications to populate this list.',
        )

        # Viewing chair gave a recommendation for one application
        one_rec_application = factories.HikingLeaderApplicationFactory.create()
        factories.LeaderRecommendationFactory.create(
            creator=self.participant,
            participant=one_rec_application.participant,
            activity=enums.Activity.HIKING.value,
        )

        response, soup = self._get('/hiking/applications/')
        self.assertEqual(response.context_data['needs_rec'],
                         [no_recs_application])
        self.assertEqual(response.context_data['needs_rating'],
                         [one_rec_application])
        self.assertIsNone(soup.find('div', attrs={'class', 'alert-info'}))
예제 #6
0
    def test_update_rescinds_approval(self):
        leader = factories.ParticipantFactory.create()
        self.client.force_login(leader.user)
        factories.LeaderRatingFactory.create(
            participant=leader, activity=enums.Activity.CLIMBING.value
        )
        trip = factories.TripFactory.create(
            creator=leader,
            program=enums.Program.CLIMBING.value,
            trip_date=date(2019, 3, 2),
            chair_approved=True,
        )

        edit_resp, soup = self._get(f'/trips/{trip.pk}/edit/')
        self.assertTrue(edit_resp.context['update_rescinds_approval'])

        form = soup.find('form')
        form_data = dict(self._form_data(form))

        self.assertEqual(
            strip_whitespace(soup.find(class_='alert-warning').text),
            'This trip has been approved by the activity chair. '
            'Making any changes will rescind this approval.',
        )

        # Upon form submission, we're redirected to the new trip's page!
        resp = self.client.post(f'/trips/{trip.pk}/edit/', form_data, follow=False)
        self.assertEqual(resp.status_code, 302)
        self.assertEqual(resp.url, f'/trips/{trip.pk}/')

        # We can see that chair approval is now removed.
        trip = models.Trip.objects.get(pk=trip.pk)
        self.assertFalse(trip.chair_approved)
예제 #7
0
    def test_user_is_not_viewing(self):
        """We render links to other participants (for leaders and admins)."""
        arthur = factories.ParticipantFactory.create(name='King Arthur')

        # Bob (and bob's pairing request) come first, but we'll sort for consistency
        bob = factories.ParticipantFactory.create(name='Bob Li')
        ana = factories.ParticipantFactory.create(name='Ana Ng')
        factories.LotteryInfoFactory.create(paired_with=arthur,
                                            participant=ana)
        factories.LotteryInfoFactory.create(paired_with=arthur,
                                            participant=bob)

        soup = self._render(arthur, user_viewing=False)
        self.assertEqual(
            [strip_whitespace(tag.text) for tag in soup.find_all('li')],
            [
                'Ana Ng has requested to be paired with King Arthur.',
                'Bob Li has requested to be paired with King Arthur.',
            ],
        )

        # We just link to the requesting users, no "change your preferences"
        self.assertEqual(
            [tag['href'] for tag in soup.find_all('a')],
            [f'/participants/{ana.pk}/', f'/participants/{bob.pk}/'],
        )
예제 #8
0
    def test_made_unrequited_requests(self):
        """We warn users if they requested to be paired with somebody else."""
        par = factories.ParticipantFactory.create()
        ringo = factories.ParticipantFactory.create(name='Ringo Starr')
        factories.LotteryInfoFactory.create(participant=par, paired_with=ringo)
        soup = self._render(par, user_viewing=True)
        self.assertEqual(
            strip_whitespace(soup.text),
            'Requested to be paired with Ringo Starr. '
            'Until Ringo Starr does the same, no effort will be made to place you both on the same trip. '
            'Change your pairing preferences',
        )

        self.assertEqual(
            strip_whitespace(self._render(par, user_viewing=False).text),
            'Requested to be paired with Ringo Starr.',
        )
예제 #9
0
 def test_no_itinerary(self):
     trip = factories.TripFactory.create(info=None)
     soup = self._render(trip)
     danger = soup.find(class_='alert alert-danger')
     self.assertEqual(
         strip_whitespace(danger.text),
         "A detailed trip itinerary has not been submitted for this trip!",
     )
예제 #10
0
 def test_preamble(self):
     _response, soup = self._get('/hiking/leaders/apply/')
     self.assertEqual(strip_whitespace(soup.find('h1').text),
                      'Hiking Leader Application')
     self.assertEqual(
         soup.find('p').text,
         "Please complete and submit the form below if you're interested in becoming a MITOC Hiking Leader.",
     )
예제 #11
0
    def test_leaders_can_view_others(self):
        par = factories.ParticipantFactory.create(user_id=self.user.pk)

        factories.FeedbackFactory.create(participant=self.participant)
        factories.FeedbackFactory.create(
            participant=self.participant,
            showed_up=False,
            comments='Slept through their alarm, did not answer phone calls',
        )

        # Any leader may view - it doesn't matter which activity!
        factories.LeaderRatingFactory.create(participant=par)
        self.assertTrue(perm_utils.is_leader(self.user))
        random.seed("Set seed, for predictable 'scrambling'")
        response = self.client.get(f'/participants/{self.participant.pk}/')
        self.assertEqual(response.status_code, 200)

        # When viewing, comments are initially scrambled
        soup = BeautifulSoup(response.content, 'html.parser')
        feedback = soup.find(id='feedback').find_next('table')
        self.assertEqual(
            strip_whitespace(feedback.find_next('td').text),
            'Flaked! oo srephh ,twihlien lnSd rmleartpagtsal choeanrudt',
        )

        # There's a button which enables us to view this feedback, unscrambled.
        reveal = soup.find(
            'a', href=f'/participants/{self.participant.pk}/?show_feedback=1'
        )
        self.assertTrue(reveal)
        with mock.patch.object(logger, 'info') as log_info:
            response = self.client.get(reveal.attrs['href'])
        log_info.assert_called_once_with(
            "%s (#%d) viewed feedback for %s (#%d)",
            par,
            par.pk,
            self.participant,
            self.participant.pk,
        )
        soup = BeautifulSoup(response.content, 'html.parser')
        feedback = soup.find(id='feedback').find_next('table')
        self.assertEqual(
            strip_whitespace(feedback.find_next('td').text),
            'Flaked! Slept through their alarm, did not answer phone calls',
        )
예제 #12
0
 def test_preamble(self):
     _response, soup = self._get('/climbing/leaders/apply/')
     expected_preamble = (
         'Please fill out the form below to apply to become a climbing leader. '
         'In addition to this form, we require that applicants have '
         'two recommendations from current MITOC climbing leaders.'
     )
     self.assertEqual(
         strip_whitespace(soup.find('h1').text), 'Climbing Leader Application'
     )
     self.assertEqual(soup.find('p').text, expected_preamble)
예제 #13
0
 def test_bad_token_not_logged_in(self):
     soup = self._get('/preferences/email/unsubscribe/bad_token/')
     self.assertEqual(
         ['Invalid token, cannot unsubscribe automatically.'],
         [alert.text.strip() for alert in soup.find_all(class_='alert')],
     )
     self.assertTrue(soup.find('a', href='/preferences/email/'))
     self.assertEqual(
         strip_whitespace(soup.find('p', class_="lead").text),
         "Edit your email preferences (login required)",
     )
예제 #14
0
    def test_upcoming_trips_can_be_filtered(self):
        """If supplying an 'after' date in the future, that still permits filtering!"""
        _next_week = factories.TripFactory.create(trip_date='2019-02-22')
        next_month = factories.TripFactory.create(trip_date='2019-03-22')
        response, soup = self._get('/trips/?after=2019-03-15')
        self._expect_link_for_date(soup, '2018-03-15')

        # We remove the RSS + email buttons
        header = soup.body.find('h3')
        self.assertEqual(strip_whitespace(header.text), 'Trips after Mar 15, 2019')

        # The trip next month is included, but not next week (since we're filtering ahead)
        self._expect_current_trips(response, [next_month.pk])
예제 #15
0
    def test_no_upcoming_trips(self):
        perm_utils.make_chair(self.user, enums.Activity.WINTER_SCHOOL)
        response = self.client.get('/trips/medical/')
        self.assertFalse(response.context['trips'].exists())

        soup = BeautifulSoup(response.content, 'html.parser')

        header = soup.find('h1', string="WIMP Information Sheet")
        self.assertTrue(header)
        self.assertEqual(
            strip_whitespace(header.find_next('p').text),
            "This page contains all known medical information for trips taking place on or after Jan. 1, 2020.",
        )
        self.assertTrue(soup.find('p', string="No upcoming trips."))
예제 #16
0
 def test_participant_since_deleted(self):
     """We handle the case of a valid token for a since-deleted participant."""
     par = factories.ParticipantFactory.create()
     token = unsubscribe.generate_unsubscribe_token(par)
     par.delete()
     soup = self._get(f'/preferences/email/unsubscribe/{token}/')
     self.assertEqual(
         ['Participant no longer exists'],
         [alert.text.strip() for alert in soup.find_all(class_='alert')],
     )
     self.assertTrue(soup.find('a', href='/preferences/email/'))
     self.assertEqual(
         strip_whitespace(soup.find('p', class_="lead").text),
         "Edit your email preferences (login required)",
     )
예제 #17
0
    def test_leading_another_trip_that_day(self):
        trip = self._make_trip(name="Some Trip That Same Day")
        leader = self._leader(trip.activity)
        trip.leaders.add(leader)

        same_day_trip = self._make_trip()
        self.assertEqual(same_day_trip.trip_date, trip.trip_date)

        # The leader is warned that they have a trip that day!
        soup = self._render(leader, same_day_trip)
        danger = soup.find(class_='alert alert-danger')
        self.assertEqual(
            strip_whitespace(danger.text),
            "Are you sure you can attend? You're also attending Some Trip That Same Day.",
        )
        # They can still sign up, though!
        self.assertTrue(soup.find('form'))
예제 #18
0
 def test_hiding_sensitive_info(self):
     old_trip = factories.TripFactory.create(
         trip_date=date(2019, 10, 10),
         info=factories.TripInfoFactory.create())
     factories.SignUpFactory.create(
         trip=old_trip,
         on_trip=True,
         participant__emergency_info__allergies="bee stings",
     )
     soup = self._render(old_trip)
     info = soup.find(class_='alert alert-info')
     self.assertIn(
         "To preserve participant privacy, sensitive medical information has been redacted",
         strip_whitespace(info.text),
     )
     self.assertFalse(soup.find(text="bee stings"))
     self.assertTrue(soup.find(text="redacted"))
예제 #19
0
    def test_signed_up_for_another_trip_that_day(self):
        trip = self._make_trip(name="Some Trip That Same Day")

        signup = factories.SignUpFactory.create(trip=trip, on_trip=True)

        same_day_trip = self._make_trip()
        self.assertEqual(same_day_trip.trip_date, trip.trip_date)

        # The participant is warned if trying to sign up that they have a trip that day!
        soup = self._render(signup.participant, same_day_trip)
        danger = soup.find(class_='alert alert-danger')
        self.assertEqual(
            strip_whitespace(danger.text),
            "Are you sure you can attend? You're also attending Some Trip That Same Day.",
        )
        # They can still sign up, though!
        self.assertTrue(soup.find('form'))
예제 #20
0
    def test_1_activity_chair(self):
        Group.objects.get(name='hiking_chair').user_set.set(
            [self.participant.user])

        application = factories.HikingLeaderApplicationFactory.create()
        response, soup = self._get('/hiking/applications/')

        self.assertEqual(response.context_data['needs_rec'], [])
        self.assertEqual(response.context_data['needs_rating'], [application])

        self.assertEqual(
            strip_whitespace(
                soup.find('div', attrs={
                    'class': 'alert-info'
                }).text),
            "When there are multiple activity chairs, co-chairs can make recommendations to one another. "
            "However, this doesn't really make sense when there's a single chair.",
        )
예제 #21
0
    def test_wimp_cannot_sign_up_for_trip(self):
        """ " You can't be the emergency contact while attending a trip."""
        wimp_participant = factories.ParticipantFactory.create()
        trip = self._make_trip(wimp=wimp_participant)
        self.assertCountEqual(
            wimp_participant.reasons_cannot_attend(trip),
            [enums.TripIneligibilityReason.IS_TRIP_WIMP],
        )

        soup = self._render(wimp_participant, trip)

        info = soup.find(class_='alert-info').get_text(' ', strip=True)
        self.assertEqual(info, 'Signups are open!')

        self.assertEqual(
            strip_whitespace(soup.find(class_='alert-danger').text),
            "In order to participate on this trip, you must be replaced in your role as the trip WIMP.",
        )
        self.assertIsNone(soup.find('form'))
예제 #22
0
    def test_reciprocally_paired(self):
        """A successful pairing just warns users about what to expect."""
        bert = factories.ParticipantFactory.create(name='Bert B')
        ernie = factories.ParticipantFactory.create(name='Ernie E')

        factories.LotteryInfoFactory.create(paired_with=bert,
                                            participant=ernie)
        factories.LotteryInfoFactory.create(paired_with=ernie,
                                            participant=bert)

        soup = self._render(bert, user_viewing=True)
        self.assertEqual(
            [strip_whitespace(tag.text) for tag in soup.find_all('p')],
            [
                'Paired with Ernie E',
                'When lotteries run, either both of you will be placed on a trip or neither will.',
                'Change your pairing preferences',
            ],
        )
예제 #23
0
    def test_success_is_shown_in_week_one(self):
        """We show participants that yes, they did in fact attend lectures.

        Participants have been confused if they submitted their attendance, then
        refreshed the page and saw no indicator that their attendance was recorded.
        """
        self._allow_setting_attendance()
        factories.LectureAttendanceFactory.create(
            participant=self.participant, year=2022
        )

        with freeze_time("Jan 6 2022 20:00:00 EST"):
            resp = self.client.get('/')
        soup = BeautifulSoup(resp.content, 'html.parser')
        attendance = soup.find('h3', string='Lecture Attendance')
        self.assertEqual(
            strip_whitespace(attendance.find_next('p').text),
            "Attended You have attended this year's lectures!",
        )
예제 #24
0
    def test_load_form_as_member_able_to_renew(self):
        """We clearly communicate when membership ends if you renew."""
        par = factories.ParticipantFactory.create(
            membership__membership_expires=date(2021, 12, 25)
        )
        self.assertTrue(par.membership.in_early_renewal_period)
        self.client.force_login(par.user)

        response = self.client.get('/profile/membership/')

        soup = BeautifulSoup(response.content, 'html.parser')
        lead_par = soup.find('p', class_='lead')
        self.assertEqual(
            lead_par.text, 'To make the most of MITOC, you must be an active member.'
        )
        self.assertEqual(
            strip_whitespace(lead_par.find_next('p').text),
            'Renewing today keeps your membership active until Dec 25, 2022. '
            "Membership enables you to rent gear from the office, participate in upcoming trips, and stay at MITOC's cabins.",
        )
예제 #25
0
    def test_currently_editable(self):
        self.trip.leaders.add(self.participant)
        _, soup = self._render()
        par = soup.find('p')
        self.assertEqual(
            strip_whitespace(par.text),
            'This form became available at 6 p.m. on Feb 15, 2018 '
            'and may be edited through the day of the trip (Sunday, Feb 18th).',
        )
        self.assertTrue(soup.find('form'))

        # Posting at this URL creates an itinerary!
        self.assertIsNone(self.trip.info)
        creation_resp = self.client.post(f'/trips/{self.trip.pk}/itinerary/',
                                         self.VALID_FORM_BODY)
        self.assertEqual(creation_resp.status_code, 302)
        self.assertEqual(creation_resp.url, f'/trips/{self.trip.pk}/')

        self.trip.refresh_from_db()
        self.assertIsNotNone(self.trip.info)
예제 #26
0
    def test_not_yet_editable(self):
        self.trip.leaders.add(self.participant)

        # Trip is Sunday. On Wednesday, we can't yet edit!
        _, soup = self._render()
        heading = soup.find('h2', string='WIMP information submission')
        par = heading.find_next('p')
        self.assertEqual(
            strip_whitespace(par.text),
            'This form will become available at 6 p.m. on Thursday, Feb 15th.',
        )
        self.assertFalse(soup.find('form'))

        # Submitting does not work!
        resp = self.client.post(f'/trips/{self.trip.pk}/itinerary/',
                                self.VALID_FORM_BODY)
        self.assertEqual(resp.context['form'].errors,
                         {'__all__': ['Itinerary cannot be created']})
        self.trip.refresh_from_db()
        self.assertIsNone(self.trip.info)
예제 #27
0
    def test_load_form_as_lapsed_member(self):
        par = factories.ParticipantFactory.create(
            membership__membership_expires=date(2021, 1, 2)
        )
        self.assertFalse(par.membership.in_early_renewal_period)
        self.assertEqual(par.membership.expiry_if_paid_today, date(2022, 12, 10))

        self.client.force_login(par.user)

        response = self.client.get('/profile/membership/')

        soup = BeautifulSoup(response.content, 'html.parser')
        lead_par = soup.find('p', class_='lead')
        self.assertEqual(
            lead_par.text, 'To make the most of MITOC, you must be an active member.'
        )
        self.assertEqual(
            strip_whitespace(lead_par.find_next('p').text),
            "Membership lasts for 365 days after dues are paid, and enables you to "
            "rent gear from the office, participate in upcoming trips, and stay at MITOC's cabins.",
        )
예제 #28
0
    def test_no_longer_editable(self):
        self.trip.leaders.add(self.participant)

        _, soup = self._render()
        par = soup.find('p')
        self.assertEqual(
            strip_whitespace(par.text),
            'This form will become available at 6 p.m. on Thursday, Feb 15th '
            'and may be edited through the day of the trip (Sunday, Feb 18th).',
        )

        # No longer editable!
        self.assertFalse(soup.find('form'))

        # Submitting does not work!
        resp = self.client.post(f'/trips/{self.trip.pk}/itinerary/',
                                self.VALID_FORM_BODY)
        self.assertEqual(resp.context['form'].errors,
                         {'__all__': ['Itinerary cannot be created']})
        self.trip.refresh_from_db()
        self.assertIsNone(self.trip.info)
예제 #29
0
    def test_0_activity_chairs(self):
        self.participant.user.is_superuser = True
        self.participant.user.save()
        self.assertFalse(
            Group.objects.get(name='hiking_chair').user_set.exists())

        application = factories.HikingLeaderApplicationFactory.create()
        response, soup = self._get('/hiking/applications/')

        # There are no chairs. Thus, the application needs no recommendation, just a rating.
        self.assertEqual(response.context_data['needs_rec'], [])
        self.assertEqual(response.context_data['needs_rating'], [application])

        self.assertEqual(
            strip_whitespace(
                soup.find('div', attrs={
                    'class': 'alert-info'
                }).text),
            "When there are multiple activity chairs, co-chairs can make recommendations to one another. "
            "However, this doesn't really make sense when there is not an acting chair.",
        )
예제 #30
0
    def test_updates_on_stale_trips(self):
        leader = factories.ParticipantFactory.create()
        self.client.force_login(leader.user)
        factories.LeaderRatingFactory.create(
            participant=leader, activity=enums.Activity.CLIMBING.value
        )
        trip = factories.TripFactory.create(
            edit_revision=0,
            creator=leader,
            program=enums.Program.CLIMBING.value,
            level=None,
            trip_date=date(2019, 3, 2),
        )

        # Simulate a stale page content by loading data *first*
        _edit_resp, initial_soup = self._get(f'/trips/{trip.pk}/edit/')
        form_data = dict(self._form_data(initial_soup.find('form')))

        # (Pretend that two others have updated edited the trip)
        trip.edit_revision += 2
        trip.leaders.add(factories.ParticipantFactory.create())
        trip.description = 'Other edits changed this description!'
        trip.last_updated_by = factories.ParticipantFactory.create(name='Joe Schmoe')
        trip.save()

        resp = self.client.post(f'/trips/{trip.pk}/edit/', form_data, follow=False)
        soup = BeautifulSoup(resp.content, 'html.parser')
        warning = strip_whitespace(soup.find(class_='alert alert-danger').text)
        self.assertIn(
            'This trip has already been edited 2 times, most recently by Joe Schmoe.',
            warning,
        )
        self.assertIn(
            'To make updates to the trip, please load the page again.', warning
        )
        self.assertIn('Fields which differ: Leaders, Description', warning)

        # No edit was made; we have form errors
        trip.refresh_from_db()
        self.assertEqual(trip.edit_revision, 2)