Skip to content
This repository has been archived by the owner on Aug 10, 2023. It is now read-only.

Commit

Permalink
Merge branch 'py36-migration'
Browse files Browse the repository at this point in the history
Upgrade the code base to Python 3.6+ and drop support for older Pythons,
in summary:

* Migrate old %-format and str.format to f-strings,
* Take advantage of the os.PathLike interface where possible
* Remove compatibility workarounds (e.g., for the `json` module)

As part of this change I have also refactored all `with` and `except`
blocks.  During that I found and fixed some bogus tests.

Furthermore, I have normalized string literals according to the
algorithm that `repr(…)` uses, i.e., strings are now preferably
surrounded by single quotes.

Please have a look at the individual commit messages for details.

Fixes: #300
  • Loading branch information
martinmo committed Dec 29, 2019
2 parents a35cc54 + 573b01d commit 8ec1930
Show file tree
Hide file tree
Showing 90 changed files with 1,219 additions and 1,264 deletions.
19 changes: 0 additions & 19 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,6 @@ dist: trusty

matrix:
include:
- language: python
name: "Python 3.5 Checks"
python:
- "3.5"
services:
- docker
cache:
pip: true
install:
- pip install -r build-requirements.txt
- pip install codacy-coverage
- make
script:
- make lint
- make coverage
after_success:
- pipenv run coverage xml
- python-codacy-coverage

- language: python
name: "Python 3.6 Checks"
python:
Expand Down
2 changes: 1 addition & 1 deletion Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,6 @@ coverage = "*"
"flake8" = "*"
"flake8-docstrings" = "*"
isort = "*"
ipython = "<7.10"
ipython = "*"
honcho = "*"
django-html-validator = "*"
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ application to manage online programming courses, powered by Git, Django and Doc
INLOOP requires the following software:

* Debian 8+ or Ubuntu 16.04+, macOS 10.11+ (for development only)
* Python 3.5, 3.6 or 3.7
* Python 3.6 or 3.7
* Docker 1.10+ ([Docker setup](docs/docker_setup.md) **!!!**)
* Redis 2.6+
* Git 2.3+ ([workarounds for older Git versions](docs/git_troubleshouting.md))
Expand Down
16 changes: 8 additions & 8 deletions inloop/accounts/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@

@admin.register(StudentDetails)
class StudentDetailsAdmin(admin.ModelAdmin):
list_display = ["user", "email", "first_name", "last_name", "matnum", "course"]
list_filter = ["course", "user__date_joined"]
search_fields = ["user__first_name", "user__last_name", "user__email", "matnum"]
fields = ["user", "email", "first_name", "last_name", "matnum", "course"]
readonly_fields = ["user", "email", "first_name", "last_name"]
list_display = ['user', 'email', 'first_name', 'last_name', 'matnum', 'course']
list_filter = ['course', 'user__date_joined']
search_fields = ['user__first_name', 'user__last_name', 'user__email', 'matnum']
fields = ['user', 'email', 'first_name', 'last_name', 'matnum', 'course']
readonly_fields = ['user', 'email', 'first_name', 'last_name']

def first_name(self, obj):
return obj.user.first_name
Expand All @@ -20,9 +20,9 @@ def last_name(self, obj):
def email(self, obj):
return '<a href="mailto:{email}">{email}</a>'.format(email=obj.user.email)

first_name.admin_order_field = "user__first_name"
last_name.admin_order_field = "user__last_name"
email.admin_order_field = "user__email"
first_name.admin_order_field = 'user__first_name'
last_name.admin_order_field = 'user__last_name'
email.admin_order_field = 'user__email'
email.allow_tags = True


Expand Down
28 changes: 14 additions & 14 deletions inloop/accounts/constance.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,28 @@
from inloop.common.validators import RegexSyntaxValidator

