Skip to content
This repository was archived by the owner on Apr 29, 2022. It is now read-only.

Commit d364970

Browse files
Merge branch 'EuroPython:ep2021' into issue-823-improve-textcha-for-registration
2 parents 4ce2dcc + 10ae259 commit d364970

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+992
-265
lines changed

Makefile

+4
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ help:
1313
@echo "update-requirements - run pip compile and rebuild the requirements files"
1414
@echo "migrations - generate migrations in a clean container"
1515
@echo "shell - start a django shell"
16+
@echo "bash - start a bash shell in a running container"
1617
@echo "urls - print url routes"
1718

1819
@echo "\n[TEST]"
@@ -80,6 +81,9 @@ migrations: build
8081
shell:
8182
docker-compose run --rm epcon "./manage.py shell_plus"
8283

84+
bash:
85+
docker-compose exec epcon /bin/bash
86+
8387
urls:
8488
docker-compose run --rm epcon "./manage.py show_urls"
8589

assets/css/europython-customizations.css

+4
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,10 @@ a.navbar-brand:hover{
8787
flex-wrap: wrap;
8888
}
8989

90+
#days-navbar {
91+
background-image: none;
92+
}
93+
9094
#content_page {
9195
min-height: 500px;
9296
margin: 40px 0;

cms_utils/cms_plugins.py

+9-4
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
1-
# -*- coding: utf-8 -*-
2-
from django.utils.translation import ugettext_lazy as _
31
from cms.plugin_base import CMSPluginBase
42
from cms.plugin_pool import plugin_pool
5-
from .models import MarkitUpPluginModel
3+
from django.utils.translation import ugettext_lazy as _
4+
5+
from .models import MarkitUpPluginModel, TemplatePluginModel
66

77

8+
@plugin_pool.register_plugin
89
class MarkItUpPlugin(CMSPluginBase):
910
name = _('MarkItUp')
1011
model = MarkitUpPluginModel
1112
render_template = 'djangocms_markitup/markitup.html'
1213

1314

14-
plugin_pool.register_plugin(MarkItUpPlugin)
15+
@plugin_pool.register_plugin
16+
class TemplatePlugin(CMSPluginBase):
17+
name = _('Template Plugin')
18+
model = TemplatePluginModel
19+
render_template = 'djangocms_template/template_plugin.html'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Generated by Django 2.2.23 on 2021-05-23 14:06
2+
3+
from django.db import migrations, models
4+
import django.db.models.deletion
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
('cms', '0022_auto_20180620_1551'),
11+
('cms_utils', '0001_initial'),
12+
]
13+
14+
operations = [
15+
migrations.CreateModel(
16+
name='TemplatePluginModel',
17+
fields=[
18+
('cmsplugin_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, related_name='cms_utils_templatepluginmodel', serialize=False, to='cms.CMSPlugin')),
19+
('body', models.TextField()),
20+
],
21+
options={
22+
'abstract': False,
23+
},
24+
bases=('cms.cmsplugin',),
25+
),
26+
]

cms_utils/models.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1-
# -*- coding: utf-8 -*-
2-
from markitup.fields import MarkupField
31
from cms.models import CMSPlugin
2+
from django.db import models
3+
from markitup.fields import MarkupField
44

55

66
class MarkitUpPluginModel(CMSPlugin):
77
body = MarkupField()
8+
9+
10+
class TemplatePluginModel(CMSPlugin):
11+
body = models.TextField()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{{ instance.body|safe }}

conference/accounts.py

+2
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from assopy.models import AssopyUser, Token
2424
from conference.models import CaptchaQuestion, AttendeeProfile
2525
from p3.models import P3Profile
26+
from conference.forms import CustomPasswordResetForm
2627

2728

2829
LOGIN_TEMPLATE = "conference/accounts/login.html"
@@ -304,6 +305,7 @@ def clean(self):
304305
success_url=reverse_lazy("accounts:password_reset_done"),
305306
email_template_name="conference/emails/password_reset_email.txt",
306307
subject_template_name="conference/emails/password_reset_subject.txt",
308+
form_class=CustomPasswordResetForm,
307309
),
308310
name="password_reset",
309311
),

conference/admin.py

+27
Original file line numberDiff line numberDiff line change
@@ -1264,6 +1264,32 @@ class NewsAdmin(admin.ModelAdmin):
12641264
readonly_fields = ('uuid',)
12651265

12661266

