Skip to content
Merged
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
3 changes: 3 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ insert_final_newline = true
indent_style = space
indent_size = 4

[*.py]
max_line_length = 88

[*.{html,json}]
indent_style = space
indent_size = 2
Expand Down
20 changes: 0 additions & 20 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,23 +62,3 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: coveralls --service=github

lint:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: 3.8
- name: Ensure latest setuptools
run: |
python -m pip install --upgrade pip setuptools
- name: Install dependencies
run: |
python -m pip install tox
- name: Run tox
run: |
python -m pip --version
python -m tox --version
python -m tox -e isort,flake8
4 changes: 4 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ repos:
args: ["--ignore-case", "--unique"]
- id: fix-encoding-pragma
args: ["--remove"]
- repo: https://github.com/psf/black
rev: 22.10.0
hooks:
- id: black
- repo: https://github.com/pycqa/isort
rev: "5.10.1"
hooks:
Expand Down
17 changes: 8 additions & 9 deletions accounts/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,29 +10,28 @@ class ProfileForm(forms.ModelForm):
Assumes that the Profile instance passed in has an associated User
object. The view (see views.py) takes care of that.
"""

name = forms.CharField(
required=False,
widget=forms.TextInput(attrs={'placeholder': 'Name'})
required=False, widget=forms.TextInput(attrs={"placeholder": "Name"})
)
email = forms.EmailField(
required=False,
widget=forms.TextInput(attrs={'placeholder': 'Email'})
required=False, widget=forms.TextInput(attrs={"placeholder": "Email"})
)

class Meta:
model = Profile
fields = ['name']
fields = ["name"]

def __init__(self, *args, **kwargs):
instance = kwargs.get('instance', None)
instance = kwargs.get("instance", None)
if instance:
kwargs.setdefault('initial', {}).update({'email': instance.user.email})
kwargs.setdefault("initial", {}).update({"email": instance.user.email})
super().__init__(*args, **kwargs)

def save(self, commit=True):
instance = super().save(commit=commit)
if 'email' in self.cleaned_data:
instance.user.email = self.cleaned_data['email']
if "email" in self.cleaned_data:
instance.user.email = self.cleaned_data["email"]
if commit:
instance.user.save()
return instance
8 changes: 3 additions & 5 deletions accounts/hashers.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
from django.contrib.auth.hashers import (
PBKDF2PasswordHasher, SHA1PasswordHasher,
)
from django.contrib.auth.hashers import PBKDF2PasswordHasher, SHA1PasswordHasher


class PBKDF2WrappedSHA1PasswordHasher(PBKDF2PasswordHasher):
algorithm = 'pbkdf2_wrapped_sha1'
algorithm = "pbkdf2_wrapped_sha1"

def encode_sha1_hash(self, sha1_hash, salt, iterations=None):
return super().encode(sha1_hash, salt, iterations)

def encode(self, password, salt, iterations=None):
_, _, sha1_hash = SHA1PasswordHasher().encode(password, salt).split('$', 2)
_, _, sha1_hash = SHA1PasswordHasher().encode(password, salt).split("$", 2)
return self.encode_sha1_hash(sha1_hash, salt, iterations)
24 changes: 18 additions & 6 deletions accounts/migrations/0001_initial.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,26 @@ class Migration(migrations.Migration):

operations = [
migrations.CreateModel(
name='Profile',
name="Profile",
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('name', models.CharField(max_length=200, blank=True)),
('user', models.OneToOneField(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)),
(
"id",
models.AutoField(
verbose_name="ID",
serialize=False,
auto_created=True,
primary_key=True,
),
),
("name", models.CharField(max_length=200, blank=True)),
(
"user",
models.OneToOneField(
to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE
),
),
],
options={
},
options={},
bases=(models.Model,),
),
]
12 changes: 6 additions & 6 deletions accounts/migrations/0002_migrate_sha1_passwords.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,20 @@


def forwards_func(apps, schema_editor):
User = apps.get_model('auth', 'User')
users = User.objects.filter(password__startswith='sha1$')
User = apps.get_model("auth", "User")
users = User.objects.filter(password__startswith="sha1$")
hasher = PBKDF2WrappedSHA1PasswordHasher()
for user in users:
algorithm, salt, sha1_hash = user.password.split('$', 2)
algorithm, salt, sha1_hash = user.password.split("$", 2)
user.password = hasher.encode_sha1_hash(sha1_hash, salt)
user.save(update_fields=['password'])
user.save(update_fields=["password"])


class Migration(migrations.Migration):

dependencies = [
('accounts', '0001_initial'),
('auth', '0007_alter_validators_add_error_messages'),
("accounts", "0001_initial"),
("auth", "0007_alter_validators_add_error_messages"),
]

operations = [
Expand Down
15 changes: 10 additions & 5 deletions accounts/test_hashers.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,16 @@
class TestHasher(SimpleTestCase):
def test(self):
iterations = 10 # low value for testing
password = 'a'
sha1_encoded = 'sha1$YoBzLZeL3w2R$757b56ad30c5fac1b552f58ad3acfddb07b674e2'
expected_pbkdf2_encoded = 'pbkdf2_wrapped_sha1$10$YoBzLZeL3w2R$djfB2Y51/PwFdzcMoIlwUXglb2wMBz2L+LZ8Hjs2wnk='
_, salt, sha1_hash = sha1_encoded.split('$', 3)
password = "a"
sha1_encoded = "sha1$YoBzLZeL3w2R$757b56ad30c5fac1b552f58ad3acfddb07b674e2"
expected_pbkdf2_encoded = (
"pbkdf2_wrapped_sha1$10$YoBzLZeL3w2R$djfB2Y51/"
"PwFdzcMoIlwUXglb2wMBz2L+LZ8Hjs2wnk="
)
_, salt, sha1_hash = sha1_encoded.split("$", 3)
hasher = PBKDF2WrappedSHA1PasswordHasher()
pbkdf2_hash = hasher.encode_sha1_hash(sha1_hash, salt, iterations)
self.assertEqual(pbkdf2_hash, expected_pbkdf2_encoded)
self.assertEqual(hasher.encode(password, salt, iterations), expected_pbkdf2_encoded)
self.assertEqual(
hasher.encode(password, salt, iterations), expected_pbkdf2_encoded
)
17 changes: 8 additions & 9 deletions accounts/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,24 @@


class ViewTests(TestCase):

def setUp(self):
self.credentials = {'username': 'a-user', 'password': 'password'}
self.credentials = {"username": "a-user", "password": "password"}
self.user = User.objects.create_user(**self.credentials)

@mock.patch('accounts.views.get_user_stats')
@mock.patch("accounts.views.get_user_stats")
def test_user_profile(self, mock_user_stats):
response = self.client.get(reverse('user_profile', host='www', args=['a-user']))
self.assertContains(response, 'a-user')
response = self.client.get(reverse("user_profile", host="www", args=["a-user"]))
self.assertContains(response, "a-user")
mock_user_stats.assert_called_once_with(self.user)

def test_login_redirect(self):
response = self.client.post(reverse('login'), self.credentials)
self.assertRedirects(response, '/accounts/edit/')
response = self.client.post(reverse("login"), self.credentials)
self.assertRedirects(response, "/accounts/edit/")

def test_profile_view_reversal(self):
"""
The profile view can be reversed for usernames containing "weird" but
valid username characters.
"""
for username in ['asdf', '@asdf', 'asd-f', 'as.df', 'as+df']:
reverse('user_profile', host='www', args=[username])
for username in ["asdf", "@asdf", "asd-f", "as.df", "as+df"]:
reverse("user_profile", host="www", args=[username])
14 changes: 7 additions & 7 deletions accounts/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,16 @@

urlpatterns = [
path(
'register/',
"register/",
RegistrationView.as_view(form_class=RegistrationFormUniqueEmail),
name='registration_register',
name="registration_register",
),
path(
'edit/',
"edit/",
account_views.edit_profile,
name='edit_profile',
name="edit_profile",
),
path('_trac/userinfo/', account_views.json_user_info),
path('', include('django.contrib.auth.urls')),
path('', include('registration.backends.default.urls')),
path("_trac/userinfo/", account_views.json_user_info),
path("", include("django.contrib.auth.urls")),
path("", include("registration.backends.default.urls")),
]
43 changes: 23 additions & 20 deletions accounts/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,16 @@

def user_profile(request, username):
user = get_object_or_404(User, username=username)
return render(request, "accounts/user_profile.html", {
'user_obj': user,
'email_hash': hashlib.md5(user.email.encode('ascii', 'ignore')).hexdigest(),
'user_can_commit': user.has_perm('auth.commit'),
'stats': get_user_stats(user),
})
return render(
request,
"accounts/user_profile.html",
{
"user_obj": user,
"email_hash": hashlib.md5(user.email.encode("ascii", "ignore")).hexdigest(),
"user_can_commit": user.has_perm("auth.commit"),
"stats": get_user_stats(user),
},
)


@login_required
Expand All @@ -30,8 +34,8 @@ def edit_profile(request):
form = ProfileForm(request.POST or None, instance=profile)
if form.is_valid():
form.save()
return redirect('user_profile', request.user.username)
return render(request, "accounts/edit_profile.html", {'form': form})
return redirect("user_profile", request.user.username)
return render(request, "accounts/edit_profile.html", {"form": form})


def json_user_info(request):
Expand All @@ -50,17 +54,16 @@ def json_user_info(request):
De-duplication on GET['user'] is performed since I don't want to have to
think about how best to do it in JavaScript :)
"""
userinfo = dict([
(name, get_user_info(name))
for name in set(request.GET.getlist('user'))
])
userinfo = dict(
[(name, get_user_info(name)) for name in set(request.GET.getlist("user"))]
)
return JSONResponse(userinfo)


