Skip to content

Commit

Permalink
Merge pull request #2570 from carpentries/feature/add-eventbrite-ques…
Browse files Browse the repository at this point in the history
…tion

Instructor Training application - add question about Eventbrite URL and associated filter
  • Loading branch information
elichad authored Nov 23, 2023
2 parents 94e02ca + d2457c9 commit 222a6ed
Show file tree
Hide file tree
Showing 13 changed files with 305 additions and 1 deletion.
10 changes: 10 additions & 0 deletions amy/extforms/forms.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from datetime import date
from typing import Iterable, cast
from urllib.parse import urlparse

from captcha.fields import ReCaptchaField
from crispy_forms.layout import HTML, Div, Field, Layout
Expand Down Expand Up @@ -39,6 +40,7 @@ class Meta:
"review_process",
"member_code",
"member_code_override",
"eventbrite_url",
"personal",
"family",
"email",
Expand Down Expand Up @@ -150,6 +152,7 @@ def set_accordion(self, layout: Layout) -> None:
"preapproved": [
self["member_code"],
self["member_code_override"],
self["eventbrite_url"],
],
"open": [], # this option doesn't require any additional fields
}
Expand All @@ -163,6 +166,7 @@ def set_accordion(self, layout: Layout) -> None:
layout.fields.remove("review_process")
layout.fields.remove("member_code")
layout.fields.remove("member_code_override")
layout.fields.remove("eventbrite_url")

