Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ whitenoise = "*"
factory_boy = "*"
setuptools = "*"
django-betterforms = {git = "https://github.com/jpic/django-betterforms.git"}
tzdata = "*"

[dev-packages]
coverage = "*"
Expand Down
926 changes: 544 additions & 382 deletions Pipfile.lock

Large diffs are not rendered by default.

97 changes: 77 additions & 20 deletions home/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2089,26 +2089,24 @@ class ProfessionalSkill(models.Model):
)

def get_skill_level_display(self):
match self.experience_level:
case self.CONCEPTS:
return "1"
case self.EXPLORING:
return "2"
case self.GROWING:
return "3"
case self.INDEPENDENT:
return "4"
if self.experience_level == self.CONCEPTS:
return "1"
elif self.experience_level == self.EXPLORING:
return "2"
elif self.experience_level == self.GROWING:
return "3"
elif self.experience_level == self.INDEPENDENT:
return "4"

def get_skill_experience_level_display(self):
match self.experience_level:
case self.CONCEPTS:
return "(Concepts) Basic understanding or theoretical knowledge of this skill"
case self.EXPLORING:
return "(Exploring) Tried using this skill in classes or personal projects"
case self.GROWING:
return "(Growing) Used this skill in several projects and can further develop it with mentorship"
case self.INDEPENDENT:
return "(Independent) Used this skill in several projects and I can use this skill independently"
if self.experience_level == self.CONCEPTS:
return "(Concepts) Basic understanding or theoretical knowledge of this skill"
elif self.experience_level == self.EXPLORING:
return "(Exploring) Tried using this skill in classes or personal projects"
elif self.experience_level == self.GROWING:
return "(Growing) Used this skill in several projects and can further develop it with mentorship"
elif self.experience_level == self.INDEPENDENT:
return "(Independent) Used this skill in several projects and I can use this skill independently"

class ProjectSkill(models.Model):
project = models.ForeignKey(Project, verbose_name="Project", on_delete=models.CASCADE)
Expand Down Expand Up @@ -2346,6 +2344,65 @@ def objects_for_dashboard(cls, user):
| models.Q(mentor__account=user)
)

def validate_irc_url(value):
parsed = urlparse(value)

def irc_channel_name():
"""
Accept channel in either the path or fragment:
- irc://host/#channel -> fragment=channel, path="/"
- irc://host/channel -> path="/channel"
"""
channel = parsed.fragment or parsed.path.lstrip("/")
if channel.startswith("#"):
channel = channel[1:]
return channel

hostname = (parsed.hostname or "").lower()

gnome_like_hosts = {"irc.gnome.org", "irc.gimp.org", "irc.mozilla.org"}
freenode_hosts = {"freenode.net", "irc.freenode.net", "freenode.org", "irc.freenode.org"}
is_freenode = hostname in freenode_hosts or hostname.endswith(".freenode.net")
must_be_irc_scheme = hostname in gnome_like_hosts or is_freenode

oftc_irc_hosts = {"oftc.net", "irc.oftc.net", "irc.debian.org"}
is_oftc_webchat = hostname == "webchat.oftc.net"

if parsed.scheme == "irc":
if hostname in oftc_irc_hosts:
raise ValidationError("Please use the format https://webchat.oftc.net/?channels=#channel")
if must_be_irc_scheme:
channel = irc_channel_name()
if not channel:
raise ValidationError("Please use the format irc://<host>[:port]/<channel>")
else:
# For all IRC URLs, require a channel somewhere (path or fragment).
channel = irc_channel_name()
if not channel:
raise ValidationError("Please use the format irc://<host>[:port]/<channel>")

if parsed.scheme in ("http", "https"):
if must_be_irc_scheme:
raise ValidationError("Please use the format irc://<host>[:port]/<channel>")

if hostname in oftc_irc_hosts:
raise ValidationError("Please use the format https://webchat.oftc.net/?channels=#channel")