def get_user_info(username):
c = caches['default']
username = username.encode('ascii', 'ignore')
key = 'trac_user_info:%s' % hashlib.md5(username).hexdigest()
c = caches["default"]
username = username.encode("ascii", "ignore")
key = "trac_user_info:%s" % hashlib.md5(username).hexdigest()
info = c.get(key)
if info is None:
try:
Expand All @@ -69,16 +72,16 @@ def get_user_info(username):
info = {"core": False, "cla": False}
else:
info = {
"core": u.has_perm('auth.commit'),
"core": u.has_perm("auth.commit"),
}
c.set(key, info, 60 * 60)
return info


def get_user_stats(user):
c = caches['default']
username = user.username.encode('ascii', 'ignore')
key = 'user_vital_status:%s' % hashlib.md5(username).hexdigest()
c = caches["default"]
username = user.username.encode("ascii", "ignore")
key = "user_vital_status:%s" % hashlib.md5(username).hexdigest()
info = c.get(key)
if info is None:
info = trac_stats.get_user_stats(user.username)
Expand All @@ -95,5 +98,5 @@ class JSONResponse(HttpResponse):
def __init__(self, obj):
super().__init__(
json.dumps(obj, indent=(2 if settings.DEBUG else None)),
content_type='application/json',
content_type="application/json",
)
12 changes: 6 additions & 6 deletions aggregator/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,21 +27,21 @@ def mark_denied(modeladmin, request, queryset):
list_filter=["feed_type", "approval_status"],
ordering=["title"],
search_fields=["title", "public_url"],
raw_id_fields=['owner'],
raw_id_fields=["owner"],
list_editable=["approval_status"],
list_per_page=500,
actions=[mark_approved, mark_denied],
)