1267+
### Streaming
1268+
1269+
@admin.register(models.StreamSet)
1270+
class StreamSetAdmin(admin.ModelAdmin):
1271+
list_display = (
1272+
'conference',
1273+
'enabled',
1274+
'name',
1275+
'start_date',
1276+
'end_date',
1277+
)
1278+
list_display_links = (
1279+
'name',
1280+
)
1281+
list_filter = (
1282+
'conference',
1283+
'enabled',
1284+
)
1285+
ordering = (
1286+
'start_date',
1287+
'name',
1288+
'enabled',
1289+
)
1290+
1291+
### Old school registrations:
1292+
12671293
admin.site.register(models.CaptchaQuestion, CaptchaQuestionAdmin)
12681294
admin.site.register(models.Conference, ConferenceAdmin)
12691295
admin.site.register(models.ConferenceTag, ConferenceTagAdmin)
@@ -1274,3 +1300,4 @@ class NewsAdmin(admin.ModelAdmin):
12741300
admin.site.register(models.Sponsor, SponsorAdmin)
12751301
admin.site.register(models.Ticket, TicketAdmin)
12761302
admin.site.register(models.News, NewsAdmin)
1303+

conference/api.py

+89-35
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@
1515
from enum import Enum
1616
import json
1717
from functools import wraps
18+
from hashlib import md5
1819
from django.conf.urls import url as re_path
19-
from django.contrib.auth.hashers import check_password
20+
from django.contrib.auth.hashers import check_password as django_check_password
21+
from django.contrib.auth.hashers import is_password_usable
2022
from django.db.models import Q
2123
from django.http import JsonResponse
2224
from django.views.decorators.csrf import csrf_exempt
@@ -29,6 +31,7 @@
2931
)
3032
from pycon.settings import MATRIX_AUTH_API_DEBUG as DEBUG
3133
from pycon.settings import MATRIX_AUTH_API_ALLOWED_IPS as ALLOWED_IPS
34+
from pycon.settings import SECRET_KEY
3235

3336

3437
# Error Codes
@@ -113,6 +116,57 @@ def wrapper(request, *args, **kwargs):
113116
return wrapper
114117

115118