if is_oftc_webchat:
if parsed.scheme != "https":
raise ValidationError("Please use the format https://webchat.oftc.net/?channels=#channel")

# Required format places the channel in the fragment after `?channels=#`.
# Accept the fragment form, plus a percent-encoded `#` in the query.
channel = parsed.fragment
if not channel:
m = re.search(r"(?:^|&)channels=(?:%23|#)([^&]+)", parsed.query or "")
channel = m.group(1) if m else ""
if channel.startswith("#"):
channel = channel[1:]
if not channel:
raise ValidationError("Please use the format https://webchat.oftc.net/?channels=#channel")

class CommunicationChannel(models.Model):
project = models.ForeignKey(Project, on_delete=models.CASCADE)

Expand All @@ -2356,9 +2413,9 @@ class CommunicationChannel(models.Model):

url = models.CharField(
max_length=200,
validators=[validators.URLValidator(schemes=['http', 'https', 'irc'])],
validators=[validators.URLValidator(schemes=['http', 'https', 'irc']), validate_irc_url],
verbose_name="Communication channel URL",
help_text='URL for the communication channel applicants will use to reach mentors and ask questions about this internship project. IRC URLs should be in the form irc://&lt;host&gt;[:port]/[channel]. Since many applicants have issues with IRC port blocking at their universities, IRC communication links will use <a href="https://kiwiirc.com/">Kiwi IRC</a> to direct applicants to a web-based IRC client. If this is a mailing list, the URL should be the mailing list subscription page.')
help_text='URL for the communication channel applicants will use to reach mentors and ask questions about this internship project. IRC URLs should be in the form irc://&lt;host&gt;[:port]/&lt;channel&gt;. Since many applicants have issues with IRC port blocking at their universities, IRC communication links will use <a href="https://kiwiirc.com/">Kiwi IRC</a> to direct applicants to a web-based IRC client. If this is a mailing list, the URL should be the mailing list subscription page. For OFTC use: https://webchat.oftc.net/?channels=#channel')