admin.site.register(
FeedItem,
list_display=['title', 'feed', 'date_modified'],
list_filter=['feed'],
search_fields=['feed__title', 'feed__public_url', 'title'],
date_heirarchy=['date_modified'],
list_display=["title", "feed", "date_modified"],
list_filter=["feed"],
search_fields=["feed__title", "feed__public_url", "title"],
date_heirarchy=["date_modified"],
)

admin.site.register(
FeedType,
prepopulated_fields={'slug': ('name',)},
prepopulated_fields={"slug": ("name",)},
)
4 changes: 2 additions & 2 deletions aggregator/context_processors.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@ def community_stats(request):
Context processor to calculate Django's age for the community pages.
"""
# Django 3.2 introduces depth kwarg. Set timesince(..., depth=1) then.
stats = {'age': timesince(DJANGO_DOB)}
return {'community_stats': stats}
stats = {"age": timesince(DJANGO_DOB)}
return {"community_stats": stats}
14 changes: 7 additions & 7 deletions aggregator/feeds.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,27 +34,27 @@ def get_object(self, request, slug=None):

def items(self, obj):
qs = FeedItem.objects.filter(feed__feed_type=obj)
qs = qs.order_by('-date_modified')
qs = qs.select_related('feed', 'feed__feed_type')
qs = qs.order_by("-date_modified")
qs = qs.select_related("feed", "feed__feed_type")
return qs[:25]

def title(self, obj):
return "Django community aggregator: %s" % obj.name

def link(self, obj):
return reverse('aggregator-feed', args=[obj.slug], host='www')
return reverse("aggregator-feed", args=[obj.slug], host="www")

def description(self, obj):
return self.title(obj)


class CommunityAggregatorFirehoseFeed(BaseCommunityAggregatorFeed):
title = 'Django community aggregator firehose'
description = 'All activity from the Django community aggregator'
title = "Django community aggregator firehose"
description = "All activity from the Django community aggregator"

def link(self):
return reverse('aggregator-firehose-feed', host='www')
return reverse("aggregator-firehose-feed", host="www")

def items(self):
qs = FeedItem.objects.order_by('-date_modified').select_related('feed')
qs = FeedItem.objects.order_by("-date_modified").select_related("feed")
return qs[:50]
Loading