119+
def check_user_password(user, password):
120+
# Two options: either our User has a valid password, in which case we do
121+
# check it, or not, in which case we check it against the generated passwd.
122+
if not is_password_usable(user.password):
123+
return password == generate_matrix_password(user)
124+
return django_check_password(password, user.password)
125+
126+
127+
def get_assigned_tickets(user, conference):
128+
return Ticket.objects.filter(
129+
Q(fare__conference=conference.code)
130+
& Q(frozen=False) # i.e. the ticket was not cancelled
131+
& Q(orderitem__order___complete=True) # i.e. they paid
132+
& Q(user=user) # i.e. assigned to user
133+
)
134+
135+
136+
def is_speaker(user, conference):
137+
# A speaker is a user with at least one accepted talk in the current
138+
# conference.
139+
try:
140+
speaker = user.speaker
141+
except Speaker.DoesNotExist:
142+
return False
143+
return TalkSpeaker.objects.filter(
144+
speaker=speaker,
145+
talk__conference=conference.code,
146+
talk__status='accepted'
147+
).count() > 0
148+
149+
150+
def generate_matrix_password(user):
151+
"""
152+
Create a temporary password for `user` to that they can login into our
153+
matrix chat server using their email address and that password. This is
154+
only needed for social auth users since they do not have a valid password
155+
in our database.
156+
157+
The generated passowrd is not stored anywhere.
158+
"""
159+
def n_base_b(n, b, nums='0123456789abcdefghijklmnopqrstuvwxyz'):
160+
"""Return `n` in base `b`."""
161+
162+
return ((n == 0) and nums[0]) or \
163+
(n_base_b(n // b, b, nums).lstrip(nums[0]) + nums[n % b])
164+
165+
encoded = md5(str(user.email + SECRET_KEY).encode()).hexdigest()
166+
n = int(encoded, 16)
167+
return n_base_b(n, 36)
168+
169+
116170
@csrf_exempt
117171
@ensure_post
118172
@ensure_https_in_ops
@@ -130,6 +184,13 @@ def isauth(request):
130184
"password": str (not encrypted)
131185
}
132186
187+
or
188+
189+
{
190+
"username": str,
191+
"password": str (not encrypted)
192+
}
193+
133194
Output (JSON)
134195
{
135196
"username": str,
@@ -153,62 +214,55 @@ def isauth(request):
153214
"error": int
154215
}
155216
"""
156-
required_fields = {'email', 'password'}
157-
158217
try:
159218
data = json.loads(request.body)
160219
except json.decoder.JSONDecodeError as ex:
161220
return _error(ApiError.INPUT_ERROR, ex.msg)
162221

163-
if not isinstance(data, dict) or not required_fields.issubset(data.keys()):
222+
if not isinstance(data, dict):
164223
return _error(ApiError.INPUT_ERROR,
165224
'please provide credentials in JSON format')
166-
167-
# First, let's find the user/account profile given the email address
168-
try:
169-
profile = AttendeeProfile.objects.get(user__email=data['email'])
170-
except AttendeeProfile.DoesNotExist:
171-
return _error(ApiError.AUTH_ERROR, 'unknown user')
225+
if 'password' not in data:
226+
return _error(ApiError.INPUT_ERROR,
227+
'please provide user password in JSON payload')
228+
if 'username' not in data and 'email' not in data:
229+
return _error(ApiError.INPUT_ERROR,
230+
'please provide username or email in JSON payload')
231+
232+
# First, let's find the user/account profile given the email/username as
233+
# appropriate.
234+
if 'email' in data:
235+
try:
236+
profile = AttendeeProfile.objects.get(user__email=data['email'])
237+
except AttendeeProfile.DoesNotExist:
238+
return _error(ApiError.AUTH_ERROR, 'unknown user')
239+
elif 'username' in data:
240+
try:
241+
profile = AttendeeProfile.objects.get(
242+
user__username=data['username']
243+
)
244+
except AttendeeProfile.DoesNotExist:
245+
return _error(ApiError.AUTH_ERROR, 'unknown user')
246+
else:
247+
return _error(ApiError.INPUT_ERROR, 'no email/username provided')
172248

173249
# Is the password OK?
174-
if not check_password(data['password'], profile.user.password):
250+
if not check_user_password(profile.user, data['password']):
175251
return _error(ApiError.AUTH_ERROR, 'authentication error')
176252

177-
# Get the tickets **assigned** to the user
178253
conference = Conference.objects.current()
179-
180-
tickets = Ticket.objects.filter(
181-
Q(fare__conference=conference.code)
182-
& Q(frozen=False) # i.e. the ticket was not cancelled
183-
& Q(orderitem__order___complete=True) # i.e. they paid
184-
& Q(user=profile.user) # i.e. assigned to user
185-
)
186-
187-
# A speaker is a user with at least one accepted talk in the current
188-
# conference.
189-
try:
190-
speaker = profile.user.speaker
191-
except Speaker.DoesNotExist:
192-
is_speaker = False
193-
else:
194-
is_speaker = TalkSpeaker.objects.filter(
195-
speaker=speaker,
196-
talk__conference=conference.code,
197-
talk__status='accepted'
198-
).count() > 0
199-
200254
payload = {
201255
"username": profile.user.username,
202256
"first_name": profile.user.first_name,
203257
"last_name": profile.user.last_name,
204258
"email": profile.user.email,
205259
"is_staff": profile.user.is_staff,
206-
"is_speaker": is_speaker,
260+
"is_speaker": is_speaker(profile.user, conference),
207261
"is_active": profile.user.is_active,
208262
"is_minor": profile.is_minor,
209263
"tickets": [
210264
{"fare_name": t.fare.name, "fare_code": t.fare.code}
211-
for t in tickets
265+
for t in get_assigned_tickets(profile.user, conference)
212266
]
213267
}
214268

conference/cfp.py

+6-3
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
from phonenumber_field.formfields import PhoneNumberField
1616

17-
from .forms import ProposalForm
17+
from .forms import ProposalForm, TalkBaseForm
1818
from .models import (
1919
Conference,
2020
AttendeeProfile,
@@ -280,8 +280,8 @@ def add_speaker_to_talk(speaker, talk):
280280

281281

282282
class AddSpeakerToTalkForm(forms.ModelForm):
283-
users_given_name = forms.CharField(label="Given name of the speaker")
284-
users_family_name = forms.CharField(label="Family name of the speaker")
283+
users_given_name = forms.CharField(label="Given name")
284+
users_family_name = forms.CharField(label="Family name")
285285
is_minor = forms.BooleanField(
286286
label="Are you a minor?",
287287
help_text=(
@@ -322,6 +322,9 @@ class AddSpeakerToTalkForm(forms.ModelForm):
322322
),
323323
widget=forms.Textarea(),
324324
)
325+
i_accept_speaker_release = TalkBaseForm.base_fields[
326+
'i_accept_speaker_release'
327+
]
325328

326329
class Meta:
327330
model = AttendeeProfile

conference/forms/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
1+
from .auth import * # noqa
12
from .forms import * # noqa
23
from .talks import * # noqa

conference/forms/auth.py

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from django.contrib.auth import get_user_model
2+
from django.contrib.auth.forms import PasswordResetForm
3+
4+
5+
class CustomPasswordResetForm(PasswordResetForm):
6+
def get_users(self, email):
7+
"""Given an email, return matching user(s) who should receive a reset.
8+
9+
This allows subclasses to more easily customize the default policies
10+
that prevent inactive users and users with unusable passwords from
11+
resetting their password.
12+
"""
13+
user_model = get_user_model()
14+
active_users = user_model._default_manager.filter(**{
15+
'email__iexact': email,
16+
'is_active': True,
17+
})
18+
d = (u for u in active_users)
19+
return d

conference/forms/talks.py

-3
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,6 @@ class TalkUpdateForm(forms.ModelForm):
2222
domain_level = TalkBaseForm.base_fields["domain_level"]
2323
if 'availability' in TalkBaseForm.base_fields:
2424
availability = TalkBaseForm.base_fields["availability"]
25-
i_accept_speaker_release = TalkBaseForm.base_fields[
26-
'i_accept_speaker_release'
27-
]
2825

2926
class Meta:
3027
model = Talk

0 commit comments

Comments
 (0)