instructions = CKEditorField(
blank=True,
Expand Down
8 changes: 5 additions & 3 deletions home/templates/home/community_form.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
{% block content %}

<h1>Public Community Information</h1>

<p>This information is shown to Outreachy applicants on the community project page.</p>
<p>
This information will be displayed to Outreachy applicants on the community project page.
Please ensure the details are accurate and clearly written.
</p>

<form action="" method="post">
{% csrf_token %}
Expand All @@ -24,7 +26,7 @@ <h1>Public Community Information</h1>
{% endif %}
</div>
{% endfor %}
<input class="btn btn-success" type="submit" value="Save community info" />
<input class="btn btn-success mt-3" type="submit" value="Save community info" />
</form>

{% endblock %}
12 changes: 10 additions & 2 deletions home/templates/home/snippet/project_landing_snippet.html
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,17 @@ <h3>How do I work with the {{ community.name }} community?</h3>
{% endif %}
{% elif url_parsed.netloc == 'oftc.net' or url_parsed.netloc == 'irc.oftc.net' or url_parsed.netloc == 'irc.debian.org' %}
{% if url_parsed.fragment %}
<a href="https://webchat.oftc.net/?channels={{ url_parsed.fragment }}" target="_blank">Follow this link</a> to join this project's public chat.
{% if url_parsed.fragment|slice:":1" == "#" %}
<a href="https://webchat.oftc.net/?channels=#{{ url_parsed.fragment|slice:"1:" }}" target="_blank">Follow this link</a> to join this project's public chat.
{% else %}
<a href="https://webchat.oftc.net/?channels=#{{ url_parsed.fragment }}" target="_blank">Follow this link</a> to join this project's public chat.
{% endif %}
{% else %}
<a href="https://webchat.oftc.net/?channels={{ url_parsed.path|slice:"1:" }}" target="_blank">Follow this link</a> to join this project's public chat.
{% if url_parsed.path|slice:"1:2" == "#" %}
<a href="https://webchat.oftc.net/?channels=#{{ url_parsed.path|slice:"2:" }}" target="_blank">Follow this link</a> to join this project's public chat.
{% else %}
<a href="https://webchat.oftc.net/?channels=#{{ url_parsed.path|slice:"1:" }}" target="_blank">Follow this link</a> to join this project's public chat.
{% endif %}
{% endif %}
{% else %}
<a href="https://kiwiirc.com/nextclient/{{ url_parsed.netloc }}{{ url_parsed.path }}" target="_blank">Follow this link</a> to join this project's public chat.
Expand Down
52 changes: 52 additions & 0 deletions home/test_communication_channel_url_validator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
from django.forms import ValidationError
from django.test import SimpleTestCase

from home.models import validate_irc_url


class CommunicationChannelUrlValidatorTests(SimpleTestCase):
def assertValid(self, url):
try:
validate_irc_url(url)
except ValidationError as e:
self.fail(f"Expected valid URL, got ValidationError: {e}")

def assertInvalid(self, url):
with self.assertRaises(ValidationError):
validate_irc_url(url)

def test_valid_irc_urls_for_required_hosts(self):
self.assertValid("irc://irc.gnome.org/#outreachy")
self.assertValid("irc://irc.gnome.org/outreachy")
self.assertValid("irc://irc.gimp.org/#gimp")
self.assertValid("irc://irc.mozilla.org/#mozilla")
self.assertValid("irc://irc.freenode.net/#channel")
self.assertValid("irc://chat.freenode.net/#channel")

def test_invalid_non_irc_scheme_for_required_hosts(self):
self.assertInvalid("https://irc.gnome.org/#outreachy")
self.assertInvalid("http://irc.mozilla.org/#mozilla")
self.assertInvalid("https://irc.freenode.net/#channel")

def test_reject_non_channel_irc_urls(self):
self.assertInvalid("irc://irc.gnome.org")
self.assertInvalid("irc://irc.gnome.org/")
self.assertInvalid("irc://example.com")
self.assertInvalid("irc://example.com/")

def test_oftc_must_use_webchat_url(self):
self.assertInvalid("irc://irc.oftc.net/#debian")
self.assertInvalid("irc://irc.debian.org/#debian")
self.assertInvalid("https://irc.oftc.net/#debian")

self.assertValid("https://webchat.oftc.net/?channels=#debian")
self.assertValid("https://webchat.oftc.net/?channels=%23debian")

self.assertInvalid("http://webchat.oftc.net/?channels=#debian")
self.assertInvalid("https://webchat.oftc.net/?channels=")

def test_non_irc_urls_are_accepted_by_this_validator(self):
# Non-IRC URLs are validated by URLValidator; this function should not reject them.
self.assertValid("https://example.com/path")
self.assertValid("http://lists.example.org/subscribe")

Empty file.
7 changes: 5 additions & 2 deletions outreachyhome/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,11 +165,14 @@
# https://docs.djangoproject.com/en/1.11/ref/settings/#databases
# Update database configuration with $DATABASE_URL.

import dj_database_url # noqa: E402
import dj_database_url
import os

DATABASES = {
'default': dj_database_url.config(
default='sqlite:///' + os.path.join(BASE_DIR, 'db.sqlite3'),
conn_max_age=600,
default='sqlite:///' + os.path.join(BASE_DIR, 'db.sqlite3'))
)
}

# In Django 3.2, developers introduced a new way to generate object IDs (pks)
Expand Down
8 changes: 8 additions & 0 deletions test_urlparse.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from urllib.parse import urlparse

p1 = urlparse("irc://irc.gnome.org/#outreachy")
print("p1:", p1)
p2 = urlparse("irc://irc.gnome.org/outreachy")
print("p2:", p2)
p3 = urlparse("https://webchat.oftc.net/?channels=#channel")
print("p3:", p3)