# insert div+field at previously saved position
layout.insert(
Expand Down Expand Up @@ -248,6 +252,12 @@ def validate_member_code(

return errors

def clean_eventbrite_url(self):
"""Check that entered URL includes 'eventbrite' in the domain."""
eventbrite_url = self.cleaned_data.get("eventbrite_url", "")
if eventbrite_url and "eventbrite" not in urlparse(eventbrite_url).hostname:
raise ValidationError("Must be an Eventbrite URL.")

def clean(self):
super().clean()
errors = dict()
Expand Down
40 changes: 40 additions & 0 deletions amy/extforms/tests/test_training_request_form.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class TestTrainingRequestForm(TestBase):
MEMBER_CODE_OVERRIDE_EMAIL_WARNING = (
"A member of our team will check the code and follow up with you"
)
INVALID_EVENTBRITE_URL_ERROR = "Must be an Eventbrite URL."

def setUp(self):
self._setUpUsersAndLogin()
Expand Down Expand Up @@ -470,3 +471,42 @@ def test_member_code_validation__code_invalid_override_full_request(self):
self.assertNotIn(
settings.TEMPLATES[0]["OPTIONS"]["string_if_invalid"], msg.body
)

def test_eventbrite_url_validation__none(self):
"""Should not error if no URL is entered."""
# Arrange
self.setUpMembership()
data = {"eventbrite_url": ""}

# Act
rv = self.client.post(reverse("training_request"), data=data)

# Assert
self.assertEqual(rv.status_code, 200)
self.assertNotContains(rv, self.INVALID_EVENTBRITE_URL_ERROR)

def test_eventbrite_url_validation__invalid(self):
"""Should error if a non-Eventbrite URL is entered."""
# Arrange
self.setUpMembership()
data = {"eventbrite_url": "https://google.com"}

# Act
rv = self.client.post(reverse("training_request"), data=data)

# Assert
self.assertEqual(rv.status_code, 200)
self.assertContains(rv, self.INVALID_EVENTBRITE_URL_ERROR)

def test_eventbrite_url_validation__valid(self):
"""Should not error if an Eventbrite URL is entered."""
# Arrange
self.setUpMembership()
data = {"eventbrite_url": "https://www.eventbrite.com/e/711576483417"}

# Act
rv = self.client.post(reverse("training_request"), data=data)

# Assert
self.assertEqual(rv.status_code, 200)
self.assertNotContains(rv, self.INVALID_EVENTBRITE_URL_ERROR)
25 changes: 25 additions & 0 deletions amy/extrequests/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import django_filters

from extrequests.models import SelfOrganisedSubmission, WorkshopInquiryRequest
from extrequests.utils import get_eventbrite_id_from_url_or_return_input
from workshops.fields import Select2Widget
from workshops.filters import (
AllCountriesFilter,
Expand Down Expand Up @@ -43,6 +44,11 @@ def __init__(self, data=None, *args, **kwargs):
field_name="member_code", lookup_expr="icontains", label="Member code"
)

eventbrite_id = django_filters.CharFilter(
label="Eventbrite ID or URL",
method="filter_eventbrite_id",
)

state = django_filters.ChoiceFilter(
label="State",
choices=(("pa", "Pending or accepted"),) + TrainingRequest.STATE_CHOICES,
Expand Down Expand Up @@ -167,6 +173,25 @@ def filter_member_code_override(
return queryset.filter(member_code_override=True)
return queryset

def filter_eventbrite_id(
self, queryset: QuerySet, name: str, value: str
) -> QuerySet:
"""
Returns the queryset filtered by an Eventbrite ID or URL.
Events have multiple possible URLs which all contain the ID, so
if a URL is used, the filter will try to extract and filter by the ID.
If no ID can be found, the filter will use the original input.
"""

try:
# if input is an integer, assume it to be a partial or full Eventbrite ID
int(value)
except ValueError:
# otherwise, try to extract an ID from the input
value = get_eventbrite_id_from_url_or_return_input(value)

return queryset.filter(eventbrite_url__icontains=value)


# ------------------------------------------------------------
# WorkshopRequest related filter and filter methods
Expand Down
13 changes: 13 additions & 0 deletions amy/extrequests/templatetags/eventbrite.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import re

from django import template

from extrequests.utils import EVENTBRITE_URL_PATTERN

register = template.Library()


@register.simple_tag
def url_matches_eventbrite_format(url: str) -> bool:
match = re.search(EVENTBRITE_URL_PATTERN, url)
return match is not None
24 changes: 24 additions & 0 deletions amy/extrequests/tests/test_filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ def setUp(self) -> None:
person=self.ironman,
review_process="preapproved",
member_code=self.membership.registration_code,
eventbrite_url="https://www.eventbrite.com/e/711575811407",
personal="Tony",
family="Stark",
email="[email protected]",
Expand Down Expand Up @@ -112,6 +113,7 @@ def test_fields(self):
"invalid_member_code",
"affiliation",
"location",
"eventbrite_id",
"order_by",
},
)
Expand Down Expand Up @@ -318,6 +320,28 @@ def test_filter_location(self):
# Assert
self.assertQuerysetEqual(result, [self.request_ironman])

def test_filter_eventbrite_id__digits(self):
# Arrange
name = "eventbrite_id"
value = "1407"

# Act
result = self.filterset.filters[name].filter(self.qs, value)

# Assert
self.assertQuerysetEqual(result, [self.request_ironman])

def test_filter_eventbrite_id__url(self):
# Arrange
name = "eventbrite_id"
value = "https://www.eventbrite.com/myevent?eid=711575811407"

# Act
result = self.filterset.filters[name].filter(self.qs, value)

# Assert
self.assertQuerysetEqual(result, [self.request_ironman])

def test_filter_order_by(self):
# Arrange
filter_name = "order_by"
Expand Down
78 changes: 77 additions & 1 deletion amy/extrequests/tests/test_template_tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

from django.test import TestCase

from amy.extrequests.templatetags.request_membership import (
from extrequests.templatetags.eventbrite import url_matches_eventbrite_format
from extrequests.templatetags.request_membership import (
membership_active,
membership_alert_type,
)
Expand Down Expand Up @@ -98,3 +99,78 @@ def test_inactive(self):

# Assert
self.assertEqual(expected, result)


class TestUrlMatchesEventbriteFormat(TestCase):
def test_long_url(self):
# Arrange
url = "https://www.eventbrite.com/e/online-instructor-training-7-8-november-2023-tickets-711575811407?aff=oddtdtcreator" # noqa: line too long

# Act
result = url_matches_eventbrite_format(url)

# Assert
self.assertTrue(result)

def test_short_url(self):
# Arrange
url = "www.eventbrite.com/e/711575811407"

# Act
result = url_matches_eventbrite_format(url)

# Assert
self.assertTrue(result)

def test_localised_url__couk(self):
# Arrange
url = "https://www.eventbrite.co.uk/e/711575811407"

# Act
result = url_matches_eventbrite_format(url)

# Assert
self.assertTrue(result)

def test_localised_url__fr(self):
# Arrange
url = "https://www.eventbrite.fr/e/711575811407"

# Act
result = url_matches_eventbrite_format(url)

# Assert
self.assertTrue(result)

def test_admin_url(self):
"""Admin url should fail - we don't expect trainees to provide this format"""
# Arrange
url = "https://www.eventbrite.com/myevent?eid=711575811407"

# Act
result = url_matches_eventbrite_format(url)

# Assert
self.assertFalse(result)

def test_non_eventbrite_url(self):
"""URLs outside the Eventbrite domain should fail."""
# Arrange
url = "https://carpentries.org/instructor-training/123123123123/"

# Act
result = url_matches_eventbrite_format(url)

# Assert
self.assertFalse(result)

def test_empty_string(self):
"""Empty string should fail."""
# Arrange
url = ""

# Act
result = url_matches_eventbrite_format(url)

# Assert
self.assertFalse(result)
35 changes: 35 additions & 0 deletions amy/extrequests/tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
from datetime import date, timedelta

from django.test import TestCase

from extrequests.tests.test_training_request import create_training_request
from extrequests.utils import (
MemberCodeValidationError,
accept_training_request_and_match_to_event,
get_eventbrite_id_from_url_or_return_input,
get_membership_from_training_request_or_raise_error,
get_membership_or_none_from_code,
get_membership_warnings_after_match,
Expand Down Expand Up @@ -613,3 +616,35 @@ def test_multiple_warnings(self):

# Assert
self.assertListEqual(expected, result)


class TestGetEventbriteIdFromUrl(TestCase):
def test_long_url(self):
# Arrange
url = "https://www.eventbrite.com/e/online-instructor-training-7-8-november-2023-tickets-711575811407?aff=oddtdtcreator" # noqa: line too long

# Act
result = get_eventbrite_id_from_url_or_return_input(url)

# Assert
self.assertEqual(result, "711575811407")

def test_short_url(self):
# Arrange
url = "https://www.eventbrite.com/e/711575811407"

# Act
result = get_eventbrite_id_from_url_or_return_input(url)

# Assert
self.assertEqual(result, "711575811407")

def test_admin_url(self):
# Arrange
url = "https://www.eventbrite.com/myevent?eid=711575811407"

# Act
result = get_eventbrite_id_from_url_or_return_input(url)

# Assert
self.assertEqual(result, "711575811407")
31 changes: 31 additions & 0 deletions amy/extrequests/utils.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from datetime import date
import re

from django.core.exceptions import ValidationError

Expand Down Expand Up @@ -170,3 +171,33 @@ def get_membership_warnings_after_match(
)

return warnings


# ----------------------------------------
# Utilities for Eventbrite URLs
# ----------------------------------------

# Eventbrite IDs are long strings of digits (~12 characters)
EVENTBRITE_ID_PATTERN = re.compile(r"\d{10,}")

# regex to cover known forms of Eventbrite URL that trainees could provide
# https://www.eventbrite.com/e/event-name-123456789012
# https://www.eventbrite.com/e/123456789012
# plus a possible query at the end e.g. ?aff=oddtdtcreator
# and considering localised domains such as .co.uk and .fr
EVENTBRITE_URL_PATTERN = re.compile(
r"^(https?:\/\/)?" # optional https://
r"www\.eventbrite\."
r"(com|co\.uk|[a-z]{2})" # possible domains - .com, .co.uk, 2-letter country domain
r"\/e\/" # /e/ should always be present at start of path
r"[a-z0-9\-]+" # optional event-name
r"\d{10,}" # event ID
r"($|\?)", # end of string or beginning of query (?)
)


def get_eventbrite_id_from_url_or_return_input(url: str) -> str:
"""Given the URL for an Eventbrite event, returns that event's ID.
If the ID can't be found, returns the input URL."""
match = re.search(EVENTBRITE_ID_PATTERN, url)
return match.group() if match else url
Loading

0 comments on commit 222a6ed

Please sign in to comment.