config = OrderedDict()
config["SIGNUP_ALLOWED"] = (False, mark_safe("Allow or disallow new users to sign up."))
config["EMAIL_PATTERN"] = ("", mark_safe(
"A Python regular expression used to test email addresses during sign up. The "
"regex is compiled using <code>re.VERBOSE</code>, which means you can use "
"comments and whitespace (which will be ignored) to structure the regex."), "regex"
config['SIGNUP_ALLOWED'] = (False, mark_safe('Allow or disallow new users to sign up.'))
config['EMAIL_PATTERN'] = ('', mark_safe(
'A Python regular expression used to test email addresses during sign up. The '
'regex is compiled using <code>re.VERBOSE</code>, which means you can use '
'comments and whitespace (which will be ignored) to structure the regex.'), 'regex'
)
config["EMAIL_HELP_TEXT"] = ("", mark_safe(
"Form field help text with a human-friendly description of <code>EMAIL_PATTERN</code>."
config['EMAIL_HELP_TEXT'] = ('', mark_safe(
'Form field help text with a human-friendly description of <code>EMAIL_PATTERN</code>.'
))
config["EMAIL_ERROR_MESSAGE"] = ("", mark_safe(
config['EMAIL_ERROR_MESSAGE'] = ('', mark_safe(
"Form field error message in case <code>EMAIL_PATTERN</code> doesn't match."
))

fieldsets = {
"Signup form settings": ["SIGNUP_ALLOWED", "EMAIL_PATTERN",
"EMAIL_HELP_TEXT", "EMAIL_ERROR_MESSAGE"]
'Signup form settings': ['SIGNUP_ALLOWED', 'EMAIL_PATTERN',
'EMAIL_HELP_TEXT', 'EMAIL_ERROR_MESSAGE']
}

fields = {
"regex": ["django.forms.CharField", {
"widget": "django.forms.Textarea",
"validators": [RegexSyntaxValidator(re.VERBOSE)],
"required": False
'regex': ['django.forms.CharField', {
'widget': 'django.forms.Textarea',
'validators': [RegexSyntaxValidator(re.VERBOSE)],
'required': False
}],
}
10 changes: 5 additions & 5 deletions inloop/accounts/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,25 +37,25 @@ def clean_email(self):
# re.compile() succeeds. If it fails to compile now, there must be some
# serious error which we can't fix here and it's better to crash.
regex = re.compile(pattern, re.VERBOSE)
if not regex.search(self.cleaned_data["email"]):
if not regex.search(self.cleaned_data['email']):
raise ValidationError(markdown(config.EMAIL_ERROR_MESSAGE))
return super().clean_email()

def clean_username(self):
"""Ensure no duplicate user names exist, using case-insensitive comparison."""
username = self.cleaned_data.get("username")
username = self.cleaned_data.get('username')
if User.objects.filter(username__iexact=username):
raise forms.ValidationError("A user with that username already exists.")
raise forms.ValidationError('A user with that username already exists.')
return username


class StudentDetailsForm(forms.ModelForm):
class Meta:
model = StudentDetails
fields = ["matnum", "course"]
fields = ['matnum', 'course']


class UserChangeForm(forms.ModelForm):
class Meta:
model = User
fields = ["first_name", "last_name"]
fields = ['first_name', 'last_name']
16 changes: 8 additions & 8 deletions inloop/accounts/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,16 @@
from django.utils.text import mark_safe

INCOMPLETE_HINT = (
"Your user profile is incomplete. To ensure we can award bonus points to you, please "
"set your name and matriculation number on <a href=\"%s\">My Profile</a>."
'Your user profile is incomplete. To ensure we can award bonus points to you, please '
'set your name and matriculation number on <a href="%s">My Profile</a>.'
)


@receiver(user_logged_in, dispatch_uid="complete_profile_hint")
@receiver(user_logged_in, dispatch_uid='complete_profile_hint')
def complete_profile_hint(sender, user, request, **kwargs):
"""Show logged in users a hint if they do not have a complete profile."""
if not user_profile_complete(user):
message = mark_safe(INCOMPLETE_HINT % reverse("accounts:profile"))
message = mark_safe(INCOMPLETE_HINT % reverse('accounts:profile'))
# fail_silently needs to be set for unit tests using RequestFactory
messages.warning(request, message, fail_silently=True)

Expand All @@ -41,20 +41,20 @@ def __str__(self):


def default_course():
course, _ = Course.objects.get_or_create(name="Other")
course, _ = Course.objects.get_or_create(name='Other')
return course.id


class StudentDetails(models.Model):
"""Associate additional data to the user account."""

class Meta:
verbose_name_plural = "Student details"
verbose_name_plural = 'Student details'

user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
matnum = models.CharField(
blank=True, verbose_name="Matriculation number", max_length=20, validators=[
RegexValidator(r'^[0-9]*$', "Please enter a numeric value or leave the field blank.")
blank=True, verbose_name='Matriculation number', max_length=20, validators=[
RegexValidator(r'^[0-9]*$', 'Please enter a numeric value or leave the field blank.')
]
)
course = models.ForeignKey(Course, default=default_course, on_delete=models.PROTECT)
Expand Down
24 changes: 12 additions & 12 deletions inloop/accounts/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,23 @@

from inloop.accounts import views

app_name = "accounts"
app_name = 'accounts'

urlpatterns = [
url(r'^signup/$', views.signup, name="signup"),
url(r'^signup/closed/$', views.signup_closed, name="signup_closed"),
url(r'^signup/complete/$', views.signup_complete, name="signup_complete"),
url(r'^activate/complete/$', views.activation_complete, name="activation_complete"),
url(r'^activate/(?P<activation_key>[-:\w]+)/$', views.activate, name="activate"),
url(r'^signup/$', views.signup, name='signup'),
url(r'^signup/closed/$', views.signup_closed, name='signup_closed'),
url(r'^signup/complete/$', views.signup_complete, name='signup_complete'),
url(r'^activate/complete/$', views.activation_complete, name='activation_complete'),
url(r'^activate/(?P<activation_key>[-:\w]+)/$', views.activate, name='activate'),

url(r'^profile/$', views.profile, name="profile"),
url(r'^password/$', views.password_change, name="password_change"),
url(r'^profile/$', views.profile, name='profile'),
url(r'^password/$', views.password_change, name='password_change'),

url(r'^password_reset/$', views.password_reset, name="password_reset"),
url(r'^password_reset_done/', views.password_reset_done, name="password_reset_done"),
url(r'^password_reset/$', views.password_reset, name='password_reset'),
url(r'^password_reset_done/', views.password_reset_done, name='password_reset_done'),
url(r'^password_reset_confirm/(?P<uidb64>[0-9A-Za-z_\-]+)/'
r'(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$',
views.password_reset_confirm, name="password_reset_confirm"),
views.password_reset_confirm, name='password_reset_confirm'),
url(r'^password_reset_confirm_done/$', views.password_reset_complete,
name="password_reset_complete"),
name='password_reset_complete'),
]
44 changes: 22 additions & 22 deletions inloop/accounts/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@


class PasswordChangeView(DjangoPasswordChangeView):
success_url = reverse_lazy("accounts:profile")
template_name = "accounts/password_change_form.html"
success_url = reverse_lazy('accounts:profile')
template_name = 'accounts/password_change_form.html'

def form_valid(self, form):
response = super().form_valid(form)
messages.success(self.request, "Your password has been updated successfully.")
messages.success(self.request, 'Your password has been updated successfully.')
return response


Expand All @@ -31,12 +31,12 @@ class ProfileView(LoginRequiredMixin, View):
View and edit a User model and its related StudentDetails using one HTML form.
"""

template_name = "accounts/profile_form.html"
success_url = reverse_lazy("accounts:profile")
template_name = 'accounts/profile_form.html'
success_url = reverse_lazy('accounts:profile')

def get(self, request):
return TemplateResponse(request, self.template_name, context={
"forms": self.get_forms()
'forms': self.get_forms()
})

def post(self, request):
Expand All @@ -55,13 +55,13 @@ def get_forms(self, data=None):

def forms_invalid(self, forms):
return TemplateResponse(self.request, self.template_name, context={
"forms": forms
'forms': forms
})

def forms_valid(self, forms):
for form in forms:
form.save()
messages.success(self.request, "Your profile has been updated successfully.")
messages.success(self.request, 'Your profile has been updated successfully.')
return HttpResponseRedirect(self.success_url)


Expand All @@ -70,45 +70,45 @@ def forms_valid(self, forms):


class SignupView(HmacRegistrationView):
template_name = "accounts/signup_form.html"
template_name = 'accounts/signup_form.html'
form_class = SignupForm
email_body_template = "accounts/activation_email.txt"
email_subject_template = "accounts/activation_email_subject.txt"
disallowed_url = reverse_lazy("accounts:signup_closed")
email_body_template = 'accounts/activation_email.txt'
email_subject_template = 'accounts/activation_email_subject.txt'
disallowed_url = reverse_lazy('accounts:signup_closed')

def dispatch(self, request, *args, **kwargs):
if request.user.is_authenticated:
return HttpResponseRedirect(reverse("home"))
return HttpResponseRedirect(reverse('home'))
return super().dispatch(request, *args, **kwargs)

def get_email_context(self, activation_key):
context = super().get_email_context(activation_key)
context["request"] = self.request
context['request'] = self.request
return context

def get_success_url(self, user):
return reverse("accounts:signup_complete")
return reverse('accounts:signup_complete')

def registration_allowed(self):
return config.SIGNUP_ALLOWED


class ActivationView(HmacActivationView):
template_name = "accounts/activation_failed.html"
template_name = 'accounts/activation_failed.html'

def get_success_url(self, user):
return reverse("accounts:activation_complete")
return reverse('accounts:activation_complete')


activate = ActivationView.as_view()
activation_complete = TemplateView.as_view(template_name="accounts/activation_complete.html")
activation_complete = TemplateView.as_view(template_name='accounts/activation_complete.html')
signup = SignupView.as_view()
signup_closed = TemplateView.as_view(template_name="accounts/signup_closed.html")
signup_complete = TemplateView.as_view(template_name="accounts/signup_complete.html")
signup_closed = TemplateView.as_view(template_name='accounts/signup_closed.html')
signup_complete = TemplateView.as_view(template_name='accounts/signup_complete.html')

password_reset = PasswordResetView.as_view(
success_url=reverse_lazy("accounts:password_reset_done"))
success_url=reverse_lazy('accounts:password_reset_done'))
password_reset_done = PasswordResetDoneView.as_view()
password_reset_confirm = PasswordResetConfirmView.as_view(
success_url=reverse_lazy("accounts:password_reset_complete"))
success_url=reverse_lazy('accounts:password_reset_complete'))
password_reset_complete = PasswordResetCompleteView.as_view()
2 changes: 1 addition & 1 deletion inloop/common/context_processors.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@
def current_site(request):
"""Context processor which populates the current site as ``site``."""
return {
"site": get_current_site(request)
'site': get_current_site(request)
}
6 changes: 3 additions & 3 deletions inloop/common/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@ def __init__(self, get_response):

def __call__(self, request):
try:
forwarded_for = request.META["HTTP_X_FORWARDED_FOR"]
forwarded_for = request.META['HTTP_X_FORWARDED_FOR']
except KeyError:
pass
else:
# HTTP_X_FORWARDED_FOR can be a comma-separated list of IPs.
# The client's IP will be the first one.
forwarded_for = forwarded_for.split(",")[0].strip()
request.META["REMOTE_ADDR"] = forwarded_for
forwarded_for = forwarded_for.split(',')[0].strip()
request.META['REMOTE_ADDR'] = forwarded_for

return self.get_response(request)
Loading

0 comments on commit 8ec1930

Please sign in to comment.