From 2cf26ed42487e80ef0cf390c856bc8510c194bbd Mon Sep 17 00:00:00 2001
From: Or Ronai
Date: Thu, 9 Sep 2021 21:51:05 +0300
Subject: [PATCH 01/27] feat: Add registration page
- Added a html template of the signup page
- Added a href button to the signup page from the login page
- Added the form of the signup to the backend and db
---
lms/lmsweb/tools/registration.py | 32 +++++++++++++++++++++++++++++
lms/lmsweb/tools/validators.py | 16 +++++++++++++++
lms/lmsweb/views.py | 35 ++++++++++++++++++++++++++------
lms/static/my.css | 12 +++++++----
lms/templates/_formhelpers.html | 13 ++++++++++++
lms/templates/login.html | 8 +++++---
lms/templates/signup.html | 29 ++++++++++++++++++++++++++
requirements.txt | 1 +
8 files changed, 133 insertions(+), 13 deletions(-)
create mode 100644 lms/lmsweb/tools/validators.py
create mode 100644 lms/templates/_formhelpers.html
create mode 100644 lms/templates/signup.html
diff --git a/lms/lmsweb/tools/registration.py b/lms/lmsweb/tools/registration.py
index e3ee2b50..c2228ff7 100644
--- a/lms/lmsweb/tools/registration.py
+++ b/lms/lmsweb/tools/registration.py
@@ -1,8 +1,14 @@
import csv
+from lms.lmsweb.tools.validators import (
+ UniqueEmailRequired, UniqueUsernameRequired,
+)
import os
import typing
from flask_babel import gettext as _ # type: ignore
+from flask_wtf import FlaskForm
+from wtforms import PasswordField, StringField
+from wtforms.validators import Email, EqualTo, InputRequired, Length
from lms.lmsdb import models
from lms.lmsweb import config
@@ -11,6 +17,32 @@
import requests
+class RegisterForm(FlaskForm):
+ email = StringField(
+ 'Email', validators=[
+ InputRequired(), Email(message=_('אימייל לא תקין')),
+ UniqueEmailRequired, Length(max=60),
+ ],
+ )
+ username = StringField(
+ 'Username', validators=[
+ InputRequired(), UniqueUsernameRequired, Length(min=4, max=20),
+ ],
+ )
+ fullname = StringField(
+ 'Full Name', validators=[InputRequired(), Length(min=3, max=60)],
+ )
+ password = PasswordField(
+ 'Password', validators=[InputRequired(), Length(min=8)], id='password',
+ )
+ confirm = PasswordField(
+ 'Password Confirmation', validators=[
+ InputRequired(),
+ EqualTo('password', message=_('הסיסמה שהוקלדה אינה זהה')),
+ ],
+ )
+
+
class UserToCreate(typing.NamedTuple):
name: str
email: str
diff --git a/lms/lmsweb/tools/validators.py b/lms/lmsweb/tools/validators.py
new file mode 100644
index 00000000..a3272dc3
--- /dev/null
+++ b/lms/lmsweb/tools/validators.py
@@ -0,0 +1,16 @@
+from flask_babel import gettext as _ # type: ignore
+from wtforms.validators import ValidationError
+
+from lms.lmsdb.models import User
+
+
+def UniqueUsernameRequired(form, field):
+ username_exists = User.get_or_none(User.username == field.data)
+ if username_exists:
+ raise ValidationError(_('שם המשתמש כבר נמצא בשימוש'))
+
+
+def UniqueEmailRequired(form, field):
+ email_exists = User.get_or_none(User.mail_address == field.data)
+ if email_exists:
+ raise ValidationError(_('האימייל כבר נמצא בשימוש'))
diff --git a/lms/lmsweb/views.py b/lms/lmsweb/views.py
index ad1d188e..acd9d934 100644
--- a/lms/lmsweb/views.py
+++ b/lms/lmsweb/views.py
@@ -13,7 +13,7 @@
from werkzeug.utils import redirect
from lms.lmsdb.models import (
- ALL_MODELS, Comment, Note, RoleOptions, SharedSolution,
+ ALL_MODELS, Comment, Note, Role, RoleOptions, SharedSolution,
Solution, SolutionFile, User, database,
)
from lms.lmsweb import babel, limiter, routes, webapp
@@ -27,6 +27,7 @@
from lms.lmsweb.redirections import (
PERMISSIVE_CORS, get_next_url, login_manager,
)
+from lms.lmsweb.tools.registration import RegisterForm
from lms.models import (
comments, notes, notifications, share_link, solutions, upload,
)
@@ -83,7 +84,7 @@ def ratelimit_handler(e):
f'{LIMITS_PER_MINUTE}/minute;{LIMITS_PER_HOUR}/hour',
deduct_when=lambda response: response.status_code != 200,
)
-def login(login_error: Optional[str] = None):
+def login(login_message: Optional[str] = None):
if current_user.is_authenticated:
return get_next_url(request.args.get('next'))
@@ -91,7 +92,7 @@ def login(login_error: Optional[str] = None):
username = request.form.get('username')
password = request.form.get('password')
next_page = request.form.get('next')
- login_error = request.args.get('login_error')
+ login_message = request.args.get('login_message')
user = User.get_or_none(username=username)
if request.method == 'POST':
@@ -99,11 +100,33 @@ def login(login_error: Optional[str] = None):
login_user(user)
return get_next_url(next_page)
elif user is None or not user.is_password_valid(password):
- login_error = 'Invalid username or password'
- error_details = {'next': next_page, 'login_error': login_error}
+ login_message = 'Invalid username or password'
+ error_details = {'next': next_page, 'login_message': login_message}
return redirect(url_for('login', **error_details))
- return render_template('login.html', login_error=login_error)
+ return render_template('login.html', login_message=login_message)
+
+
+@webapp.route('/signup', methods=['GET', 'POST'])
+def signup():
+ form = RegisterForm()
+
+ if form.validate_on_submit():
+ User.get_or_create(**{
+ User.mail_address.name: form.email.data,
+ User.username.name: form.username.data,
+ }, defaults={
+ User.fullname.name: form.fullname.data,
+ User.role.name: Role.get_student_role(),
+ User.password.name: form.password.data,
+ User.api_key.name: User.random_password(),
+ })
+
+ return redirect(url_for(
+ 'login', login_message='Registration Successfully',
+ ))
+
+ return render_template('signup.html', form=form)
@webapp.route('/logout')
diff --git a/lms/static/my.css b/lms/static/my.css
index 8380ae18..4e9cd078 100644
--- a/lms/static/my.css
+++ b/lms/static/my.css
@@ -46,25 +46,29 @@ a {
text-align: right;
}
-#login-container {
+#login-container,
+#signup-container {
height: 100%;
align-items: center;
display: flex;
align-self: center;
}
-#login {
+#login,
+#signup {
margin: auto;
max-width: 420px;
padding: 15px;
width: 100%;
}
-#login-logo {
+#login-logo,
+#signup-logo {
margin-bottom: 1rem;
}
-#login-messege-box {
+#login-message-box,
+#signup-errors {
background: #f1c8c8;
color: #860606;
}
diff --git a/lms/templates/_formhelpers.html b/lms/templates/_formhelpers.html
new file mode 100644
index 00000000..4175e804
--- /dev/null
+++ b/lms/templates/_formhelpers.html
@@ -0,0 +1,13 @@
+{% macro render_field(field, cls) %}
+
+
+ {{ field(class=cls, **kwargs) | safe }}
+
+ {% if field.errors %}
+ {% for error in field.errors %}
+ {{ error }}
+ {% endfor %}
+ {% endif %}
+
+
+{% endmacro %}
diff --git a/lms/templates/login.html b/lms/templates/login.html
index 3fb5515e..87b13f27 100644
--- a/lms/templates/login.html
+++ b/lms/templates/login.html
@@ -10,10 +10,10 @@ {{ _('התחברות') }}
{{ _('ברוכים הבאים למערכת התרגילים!') }}
{{ _('הזינו את שם המשתמש והסיסמה שלכם:') }}
- {% if login_error %}
-
+ {% if login_message %}
+
- {{ login_error }}
+ {{ login_message }}
{% endif %}
@@ -34,6 +34,8 @@
{{ _('התחברות') }}
+
+
{{ _('הרשם') }}
diff --git a/lms/templates/signup.html b/lms/templates/signup.html
new file mode 100644
index 00000000..72e5c413
--- /dev/null
+++ b/lms/templates/signup.html
@@ -0,0 +1,29 @@
+{% extends 'base.html' %}
+{% from "_formhelpers.html" import render_field %}
+
+{% block page_content %}
+
+
+
+
 }})
+
{{ _('הרשמה') }}
+
+ {{ _('ברוכים הבאים למערכת התרגילים!') }}
+ {{ _('הזינו אימייל וסיסמה לצורך רישום למערכת:') }}
+
+
+
+
{{ _('חזרה לדף ההתחברות') }}
+
+
+
+{% endblock %}
diff --git a/requirements.txt b/requirements.txt
index 86f6b8c7..a90ba429 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -19,6 +19,7 @@ debugpy==1.0.0rc2
decorator==4.4.2
diff-cover==2.6.1
docker-pycreds==0.4.0
+email-validator==1.1.3
entrypoints==0.3
eradicate==1.0
flake8-alfred==1.1.1
From 3913a5e92860738dfb00cc742a7f09e9b6356ed3 Mon Sep 17 00:00:00 2001
From: Or Ronai
Date: Fri, 10 Sep 2021 19:20:50 +0300
Subject: [PATCH 02/27] feat: Add registration page
- Added confirmation email system
- Added some tests
---
lms/lmsdb/models.py | 10 +++++
lms/lmsweb/__init__.py | 3 ++
lms/lmsweb/config.py.example | 8 ++++
lms/lmsweb/tools/registration.py | 25 ++++++++++-
lms/lmsweb/views.py | 60 +++++++++++++++++++++++----
lms/templates/login.html | 2 +-
requirements.txt | 5 ++-
tests/conftest.py | 9 ++++
tests/samples/config.py.example | 2 +
tests/samples/config_copy.py | 2 +
tests/test_login.py | 11 +++++
tests/test_registration.py | 71 ++++++++++++++++++++++++++++++++
tests/test_users.py | 2 +-
13 files changed, 197 insertions(+), 13 deletions(-)
create mode 100644 tests/test_registration.py
diff --git a/lms/lmsdb/models.py b/lms/lmsdb/models.py
index 8fd5b812..c5f5d11b 100644
--- a/lms/lmsdb/models.py
+++ b/lms/lmsdb/models.py
@@ -34,6 +34,7 @@
class RoleOptions(enum.Enum):
BANNED = 'Banned'
+ NOT_CONFIRMED = 'Not_Confirmed'
STUDENT = 'Student'
STAFF = 'Staff'
VIEWER = 'Viewer'
@@ -67,6 +68,7 @@ class Role(BaseModel):
(RoleOptions.STAFF.value, RoleOptions.STAFF.value),
(RoleOptions.VIEWER.value, RoleOptions.VIEWER.value),
(RoleOptions.STUDENT.value, RoleOptions.STUDENT.value),
+ (RoleOptions.NOT_CONFIRMED.value, RoleOptions.NOT_CONFIRMED.value),
(RoleOptions.BANNED.value, RoleOptions.BANNED.value),
))
@@ -77,6 +79,10 @@ def __str__(self):
def get_banned_role(cls) -> 'Role':
return cls.get(Role.name == RoleOptions.BANNED.value)
+ @classmethod
+ def get_not_confirmed_role(cls) -> 'Role':
+ return cls.get(Role.name == RoleOptions.NOT_CONFIRMED.value)
+
@classmethod
def get_student_role(cls) -> 'Role':
return cls.get(Role.name == RoleOptions.STUDENT.value)
@@ -100,6 +106,10 @@ def by_name(cls, name) -> 'Role':
def is_banned(self) -> bool:
return self.name == RoleOptions.BANNED.value
+ @property
+ def is_not_confirmed(self) -> bool:
+ return self.name == RoleOptions.NOT_CONFIRMED.value
+
@property
def is_student(self) -> bool:
return self.name == RoleOptions.STUDENT.value
diff --git a/lms/lmsweb/__init__.py b/lms/lmsweb/__init__.py
index 5f3a5347..ec48f2ae 100644
--- a/lms/lmsweb/__init__.py
+++ b/lms/lmsweb/__init__.py
@@ -5,6 +5,7 @@
from flask_babel import Babel # type: ignore
from flask_limiter import Limiter # type: ignore
from flask_limiter.util import get_remote_address # type: ignore
+from flask_mail import Mail # type: ignore
from flask_wtf.csrf import CSRFProtect # type: ignore
from lms.utils import config_migrator, debug
@@ -41,6 +42,8 @@
# Localizing configurations
babel = Babel(webapp)
+webmail = Mail(webapp)
+
# Must import files after app's creation
from lms.lmsdb import models # NOQA: F401, E402, I202
diff --git a/lms/lmsweb/config.py.example b/lms/lmsweb/config.py.example
index 0bf3aca7..d0d64764 100644
--- a/lms/lmsweb/config.py.example
+++ b/lms/lmsweb/config.py.example
@@ -9,6 +9,14 @@ MAILGUN_API_KEY = os.getenv('MAILGUN_API_KEY')
MAILGUN_DOMAIN = os.getenv('MAILGUN_DOMAIN', 'mail.pythonic.guru')
SERVER_ADDRESS = os.getenv('SERVER_ADDRESS', '127.0.0.1:5000')
+# MAIL CONFIGURATION
+MAIL_SERVER = 'smtp.gmail.com'
+MAIL_PORT = 465
+MAIL_USE_SSL = True
+MAIL_USE_TLS = False
+MAIL_USERNAME = 'username@gmail.com'
+MAIL_PASSWORD = 'password'
+
# SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = 'Lax'
diff --git a/lms/lmsweb/tools/registration.py b/lms/lmsweb/tools/registration.py
index c2228ff7..c65aeeec 100644
--- a/lms/lmsweb/tools/registration.py
+++ b/lms/lmsweb/tools/registration.py
@@ -5,18 +5,24 @@
import os
import typing
+from flask import url_for
from flask_babel import gettext as _ # type: ignore
+from flask_mail import Message # type: ignore
from flask_wtf import FlaskForm
+from itsdangerous import URLSafeTimedSerializer
from wtforms import PasswordField, StringField
from wtforms.validators import Email, EqualTo, InputRequired, Length
from lms.lmsdb import models
-from lms.lmsweb import config
+from lms.lmsweb import config, webmail
from lms.utils.log import log
import requests
+SERIALIZER = URLSafeTimedSerializer(config.SECRET_KEY)
+
+
class RegisterForm(FlaskForm):
email = StringField(
'Email', validators=[
@@ -38,7 +44,7 @@ class RegisterForm(FlaskForm):
confirm = PasswordField(
'Password Confirmation', validators=[
InputRequired(),
- EqualTo('password', message=_('הסיסמה שהוקלדה אינה זהה')),
+ EqualTo('password', message=_('הסיסמאות שהוקלדו אינן זהות')),
],
)
@@ -161,6 +167,21 @@ def _build_user_text(user: UserToCreate) -> str:
return msg
+def generate_confirmation_token(email: str) -> str:
+ return SERIALIZER.dumps(email, salt='email-confirmation')
+
+
+def send_confirmation_mail(email: str, fullname: str) -> None:
+ token = generate_confirmation_token(email)
+ msg = Message(
+ 'Confirmation Email - Learn Python',
+ sender=f'lms@{config.MAILGUN_DOMAIN}', recipients=[email],
+ )
+ link = url_for('confirm_email', token=token, _external=True)
+ msg.body = f'Hey {fullname},\nYour confirmation link is: {link}'
+ webmail.send(msg)
+
+
if __name__ == '__main__':
registration = UserRegistrationCreator.from_csv_file(config.USERS_CSV)
print(registration.users_to_create) # noqa: T001
diff --git a/lms/lmsweb/views.py b/lms/lmsweb/views.py
index acd9d934..41d255e6 100644
--- a/lms/lmsweb/views.py
+++ b/lms/lmsweb/views.py
@@ -5,10 +5,12 @@
jsonify, make_response, render_template,
request, send_from_directory, url_for,
)
+from flask_babel import gettext as _ # type: ignore
from flask_limiter.util import get_remote_address # type: ignore
from flask_login import ( # type: ignore
current_user, login_required, login_user, logout_user,
)
+from itsdangerous import BadSignature, BadTimeSignature, SignatureExpired
from werkzeug.datastructures import FileStorage
from werkzeug.utils import redirect
@@ -27,7 +29,9 @@
from lms.lmsweb.redirections import (
PERMISSIVE_CORS, get_next_url, login_manager,
)
-from lms.lmsweb.tools.registration import RegisterForm
+from lms.lmsweb.tools.registration import (
+ RegisterForm, SERIALIZER, send_confirmation_mail,
+)
from lms.models import (
comments, notes, notifications, share_link, solutions, upload,
)
@@ -96,11 +100,19 @@ def login(login_message: Optional[str] = None):
user = User.get_or_none(username=username)
if request.method == 'POST':
- if user is not None and user.is_password_valid(password):
+ if (
+ user is not None and user.is_password_valid(password)
+ and not user.role.is_not_confirmed
+ ):
login_user(user)
return get_next_url(next_page)
- elif user is None or not user.is_password_valid(password):
- login_message = 'Invalid username or password'
+ elif (
+ user is None or not user.is_password_valid(password)
+ or user.role.is_not_confirmed
+ ):
+ login_message = _('שם המשתמש או הסיסמה שהוזנו לא תקינים')
+ if user is not None and user.role.is_not_confirmed:
+ login_message = _('עליך לאשר את המייל')
error_details = {'next': next_page, 'login_message': login_message}
return redirect(url_for('login', **error_details))
@@ -110,25 +122,59 @@ def login(login_message: Optional[str] = None):
@webapp.route('/signup', methods=['GET', 'POST'])
def signup():
form = RegisterForm()
-
if form.validate_on_submit():
User.get_or_create(**{
User.mail_address.name: form.email.data,
User.username.name: form.username.data,
}, defaults={
User.fullname.name: form.fullname.data,
- User.role.name: Role.get_student_role(),
+ User.role.name: Role.get_not_confirmed_role(),
User.password.name: form.password.data,
User.api_key.name: User.random_password(),
})
+ send_confirmation_mail(form.email.data, form.fullname.data)
+
return redirect(url_for(
- 'login', login_message='Registration Successfully',
+ 'login', login_message=_('ההרשמה בוצעה בהצלחה'),
))
return render_template('signup.html', form=form)
+@webapp.route('/confirm-email/')
+def confirm_email(token: str):
+ try:
+ email = SERIALIZER.loads(
+ token, salt='email-confirmation', max_age=3600,
+ )
+ user = User.get_or_none(User.mail_address == email)
+ if user is None:
+ return fail(404, f'No such user with email {email}.')
+ if not user.role.is_not_confirmed:
+ return fail(
+ 403, f'User has been already confirmed {user.username}',
+ )
+ update = User.update(
+ role=Role.get_student_role(),
+ ).where(User.username == user.username)
+ update.execute()
+
+ return redirect(url_for(
+ 'login', login_message=(
+ _('המשתמש שלך אומת בהצלחה, כעת הינך יכול להתחבר למערכת'),
+ ),
+ ))
+ except SignatureExpired:
+ return redirect(url_for(
+ 'login', login_message=(
+ _('קישור האימות פג תוקף, קישור חדש נשלח אל תיבת המייל שלך'),
+ ),
+ ))
+ except (BadSignature, BadTimeSignature):
+ return fail(404, 'No such signature')
+
+
@webapp.route('/logout')
@login_required
def logout():
diff --git a/lms/templates/login.html b/lms/templates/login.html
index 87b13f27..b1dde490 100644
--- a/lms/templates/login.html
+++ b/lms/templates/login.html
@@ -35,7 +35,7 @@ {{ _('התחברות') }}
- {{ _('הרשם') }}
+ {{ _('הירשם') }}
diff --git a/requirements.txt b/requirements.txt
index a90ba429..baa16b76 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -42,7 +42,8 @@ flake8==3.8.3
Flask-Admin==1.5.6
Flask-Babel==2.0.0
Flask-Limiter==1.4
-git+git://github.com/maxcountryman/flask-login@e3d8079#egg=flask-login
+Flask-Login==0.5.0
+Flask-Mail==0.9.1
Flask-WTF==0.14.3
Flask==1.1.2
future==0.18.2
@@ -56,7 +57,7 @@ ipykernel==5.3.4
ipython-genutils==0.2.0
ipython==7.18.1
ipywidgets==7.5.1
-itsdangerous==1.1.0
+itsdangerous==2.0.1
jedi==0.17.2
jinja2-pluralize==0.3.0
Jinja2==2.11.3
diff --git a/tests/conftest.py b/tests/conftest.py
index 226209dc..b9c7bfaa 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -118,6 +118,10 @@ def create_banned_user(index: int = 0) -> User:
return create_user(RoleOptions.BANNED.value, index)
+def create_not_confirmed_user(index: int = 0) -> User:
+ return create_user(RoleOptions.NOT_CONFIRMED.value, index)
+
+
def create_student_user(index: int = 0) -> User:
return create_user(RoleOptions.STUDENT.value, index)
@@ -141,6 +145,11 @@ def staff_user(staff_password):
return create_staff_user()
+@pytest.fixture()
+def not_confirmed_user():
+ return create_not_confirmed_user()
+
+
@pytest.fixture()
def student_user():
return create_student_user()
diff --git a/tests/samples/config.py.example b/tests/samples/config.py.example
index dde2b013..f72fe745 100644
--- a/tests/samples/config.py.example
+++ b/tests/samples/config.py.example
@@ -13,6 +13,8 @@ FEATURE_FLAG_CHECK_IDENTICAL_CODE_ON = os.getenv(
'FEATURE_FLAG_CHECK_IDENTICAL_CODE_ON', False,
)
+TESTING = True
+
MAIL_WELCOME_MESSAGE = 'welcome-email'
USERS_CSV = 'users.csv'
diff --git a/tests/samples/config_copy.py b/tests/samples/config_copy.py
index 0d2d93ce..ca2f0573 100644
--- a/tests/samples/config_copy.py
+++ b/tests/samples/config_copy.py
@@ -11,6 +11,8 @@
'FEATURE_FLAG_CHECK_IDENTICAL_CODE_ON', False,
)
+TESTING = True
+
USERS_CSV = 'users.csv'
diff --git a/tests/test_login.py b/tests/test_login.py
index f44edc45..fe5165d7 100644
--- a/tests/test_login.py
+++ b/tests/test_login.py
@@ -23,6 +23,17 @@ def test_login_username_fail(student_user: User):
fail_login_response = client.get('/exercises')
assert fail_login_response.status_code == 302
+ @staticmethod
+ def test_login_not_confirmed_user(not_confirmed_user: User):
+ client = webapp.test_client()
+ login_response = client.post('/login', data={
+ 'username': not_confirmed_user.username,
+ 'password': 'fake pass',
+ }, follow_redirects=True)
+ assert login_response.request.path == '/login'
+ fail_login_response = client.get('/exercises')
+ assert fail_login_response.status_code == 302
+
@staticmethod
def test_login_success(student_user: User):
client = webapp.test_client()
diff --git a/tests/test_registration.py b/tests/test_registration.py
new file mode 100644
index 00000000..86009b60
--- /dev/null
+++ b/tests/test_registration.py
@@ -0,0 +1,71 @@
+from lms.lmsweb.tools.registration import generate_confirmation_token
+from lms.lmsdb.models import User
+from lms.lmsweb import webapp
+
+
+class TestRegistration:
+ @staticmethod
+ def test_invalid_username(student_user: User):
+ client = webapp.test_client()
+ response = client.post('/signup', data={
+ 'email': 'some_name@mail.com',
+ 'username': student_user.username,
+ 'fullname': 'some_name',
+ 'password': 'some_password',
+ 'confirm': 'some_password',
+ }, follow_redirects=True)
+ assert response.request.path == '/signup'
+
+ client.post('/login', data={
+ 'username': student_user.username,
+ 'password': 'some_password',
+ }, follow_redirects=True)
+ fail_login_response = client.get('/exercises')
+ assert fail_login_response.status_code == 302
+
+ @staticmethod
+ def test_invalid_email(student_user: User):
+ client = webapp.test_client()
+ response = client.post('/signup', data={
+ 'email': student_user.mail_address,
+ 'username': 'some_user',
+ 'fullname': 'some_name',
+ 'password': 'some_password',
+ 'confirm': 'some_password',
+ }, follow_redirects=True)
+ assert response.request.path == '/signup'
+
+ client.post('/login', data={
+ 'username': 'some_user',
+ 'password': 'some_password',
+ }, follow_redirects=True)
+ fail_login_response = client.get('/exercises')
+ assert fail_login_response.status_code == 302
+
+ @staticmethod
+ def test_successful_registration():
+ client = webapp.test_client()
+ response = client.post('/signup', data={
+ 'email': 'some_user123@mail.com',
+ 'username': 'some_user',
+ 'fullname': 'some_name',
+ 'password': 'some_password',
+ 'confirm': 'some_password',
+ }, follow_redirects=True)
+ assert response.request.path == '/login'
+
+ client.post('/login', data={
+ 'username': 'some_user',
+ 'password': 'some_password',
+ }, follow_redirects=True)
+ fail_login_response = client.get('/exercises')
+ assert fail_login_response.status_code == 302
+
+ token = generate_confirmation_token('some_user123@mail.com')
+ client.get(f'/confirm-email/{token}', follow_redirects=True)
+ client.post('/login', data={
+ 'username': 'some_user',
+ 'password': 'some_password',
+ }, follow_redirects=True)
+ fail_login_response = client.get('/exercises')
+ assert fail_login_response.status_code == 200
diff --git a/tests/test_users.py b/tests/test_users.py
index 8adb55b4..06210ab1 100644
--- a/tests/test_users.py
+++ b/tests/test_users.py
@@ -62,7 +62,7 @@ def test_logout(student_user: User):
@staticmethod
def test_banned_user(banned_user: User):
- client = client = webapp.test_client()
+ client = webapp.test_client()
login_response = client.post('/login', data={
'username': banned_user.username,
'password': 'fake pass',
From d206c6a67e4482aa70e99dc11d1cef707269d444 Mon Sep 17 00:00:00 2001
From: Or Ronai
Date: Fri, 10 Sep 2021 19:24:56 +0300
Subject: [PATCH 03/27] Fixed sourcey-ai issues
---
lms/lmsdb/models.py | 3 +--
lms/lmsweb/views.py | 2 +-
2 files changed, 2 insertions(+), 3 deletions(-)
diff --git a/lms/lmsdb/models.py b/lms/lmsdb/models.py
index c5f5d11b..9d252d1a 100644
--- a/lms/lmsdb/models.py
+++ b/lms/lmsdb/models.py
@@ -417,8 +417,7 @@ def set_state(self, new_state: SolutionState, **kwargs) -> bool:
**{Solution.state.name: new_state.name},
**kwargs,
).where(requested_solution)
- updated = changes.execute() == 1
- return updated
+ return changes.execute() == 1
def ordered_versions(self) -> Iterable['Solution']:
return Solution.select().where(
diff --git a/lms/lmsweb/views.py b/lms/lmsweb/views.py
index 41d255e6..f79dcdff 100644
--- a/lms/lmsweb/views.py
+++ b/lms/lmsweb/views.py
@@ -171,7 +171,7 @@ def confirm_email(token: str):
_('קישור האימות פג תוקף, קישור חדש נשלח אל תיבת המייל שלך'),
),
))
- except (BadSignature, BadTimeSignature):
+ except BadSignature:
return fail(404, 'No such signature')
From 6ad4d4d05af4b2d98bfe04209c9ba099cc6ec952 Mon Sep 17 00:00:00 2001
From: Or Ronai
Date: Fri, 10 Sep 2021 19:34:04 +0300
Subject: [PATCH 04/27] fixed resend email confirmation
---
lms/lmsweb/views.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/lms/lmsweb/views.py b/lms/lmsweb/views.py
index f79dcdff..0fd92aa7 100644
--- a/lms/lmsweb/views.py
+++ b/lms/lmsweb/views.py
@@ -166,6 +166,7 @@ def confirm_email(token: str):
),
))
except SignatureExpired:
+ send_confirmation_mail(email, user.fullname)
return redirect(url_for(
'login', login_message=(
_('קישור האימות פג תוקף, קישור חדש נשלח אל תיבת המייל שלך'),
From 6b137afe63b6642ecbbed0fc7e27f9da59166ebb Mon Sep 17 00:00:00 2001
From: Or Ronai
Date: Sat, 11 Sep 2021 09:18:39 +0300
Subject: [PATCH 05/27] Added translations
---
.../translations/en/LC_MESSAGES/messages.po | 94 ++++++++++++++-----
1 file changed, 72 insertions(+), 22 deletions(-)
diff --git a/lms/lmsweb/translations/en/LC_MESSAGES/messages.po b/lms/lmsweb/translations/en/LC_MESSAGES/messages.po
index 5b5f12e3..06015a55 100644
--- a/lms/lmsweb/translations/en/LC_MESSAGES/messages.po
+++ b/lms/lmsweb/translations/en/LC_MESSAGES/messages.po
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: lmsweb-1.0\n"
"Report-Msgid-Bugs-To: bugs@mesicka.com\n"
-"POT-Creation-Date: 2020-10-09 11:20+0300\n"
+"POT-Creation-Date: 2021-09-11 09:07+0300\n"
"PO-Revision-Date: 2020-09-16 18:29+0300\n"
"Last-Translator: Or Ronai\n"
"Language: en\n"
@@ -16,9 +16,9 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Babel 2.8.0\n"
+"Generated-By: Babel 2.9.1\n"
-#: lmsdb/models.py:632
+#: lmsdb/models.py:698
msgid "כישלון חמור"
msgstr "Fatal error"
@@ -45,26 +45,63 @@ msgstr "The automatic checker couldn't run your code."
msgid "אחי, בדקת את הקוד שלך?"
msgstr "Bro, did you check your code?"
-#: lmsweb/tools/registration.py:105
+#: lmsweb/views.py:113
+#, fuzzy
+msgid "שם המשתמש או הסיסמה שהוזנו לא תקינים"
+msgstr "Invalid username or password"
+
+#: lmsweb/views.py:115
+msgid "עליך לאשר את המייל"
+msgstr "You have to confirm your registration with the link sent to your email"
+
+#: lmsweb/views.py:139
+msgid "ההרשמה בוצעה בהצלחה"
+msgstr "Registration successfully"
+
+#: lmsweb/views.py:165
+msgid "המשתמש שלך אומת בהצלחה, כעת הינך יכול להתחבר למערכת"
+msgstr "Your user has been successfully confirmed, you can now login"
+
+#: lmsweb/views.py:172
+msgid "קישור האימות פג תוקף, קישור חדש נשלח אל תיבת המייל שלך"
+msgstr "The confirmation link is expired, new link has been sent to your email"
+
+#: lmsweb/tools/registration.py:29
+msgid "אימייל לא תקין"
+msgstr "Invalid email"
+
+#: lmsweb/tools/registration.py:47
+msgid "הסיסמאות שהוקלדו אינן זהות"
+msgstr "The passwords are not identical"
+
+#: lmsweb/tools/registration.py:143
msgid "מערכת הגשת התרגילים"
msgstr "Exercuse submission system"
-#: models/solutions.py:46
+#: lmsweb/tools/validators.py:10
+msgid "שם המשתמש כבר נמצא בשימוש"
+msgstr "The username already in use"
+
+#: lmsweb/tools/validators.py:16
+msgid "האימייל כבר נמצא בשימוש"
+msgstr "The email already in use"
+
+#: models/solutions.py:50
#, python-format
msgid "%(solver)s הגיב לך על בדיקת תרגיל \"%(subject)s\"."
msgstr "%(solver)s has replied for your \"%(subject)s\" check."
-#: models/solutions.py:53
+#: models/solutions.py:57
#, python-format
msgid "%(checker)s הגיב לך על תרגיל \"%(subject)s\"."
msgstr "%(checker)s replied for \"%(subject)s\"."
-#: models/solutions.py:65
+#: models/solutions.py:69
#, python-format
msgid "הפתרון שלך לתרגיל \"%(subject)s\" נבדק."
msgstr "Your solution for the \"%(subject)s\" exercise has been checked."
-#: templates/banned.html:8 templates/login.html:7
+#: templates/banned.html:8 templates/login.html:7 templates/signup.html:8
msgid "תמונת הפרופיל של קורס פייתון"
msgstr "Profile picture of the Python Course"
@@ -84,7 +121,7 @@ msgstr "Exercise submission system for the Python Course"
msgid "תרגילים"
msgstr "Exercises"
-#: templates/exercises.html:21
+#: templates/exercises.html:21 templates/view.html:101
msgid "הערות על התרגיל"
msgstr "Comments for the solution"
@@ -108,7 +145,7 @@ msgstr "All Exercises"
msgid "התחברות"
msgstr "Login"
-#: templates/login.html:10
+#: templates/login.html:10 templates/signup.html:11
msgid "ברוכים הבאים למערכת התרגילים!"
msgstr "Welcome to the exercise system!"
@@ -116,18 +153,22 @@ msgstr "Welcome to the exercise system!"
msgid "הזינו את שם המשתמש והסיסמה שלכם:"
msgstr "Insert your username and password:"
-#: templates/login.html:15 templates/login.html:17
+#: templates/login.html:22 templates/login.html:24
msgid "שם משתמש"
msgstr "Username"
-#: templates/login.html:21 templates/login.html:23
+#: templates/login.html:28 templates/login.html:30
msgid "סיסמה"
msgstr "Password"
-#: templates/login.html:28
+#: templates/login.html:35
msgid "התחבר"
msgstr "Login"
+#: templates/login.html:38 templates/signup.html:22
+msgid "הירשם"
+msgstr "Register"
+
#: templates/navbar.html:8
msgid "הלוגו של פרויקט לומדים פייתון: נחש צהוב על רקע עיגול בצבע תכלת, ומתחתיו כתוב - לומדים פייתון."
msgstr "The logo of the Learning Python project: yellow snake on light blue circle background and behind of it written - Learning Python"
@@ -164,6 +205,19 @@ msgstr "Check Exercises"
msgid "התנתקות"
msgstr "Logout"
+#: templates/signup.html:9
+#, fuzzy
+msgid "הרשמה"
+msgstr "Registration"
+
+#: templates/signup.html:12
+msgid "הזינו אימייל וסיסמה לצורך רישום למערכת:"
+msgstr "Insert your email and password for registration:"
+
+#: templates/signup.html:25
+msgid "חזרה לדף ההתחברות"
+msgstr "Back to login page"
+
#: templates/status.html:7
msgid "חמ\"ל תרגילים"
msgstr "Exercises operations room"
@@ -256,23 +310,23 @@ msgstr "Submitted"
msgid "לא הוגש"
msgstr "Not submitted"
-#: templates/user.html:43
+#: templates/user.html:44
msgid "פתקיות:"
msgstr "Notes:"
-#: templates/user.html:60 templates/user.html:62
+#: templates/user.html:49 templates/user.html:51
msgid "פתקית חדשה"
msgstr "New Note"
-#: templates/user.html:66
+#: templates/user.html:55
msgid "תרגיל משויך:"
msgstr "Exercise:"
-#: templates/user.html:75
+#: templates/user.html:64
msgid "רמת פרטיות:"
msgstr "Privacy Level:"
-#: templates/user.html:81
+#: templates/user.html:70
msgid "הוסף פתקית"
msgstr "Add Note"
@@ -332,10 +386,6 @@ msgstr "Error:"
msgid "שגיאת סגל:"
msgstr "Staff Error:"
-#: templates/view.html:101
-msgid "הערות על התרגיל"
-msgstr "Comments for the exercise"
-
#: templates/view.html:109
msgid "הערות כלליות"
msgstr "General comments"
From 3798a2d3d2460b8820d2766b2f202d84c57a8f7a Mon Sep 17 00:00:00 2001
From: Or Ronai
Date: Sat, 11 Sep 2021 09:23:42 +0300
Subject: [PATCH 06/27] Removed unused module
---
lms/lmsweb/views.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lms/lmsweb/views.py b/lms/lmsweb/views.py
index 0fd92aa7..f54ade2c 100644
--- a/lms/lmsweb/views.py
+++ b/lms/lmsweb/views.py
@@ -10,7 +10,7 @@
from flask_login import ( # type: ignore
current_user, login_required, login_user, logout_user,
)
-from itsdangerous import BadSignature, BadTimeSignature, SignatureExpired
+from itsdangerous import BadSignature, SignatureExpired
from werkzeug.datastructures import FileStorage
from werkzeug.utils import redirect
From 1377e9ae9d5e3cec4eaaf2782b4ca2e0ca86a391 Mon Sep 17 00:00:00 2001
From: Or Ronai
Date: Sat, 11 Sep 2021 09:44:17 +0300
Subject: [PATCH 07/27] Changed versions of requirements
---
requirements.txt | 26 +++++++++++++-------------
1 file changed, 13 insertions(+), 13 deletions(-)
diff --git a/requirements.txt b/requirements.txt
index baa16b76..105f584a 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,22 +1,22 @@
-amqp==5.0.1
+amqp==5.0.6
arrow==0.16.0
atomicwrites==1.4.0
-attrs==20.2.0
-Babel==2.8.0
+attrs==21.2.0
+Babel==2.9.1
backcall==0.2.0
bandit==1.6.2
bench-it==1.0.1
-billiard==3.6.3.0
-celery==5.0.0
-certifi==2020.6.20
-chardet==3.0.4
+billiard==3.6.4.0
+celery==5.1.2
+certifi==2021.5.30
+chardet==4.0.0
click-didyoumean==0.0.3
-click-repl==0.1.6
+click-repl==0.2.0
click==7.1.2
-colorama==0.4.3
-configparser==5.0.0
+colorama==0.4.4
+configparser==5.0.2
debugpy==1.0.0rc2
-decorator==4.4.2
+decorator==5.0.9
diff-cover==2.6.1
docker-pycreds==0.4.0
email-validator==1.1.3
@@ -44,8 +44,8 @@ Flask-Babel==2.0.0
Flask-Limiter==1.4
Flask-Login==0.5.0
Flask-Mail==0.9.1
-Flask-WTF==0.14.3
-Flask==1.1.2
+Flask-WTF==0.15.1
+Flask==2.0.1
future==0.18.2
gitdb==4.0.5
GitPython==3.1.8
From 93da8605052113e980d350c72eec2fa418b342b7 Mon Sep 17 00:00:00 2001
From: Or Ronai
Date: Sat, 11 Sep 2021 09:46:41 +0300
Subject: [PATCH 08/27] Changed versions of requirements
---
requirements.txt | 22 +++++++++++-----------
1 file changed, 11 insertions(+), 11 deletions(-)
diff --git a/requirements.txt b/requirements.txt
index 105f584a..f8ea7d31 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,22 +1,22 @@
-amqp==5.0.6
+amqp==5.0.1
arrow==0.16.0
atomicwrites==1.4.0
-attrs==21.2.0
-Babel==2.9.1
+attrs==20.2.0
+Babel==2.8.0
backcall==0.2.0
bandit==1.6.2
bench-it==1.0.1
-billiard==3.6.4.0
-celery==5.1.2
-certifi==2021.5.30
-chardet==4.0.0
+billiard==3.6.3.0
+celery==5.0.0
+certifi==2020.6.20
+chardet==3.0.4
click-didyoumean==0.0.3
-click-repl==0.2.0
+click-repl==0.1.6
click==7.1.2
-colorama==0.4.4
-configparser==5.0.2
+colorama==0.4.3
+configparser==5.0.0
debugpy==1.0.0rc2
-decorator==5.0.9
+decorator==4.4.2
diff-cover==2.6.1
docker-pycreds==0.4.0
email-validator==1.1.3
From ec523001f6962c1b090735df0df928d74d0c47ae Mon Sep 17 00:00:00 2001
From: Or Ronai
Date: Sat, 11 Sep 2021 09:50:21 +0300
Subject: [PATCH 09/27] Changed versions of requirements
---
requirements.txt | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/requirements.txt b/requirements.txt
index f8ea7d31..10c2cbb6 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -60,14 +60,14 @@ ipywidgets==7.5.1
itsdangerous==2.0.1
jedi==0.17.2
jinja2-pluralize==0.3.0
-Jinja2==2.11.3
+Jinja2==3.0.1
junitparser==1.4.1
jupyter-client==6.1.7
jupyter-console==6.2.0
jupyter-core==4.6.3
jupyter==1.0.0
jupyterlab-pygments==0.1.2
-kombu==5.0.2
+kombu==5.1.0
loguru==0.5.3
MarkupSafe==1.1.1
mccabe==0.6.1
From 4e47d57608f5e6ee9e17bc1a5a50af243479b9ea Mon Sep 17 00:00:00 2001
From: Or Ronai
Date: Sat, 11 Sep 2021 09:51:51 +0300
Subject: [PATCH 10/27] Changed versions of requirements
---
requirements.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/requirements.txt b/requirements.txt
index 10c2cbb6..7a930b68 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -117,7 +117,7 @@ typing-extensions==3.7.4.3
urllib3==1.25.10
vine==5.0.0
wcwidth==0.2.5
-Werkzeug==1.0.1
+Werkzeug==2.0.1
wtf-peewee==3.0.2
WTForms==2.3.3
zipp==3.2.0
From 8d08863e856048417369320d74f1ddf6efc6d599 Mon Sep 17 00:00:00 2001
From: Or Ronai
Date: Sat, 11 Sep 2021 09:53:38 +0300
Subject: [PATCH 11/27] Changed versions of requirements
---
requirements.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/requirements.txt b/requirements.txt
index 7a930b68..26c1b856 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -69,7 +69,7 @@ jupyter==1.0.0
jupyterlab-pygments==0.1.2
kombu==5.1.0
loguru==0.5.3
-MarkupSafe==1.1.1
+MarkupSafe==2.0.1
mccabe==0.6.1
more-itertools==8.5.0
mypy-extensions==0.4.3
From 366b53fd73a132170401ace1780d712dafbd867f Mon Sep 17 00:00:00 2001
From: Or Ronai
Date: Sat, 11 Sep 2021 10:10:41 +0300
Subject: [PATCH 12/27] Changed versions of requirements
---
requirements.txt | 94 ++++++++++++++++++++++++------------------------
1 file changed, 47 insertions(+), 47 deletions(-)
diff --git a/requirements.txt b/requirements.txt
index 26c1b856..874804e1 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,22 +1,22 @@
-amqp==5.0.1
-arrow==0.16.0
+amqp==5.0.6
+arrow==1.1.0
atomicwrites==1.4.0
-attrs==20.2.0
-Babel==2.8.0
+attrs==21.2.0
+Babel==2.9.1
backcall==0.2.0
-bandit==1.6.2
+bandit==1.7.0
bench-it==1.0.1
-billiard==3.6.3.0
-celery==5.0.0
-certifi==2020.6.20
-chardet==3.0.4
+billiard==3.6.4.0
+celery==5.1.2
+certifi==2021.5.30
+chardet==4.0.0
click-didyoumean==0.0.3
-click-repl==0.1.6
+click-repl==0.2.0
click==7.1.2
-colorama==0.4.3
-configparser==5.0.0
+colorama==0.4.4
+configparser==5.0.2
debugpy==1.0.0rc2
-decorator==4.4.2
+decorator==5.0.9
diff-cover==2.6.1
docker-pycreds==0.4.0
email-validator==1.1.3
@@ -38,7 +38,7 @@ flake8-print==3.1.4
flake8-quotes==3.2.0
flake8-tidy-imports==4.1.0
flake8-todo==0.7
-flake8==3.8.3
+flake8==3.9.2
Flask-Admin==1.5.6
Flask-Babel==2.0.0
Flask-Limiter==1.4
@@ -47,24 +47,24 @@ Flask-Mail==0.9.1
Flask-WTF==0.15.1
Flask==2.0.1
future==0.18.2
-gitdb==4.0.5
-GitPython==3.1.8
-gunicorn==20.0.4
-idna==2.10
+gitdb==4.0.7
+GitPython==3.1.18
+gunicorn==20.1.0
+idna==3.2
importlib-metadata==2.0.0
-inflect==4.1.0
-ipykernel==5.3.4
+inflect==5.3.0
+ipykernel==6.0.3
ipython-genutils==0.2.0
-ipython==7.18.1
-ipywidgets==7.5.1
+ipython==7.26.0
+ipywidgets==7.6.3
itsdangerous==2.0.1
-jedi==0.17.2
+jedi==0.18.0
jinja2-pluralize==0.3.0
Jinja2==3.0.1
-junitparser==1.4.1
-jupyter-client==6.1.7
-jupyter-console==6.2.0
-jupyter-core==4.6.3
+junitparser==2.1.1
+jupyter-client==6.1.12
+jupyter-console==6.4.0
+jupyter-core==4.7.1
jupyter==1.0.0
jupyterlab-pygments==0.1.2
kombu==5.1.0
@@ -74,25 +74,25 @@ mccabe==0.6.1
more-itertools==8.5.0
mypy-extensions==0.4.3
mypy==0.782
-numpy==1.19.2
+numpy==1.21.1
oyaml==1.0
-packaging==20.4
-parso==0.7.1
-pathspec==0.8.0
-pbr==5.5.0
-peewee==3.13.3
+packaging==21.0
+parso==0.8.2
+pathspec==0.9.0
+pbr==5.6.0
+peewee==3.14.4
pexpect==4.8.0
pickleshare==0.7.5
pluggy==0.13.1
-prompt-toolkit==3.0.7
+prompt-toolkit==3.0.19
psycopg2-binary==2.8.6
ptyprocess==0.6.0
py==1.10.0
pycairo==1.19.1
-pycodestyle==2.6.0
+pycodestyle==2.7.0
pycparser==2.20
-pyflakes==2.2.0
-Pygments==2.7.4
+pyflakes==2.3.1
+Pygments==2.9.0
pylint==2.6.0
pynvim==0.4.2
pyodbc==4.0.30
@@ -100,21 +100,21 @@ pyparsing==2.4.7
pyrsistent==0.17.3
pytest-env==0.6.2
pytest==6.1.0
-python-dateutil==2.8.1
+python-dateutil==2.8.2
python-dotenv==0.14.0
pytoml==0.1.21
-pytz==2020.1
+pytz==2021.1
PyYAML==5.4
-pyzmq==19.0.2
-requests==2.24.0
-six==1.15.0
-smmap==3.0.4
+pyzmq==22.2.1
+requests==2.26.0
+six==1.16.0
+smmap==4.0.0
sqlfluff==0.3.6
-stevedore==3.2.2
-traitlets==5.0.4
+stevedore==3.4.0
+traitlets==5.0.5
typed-ast==1.4.1
-typing-extensions==3.7.4.3
-urllib3==1.25.10
+typing-extensions==3.10.0.2
+urllib3==1.26.6
vine==5.0.0
wcwidth==0.2.5
Werkzeug==2.0.1
From 1443bad327820c689ce3163b688dc3cd6e4cdcdb Mon Sep 17 00:00:00 2001
From: Or Ronai
Date: Sat, 11 Sep 2021 10:13:43 +0300
Subject: [PATCH 13/27] Removed versions change
---
requirements.txt | 106 +++++++++++++++++++++++------------------------
1 file changed, 53 insertions(+), 53 deletions(-)
diff --git a/requirements.txt b/requirements.txt
index 874804e1..baa16b76 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,22 +1,22 @@
-amqp==5.0.6
-arrow==1.1.0
+amqp==5.0.1
+arrow==0.16.0
atomicwrites==1.4.0
-attrs==21.2.0
-Babel==2.9.1
+attrs==20.2.0
+Babel==2.8.0
backcall==0.2.0
-bandit==1.7.0
+bandit==1.6.2
bench-it==1.0.1
-billiard==3.6.4.0
-celery==5.1.2
-certifi==2021.5.30
-chardet==4.0.0
+billiard==3.6.3.0
+celery==5.0.0
+certifi==2020.6.20
+chardet==3.0.4
click-didyoumean==0.0.3
-click-repl==0.2.0
+click-repl==0.1.6
click==7.1.2
-colorama==0.4.4
-configparser==5.0.2
+colorama==0.4.3
+configparser==5.0.0
debugpy==1.0.0rc2
-decorator==5.0.9
+decorator==4.4.2
diff-cover==2.6.1
docker-pycreds==0.4.0
email-validator==1.1.3
@@ -38,61 +38,61 @@ flake8-print==3.1.4
flake8-quotes==3.2.0
flake8-tidy-imports==4.1.0
flake8-todo==0.7
-flake8==3.9.2
+flake8==3.8.3
Flask-Admin==1.5.6
Flask-Babel==2.0.0
Flask-Limiter==1.4
Flask-Login==0.5.0
Flask-Mail==0.9.1
-Flask-WTF==0.15.1
-Flask==2.0.1
+Flask-WTF==0.14.3
+Flask==1.1.2
future==0.18.2
-gitdb==4.0.7
-GitPython==3.1.18
-gunicorn==20.1.0
-idna==3.2
+gitdb==4.0.5
+GitPython==3.1.8
+gunicorn==20.0.4
+idna==2.10
importlib-metadata==2.0.0
-inflect==5.3.0
-ipykernel==6.0.3
+inflect==4.1.0
+ipykernel==5.3.4
ipython-genutils==0.2.0
-ipython==7.26.0
-ipywidgets==7.6.3
+ipython==7.18.1
+ipywidgets==7.5.1
itsdangerous==2.0.1
-jedi==0.18.0
+jedi==0.17.2
jinja2-pluralize==0.3.0
-Jinja2==3.0.1
-junitparser==2.1.1
-jupyter-client==6.1.12
-jupyter-console==6.4.0
-jupyter-core==4.7.1
+Jinja2==2.11.3
+junitparser==1.4.1
+jupyter-client==6.1.7
+jupyter-console==6.2.0
+jupyter-core==4.6.3
jupyter==1.0.0
jupyterlab-pygments==0.1.2
-kombu==5.1.0
+kombu==5.0.2
loguru==0.5.3
-MarkupSafe==2.0.1
+MarkupSafe==1.1.1
mccabe==0.6.1
more-itertools==8.5.0
mypy-extensions==0.4.3
mypy==0.782
-numpy==1.21.1
+numpy==1.19.2
oyaml==1.0
-packaging==21.0
-parso==0.8.2
-pathspec==0.9.0
-pbr==5.6.0
-peewee==3.14.4
+packaging==20.4
+parso==0.7.1
+pathspec==0.8.0
+pbr==5.5.0
+peewee==3.13.3
pexpect==4.8.0
pickleshare==0.7.5
pluggy==0.13.1
-prompt-toolkit==3.0.19
+prompt-toolkit==3.0.7
psycopg2-binary==2.8.6
ptyprocess==0.6.0
py==1.10.0
pycairo==1.19.1
-pycodestyle==2.7.0
+pycodestyle==2.6.0
pycparser==2.20
-pyflakes==2.3.1
-Pygments==2.9.0
+pyflakes==2.2.0
+Pygments==2.7.4
pylint==2.6.0
pynvim==0.4.2
pyodbc==4.0.30
@@ -100,24 +100,24 @@ pyparsing==2.4.7
pyrsistent==0.17.3
pytest-env==0.6.2
pytest==6.1.0
-python-dateutil==2.8.2
+python-dateutil==2.8.1
python-dotenv==0.14.0
pytoml==0.1.21
-pytz==2021.1
+pytz==2020.1
PyYAML==5.4
-pyzmq==22.2.1
-requests==2.26.0
-six==1.16.0
-smmap==4.0.0
+pyzmq==19.0.2
+requests==2.24.0
+six==1.15.0
+smmap==3.0.4
sqlfluff==0.3.6
-stevedore==3.4.0
-traitlets==5.0.5
+stevedore==3.2.2
+traitlets==5.0.4
typed-ast==1.4.1
-typing-extensions==3.10.0.2
-urllib3==1.26.6
+typing-extensions==3.7.4.3
+urllib3==1.25.10
vine==5.0.0
wcwidth==0.2.5
-Werkzeug==2.0.1
+Werkzeug==1.0.1
wtf-peewee==3.0.2
WTForms==2.3.3
zipp==3.2.0
From 1d00a274e3f0f57573135ceeab1c34aa6ad3d4b8 Mon Sep 17 00:00:00 2001
From: Or Ronai
Date: Sat, 11 Sep 2021 12:54:27 +0300
Subject: [PATCH 14/27] Fixed tests
---
tests/conftest.py | 26 +++++++++++++++++++++++++-
tests/samples/config.py.example | 1 -
tests/samples/config_copy.py | 1 -
tests/test_login.py | 2 +-
tests/test_registration.py | 23 +++++++++++++----------
5 files changed, 39 insertions(+), 14 deletions(-)
diff --git a/tests/conftest.py b/tests/conftest.py
index b9c7bfaa..d9966dba 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,11 +1,11 @@
import datetime
from functools import wraps
-from lms.models import notes
import os
import random
import string
from typing import List, Optional
+from flask import template_rendered
from flask.testing import FlaskClient
from peewee import SqliteDatabase
import pytest
@@ -49,6 +49,11 @@ def db(db_in_memory):
db_in_memory.rollback()
+@pytest.fixture(autouse=True, scope='session')
+def app():
+ return webapp
+
+
@pytest.fixture(autouse=True, scope='session')
def celery_eager():
public_app.conf.update(task_always_eager=True)
@@ -65,6 +70,11 @@ def webapp_configurations():
limiter.enabled = False
+@pytest.fixture(autouse=True, scope='session')
+def disable_mail_sending():
+ webapp.config['TESTING'] = True
+
+
def disable_shareable_solutions():
webapp.config['SHAREABLE_SOLUTIONS'] = False
@@ -168,6 +178,20 @@ def admin_user():
)
+@pytest.fixture(autouse=True, scope='session')
+def captured_templates(app):
+ recorded = []
+
+ def record(sender, template, context, **kwargs):
+ recorded.append((template, context))
+
+ template_rendered.connect(record, app)
+ try:
+ yield recorded
+ finally:
+ template_rendered.disconnect(record, app)
+
+
def create_notification(
student_user: User,
solution: Solution,
diff --git a/tests/samples/config.py.example b/tests/samples/config.py.example
index f72fe745..abdf7fc2 100644
--- a/tests/samples/config.py.example
+++ b/tests/samples/config.py.example
@@ -13,7 +13,6 @@ FEATURE_FLAG_CHECK_IDENTICAL_CODE_ON = os.getenv(
'FEATURE_FLAG_CHECK_IDENTICAL_CODE_ON', False,
)
-TESTING = True
MAIL_WELCOME_MESSAGE = 'welcome-email'
diff --git a/tests/samples/config_copy.py b/tests/samples/config_copy.py
index ca2f0573..d56c7b9d 100644
--- a/tests/samples/config_copy.py
+++ b/tests/samples/config_copy.py
@@ -11,7 +11,6 @@
'FEATURE_FLAG_CHECK_IDENTICAL_CODE_ON', False,
)
-TESTING = True
USERS_CSV = 'users.csv'
diff --git a/tests/test_login.py b/tests/test_login.py
index fe5165d7..714c66f7 100644
--- a/tests/test_login.py
+++ b/tests/test_login.py
@@ -14,7 +14,7 @@ def test_login_password_fail(student_user: User):
assert fail_login_response.status_code == 302
@staticmethod
- def test_login_username_fail(student_user: User):
+ def test_login_username_fail():
client = webapp.test_client()
client.post('/login', data={
'username': 'wrong_user',
diff --git a/tests/test_registration.py b/tests/test_registration.py
index 86009b60..956b0d18 100644
--- a/tests/test_registration.py
+++ b/tests/test_registration.py
@@ -3,18 +3,19 @@
from lms.lmsweb import webapp
-class TestRegistration:
+class TestReg:
@staticmethod
- def test_invalid_username(student_user: User):
+ def test_invalid_username(student_user: User, captured_templates):
client = webapp.test_client()
- response = client.post('/signup', data={
+ client.post('/signup', data={
'email': 'some_name@mail.com',
'username': student_user.username,
'fullname': 'some_name',
'password': 'some_password',
'confirm': 'some_password',
}, follow_redirects=True)
- assert response.request.path == '/signup'
+ template, _ = captured_templates[-1]
+ assert template.name == "signup.html"
client.post('/login', data={
'username': student_user.username,
@@ -24,16 +25,17 @@ def test_invalid_username(student_user: User):
assert fail_login_response.status_code == 302
@staticmethod
- def test_invalid_email(student_user: User):
+ def test_invalid_email(student_user: User, captured_templates):
client = webapp.test_client()
- response = client.post('/signup', data={
+ client.post('/signup', data={
'email': student_user.mail_address,
'username': 'some_user',
'fullname': 'some_name',
'password': 'some_password',
'confirm': 'some_password',
}, follow_redirects=True)
- assert response.request.path == '/signup'
+ template, _ = captured_templates[-1]
+ assert template.name == 'signup.html'
client.post('/login', data={
'username': 'some_user',
@@ -43,16 +45,17 @@ def test_invalid_email(student_user: User):
assert fail_login_response.status_code == 302
@staticmethod
- def test_successful_registration():
+ def test_successful_registration(captured_templates):
client = webapp.test_client()
- response = client.post('/signup', data={
+ client.post('/signup', data={
'email': 'some_user123@mail.com',
'username': 'some_user',
'fullname': 'some_name',
'password': 'some_password',
'confirm': 'some_password',
}, follow_redirects=True)
- assert response.request.path == '/login'
+ template, _ = captured_templates[-1]
+ assert template.name == 'login.html'
client.post('/login', data={
'username': 'some_user',
From 036bdf5ef273a619aaadccf6af729156f4c44138 Mon Sep 17 00:00:00 2001
From: Or Ronai
Date: Sat, 11 Sep 2021 13:18:44 +0300
Subject: [PATCH 15/27] Fixed a test
---
tests/test_login.py | 8 ++++++--
tests/test_registration.py | 2 +-
2 files changed, 7 insertions(+), 3 deletions(-)
diff --git a/tests/test_login.py b/tests/test_login.py
index 714c66f7..642440c2 100644
--- a/tests/test_login.py
+++ b/tests/test_login.py
@@ -24,13 +24,17 @@ def test_login_username_fail():
assert fail_login_response.status_code == 302
@staticmethod
- def test_login_not_confirmed_user(not_confirmed_user: User):
+ def test_login_not_confirmed_user(
+ not_confirmed_user: User, captured_templates,
+ ):
client = webapp.test_client()
login_response = client.post('/login', data={
'username': not_confirmed_user.username,
'password': 'fake pass',
}, follow_redirects=True)
- assert login_response.request.path == '/login'
+ template, _ = captured_templates[-1]
+ assert template.name == 'login.html'
+
fail_login_response = client.get('/exercises')
assert fail_login_response.status_code == 302
diff --git a/tests/test_registration.py b/tests/test_registration.py
index 956b0d18..800739c7 100644
--- a/tests/test_registration.py
+++ b/tests/test_registration.py
@@ -3,7 +3,7 @@
from lms.lmsweb import webapp
-class TestReg:
+class TestRegistration:
@staticmethod
def test_invalid_username(student_user: User, captured_templates):
client = webapp.test_client()
From b4261022ad208e03430945fc8977a74e9487bd9f Mon Sep 17 00:00:00 2001
From: Or Ronai
Date: Sat, 11 Sep 2021 14:55:21 +0300
Subject: [PATCH 16/27] Fixed test and updated client fixture
---
lms/lmsweb/tools/registration.py | 5 +++--
tests/conftest.py | 12 ++++++------
tests/test_flask_limiter.py | 11 +++++------
tests/test_login.py | 17 +++++++----------
tests/test_registration.py | 16 +++++++++-------
tests/test_users.py | 6 +++---
6 files changed, 33 insertions(+), 34 deletions(-)
diff --git a/lms/lmsweb/tools/registration.py b/lms/lmsweb/tools/registration.py
index c65aeeec..643d8665 100644
--- a/lms/lmsweb/tools/registration.py
+++ b/lms/lmsweb/tools/registration.py
@@ -14,7 +14,7 @@
from wtforms.validators import Email, EqualTo, InputRequired, Length
from lms.lmsdb import models
-from lms.lmsweb import config, webmail
+from lms.lmsweb import config, webapp, webmail
from lms.utils.log import log
import requests
@@ -179,7 +179,8 @@ def send_confirmation_mail(email: str, fullname: str) -> None:
)
link = url_for('confirm_email', token=token, _external=True)
msg.body = f'Hey {fullname},\nYour confirmation link is: {link}'
- webmail.send(msg)
+ if not webapp.config.get('TESTING'):
+ webmail.send(msg)
if __name__ == '__main__':
diff --git a/tests/conftest.py b/tests/conftest.py
index d9966dba..f6fa8fff 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -49,9 +49,9 @@ def db(db_in_memory):
db_in_memory.rollback()
-@pytest.fixture(autouse=True, scope='session')
-def app():
- return webapp
+@pytest.fixture(autouse=True, scope='function')
+def client():
+ return webapp.test_client()
@pytest.fixture(autouse=True, scope='session')
@@ -179,17 +179,17 @@ def admin_user():
@pytest.fixture(autouse=True, scope='session')
-def captured_templates(app):
+def captured_templates():
recorded = []
def record(sender, template, context, **kwargs):
recorded.append((template, context))
- template_rendered.connect(record, app)
+ template_rendered.connect(record, webapp)
try:
yield recorded
finally:
- template_rendered.disconnect(record, app)
+ template_rendered.disconnect(record, webapp)
def create_notification(
diff --git a/tests/test_flask_limiter.py b/tests/test_flask_limiter.py
index 311cff55..3457230d 100644
--- a/tests/test_flask_limiter.py
+++ b/tests/test_flask_limiter.py
@@ -1,3 +1,5 @@
+from flask.testing import FlaskClient
+
from lms.lmsweb import routes
from lms.lmsdb.models import Solution, User
from lms.lmsweb import webapp
@@ -7,8 +9,7 @@
class TestLimiter:
@staticmethod
@conftest.use_limiter
- def test_limiter_login_fails(student_user: User):
- client = webapp.test_client()
+ def test_limiter_login_fails(client: FlaskClient, student_user: User):
for _ in range(webapp.config['LIMITS_PER_MINUTE'] - 1):
response = client.post('/login', data={
'username': student_user.username,
@@ -25,16 +26,14 @@ def test_limiter_login_fails(student_user: User):
@staticmethod
@conftest.use_limiter
- def test_limiter_login_refreshes():
- client = webapp.test_client()
+ def test_limiter_login_refreshes(client: FlaskClient):
for _ in range(webapp.config['LIMITS_PER_MINUTE'] + 1):
response = client.get('/login')
assert response.status_code == 200
@staticmethod
@conftest.use_limiter
- def test_limiter_login_success(student_user: User):
- client = webapp.test_client()
+ def test_limiter_login_success(client: FlaskClient, student_user: User):
client.post('/login', data={
'username': student_user.username,
'password': 'fake5',
diff --git a/tests/test_login.py b/tests/test_login.py
index 642440c2..c2d90074 100644
--- a/tests/test_login.py
+++ b/tests/test_login.py
@@ -1,11 +1,11 @@
+from flask.testing import FlaskClient
+
from lms.lmsdb.models import User
-from lms.lmsweb import webapp
class TestLogin:
@staticmethod
- def test_login_password_fail(student_user: User):
- client = webapp.test_client()
+ def test_login_password_fail(client: FlaskClient, student_user: User):
client.post('/login', data={
'username': student_user.username,
'password': 'wrong_pass',
@@ -14,8 +14,7 @@ def test_login_password_fail(student_user: User):
assert fail_login_response.status_code == 302
@staticmethod
- def test_login_username_fail():
- client = webapp.test_client()
+ def test_login_username_fail(client: FlaskClient):
client.post('/login', data={
'username': 'wrong_user',
'password': 'fake pass',
@@ -25,10 +24,9 @@ def test_login_username_fail():
@staticmethod
def test_login_not_confirmed_user(
- not_confirmed_user: User, captured_templates,
+ client: FlaskClient, not_confirmed_user: User, captured_templates,
):
- client = webapp.test_client()
- login_response = client.post('/login', data={
+ client.post('/login', data={
'username': not_confirmed_user.username,
'password': 'fake pass',
}, follow_redirects=True)
@@ -39,8 +37,7 @@ def test_login_not_confirmed_user(
assert fail_login_response.status_code == 302
@staticmethod
- def test_login_success(student_user: User):
- client = webapp.test_client()
+ def test_login_success(client: FlaskClient, student_user: User):
client.post('/login', data={
'username': student_user.username,
'password': 'fake pass',
diff --git a/tests/test_registration.py b/tests/test_registration.py
index 800739c7..ed53c223 100644
--- a/tests/test_registration.py
+++ b/tests/test_registration.py
@@ -1,12 +1,14 @@
+from flask.testing import FlaskClient
+
from lms.lmsweb.tools.registration import generate_confirmation_token
from lms.lmsdb.models import User
-from lms.lmsweb import webapp
class TestRegistration:
@staticmethod
- def test_invalid_username(student_user: User, captured_templates):
- client = webapp.test_client()
+ def test_invalid_username(
+ client: FlaskClient, student_user: User, captured_templates,
+ ):
client.post('/signup', data={
'email': 'some_name@mail.com',
'username': student_user.username,
@@ -25,8 +27,9 @@ def test_invalid_username(student_user: User, captured_templates):
assert fail_login_response.status_code == 302
@staticmethod
- def test_invalid_email(student_user: User, captured_templates):
- client = webapp.test_client()
+ def test_invalid_email(
+ client: FlaskClient, student_user: User, captured_templates,
+ ):
client.post('/signup', data={
'email': student_user.mail_address,
'username': 'some_user',
@@ -45,8 +48,7 @@ def test_invalid_email(student_user: User, captured_templates):
assert fail_login_response.status_code == 302
@staticmethod
- def test_successful_registration(captured_templates):
- client = webapp.test_client()
+ def test_successful_registration(client: FlaskClient, captured_templates):
client.post('/signup', data={
'email': 'some_user123@mail.com',
'username': 'some_user',
diff --git a/tests/test_users.py b/tests/test_users.py
index 06210ab1..043570ca 100644
--- a/tests/test_users.py
+++ b/tests/test_users.py
@@ -1,5 +1,6 @@
+from flask.testing import FlaskClient
+
from lms.lmsdb.models import User
-from lms.lmsweb import webapp
from tests import conftest
@@ -61,8 +62,7 @@ def test_logout(student_user: User):
assert logout_response.status_code == 200
@staticmethod
- def test_banned_user(banned_user: User):
- client = webapp.test_client()
+ def test_banned_user(client: FlaskClient, banned_user: User):
login_response = client.post('/login', data={
'username': banned_user.username,
'password': 'fake pass',
From c2ffa37a7bfd176fa5244b4a9f476cbfb4960e57 Mon Sep 17 00:00:00 2001
From: Or Ronai
Date: Sat, 11 Sep 2021 15:10:30 +0300
Subject: [PATCH 17/27] Added tests for coverage
---
tests/test_registration.py | 28 ++++++++++++++++++++++++++++
1 file changed, 28 insertions(+)
diff --git a/tests/test_registration.py b/tests/test_registration.py
index ed53c223..05ef637f 100644
--- a/tests/test_registration.py
+++ b/tests/test_registration.py
@@ -74,3 +74,31 @@ def test_successful_registration(client: FlaskClient, captured_templates):
}, follow_redirects=True)
fail_login_response = client.get('/exercises')
assert fail_login_response.status_code == 200
+
+ @staticmethod
+ def test_bad_token(client: FlaskClient):
+ bad_token = "fake-token43@$@"
+ fail_confirm_response = client.get(
+ f'/confirm-email/{bad_token}', follow_redirects=True,
+ )
+ assert fail_confirm_response.status_code == 404
+
+ @staticmethod
+ def test_use_token_twice(client: FlaskClient):
+ client.post('/signup', data={
+ 'email': 'some_user123@mail.com',
+ 'username': 'some_user',
+ 'fullname': 'some_name',
+ 'password': 'some_password',
+ 'confirm': 'some_password',
+ }, follow_redirects=True)
+ token = generate_confirmation_token('some_user123@mail.com')
+ success_token_response = client.get(
+ f'/confirm-email/{token}', follow_redirects=True,
+ )
+ assert success_token_response.status_code == 200
+
+ fail_token_response = client.get(
+ f'/confirm-email/{token}', follow_redirects=True,
+ )
+ assert fail_token_response.status_code == 403
From fda15583e72c296f5887bfc709dbcc49d2e2e539 Mon Sep 17 00:00:00 2001
From: Or Ronai
Date: Sat, 11 Sep 2021 21:32:48 +0300
Subject: [PATCH 18/27] Added a test for signature expired
---
lms/lmsweb/config.py.example | 2 ++
lms/lmsweb/views.py | 7 ++++--
tests/test_registration.py | 43 ++++++++++++++++++++++++++++++++++--
3 files changed, 48 insertions(+), 4 deletions(-)
diff --git a/lms/lmsweb/config.py.example b/lms/lmsweb/config.py.example
index bee1d7eb..ed899797 100644
--- a/lms/lmsweb/config.py.example
+++ b/lms/lmsweb/config.py.example
@@ -10,6 +10,8 @@ MAILGUN_DOMAIN = os.getenv('MAILGUN_DOMAIN', 'mail.pythonic.guru')
SERVER_ADDRESS = os.getenv('SERVER_ADDRESS', '127.0.0.1:5000')
SITE_NAME = 'Learning Python'
+CONFIRMATION_TIME = 3600
+
# MAIL CONFIGURATION
MAIL_SERVER = 'smtp.gmail.com'
MAIL_PORT = 465
diff --git a/lms/lmsweb/views.py b/lms/lmsweb/views.py
index a252ce85..94139508 100644
--- a/lms/lmsweb/views.py
+++ b/lms/lmsweb/views.py
@@ -23,7 +23,8 @@
AdminModelView, SPECIAL_MAPPING, admin, managers_only,
)
from lms.lmsweb.config import (
- LANGUAGES, LIMITS_PER_HOUR, LIMITS_PER_MINUTE, LOCALE, MAX_UPLOAD_SIZE,
+ CONFIRMATION_TIME, LANGUAGES, LIMITS_PER_HOUR,
+ LIMITS_PER_MINUTE, LOCALE, MAX_UPLOAD_SIZE,
)
from lms.lmsweb.forms.register import RegisterForm
from lms.lmsweb.manifest import MANIFEST
@@ -157,7 +158,9 @@ def confirm_email(user_id: int, token: str):
403, f'User has been already confirmed {user.username}',
)
- SERIALIZER.loads(token, salt=retrieve_salt(user), max_age=3600)
+ SERIALIZER.loads(
+ token, salt=retrieve_salt(user), max_age=CONFIRMATION_TIME,
+ )
update = User.update(
role=Role.get_student_role(),
).where(User.username == user.username)
diff --git a/tests/test_registration.py b/tests/test_registration.py
index 0b97e75c..dd5188c0 100644
--- a/tests/test_registration.py
+++ b/tests/test_registration.py
@@ -1,5 +1,9 @@
+import time
+from unittest.mock import Mock, patch
+
from flask.testing import FlaskClient
+from lms.lmsweb.config import CONFIRMATION_TIME
from lms.lmsdb.models import User
from lms.models.register import generate_confirmation_token
@@ -73,8 +77,8 @@ def test_successful_registration(client: FlaskClient, captured_templates):
'username': 'some_user',
'password': 'some_password',
}, follow_redirects=True)
- fail_login_response = client.get('/exercises')
- assert fail_login_response.status_code == 200
+ success_login_response = client.get('/exercises')
+ assert success_login_response.status_code == 200
@staticmethod
def test_bad_token_or_id(client: FlaskClient):
@@ -110,3 +114,38 @@ def test_use_token_twice(client: FlaskClient):
f'/confirm-email/{user.id}/{token}', follow_redirects=True,
)
assert fail_token_response.status_code == 403
+
+ @staticmethod
+ def test_expired_token(client: FlaskClient):
+ client.post('/signup', data={
+ 'email': 'some_user123@mail.com',
+ 'username': 'some_user',
+ 'fullname': 'some_name',
+ 'password': 'some_password',
+ 'confirm': 'some_password',
+ }, follow_redirects=True)
+ user = User.get_or_none(User.username == 'some_user')
+ token = generate_confirmation_token(user)
+
+ fake_time = time.time() + CONFIRMATION_TIME + 1
+ with patch('time.time', Mock(return_value=fake_time)):
+ client.get(
+ f'/confirm-email/{user.id}/{token}', follow_redirects=True,
+ )
+ client.post('/login', data={
+ 'username': 'some_user',
+ 'password': 'some_password',
+ }, follow_redirects=True)
+ fail_login_response = client.get('/exercises')
+ assert fail_login_response.status_code == 302
+
+ token = generate_confirmation_token(user)
+ client.get(
+ f'/confirm-email/{user.id}/{token}', follow_redirects=True,
+ )
+ client.post('/login', data={
+ 'username': 'some_user',
+ 'password': 'some_password',
+ }, follow_redirects=True)
+ success_login_response = client.get('/exercises')
+ assert success_login_response.status_code == 200
From 6ad81a2d817ba3f729680636cea627cca7a3cb9c Mon Sep 17 00:00:00 2001
From: Or Ronai
Date: Sat, 11 Sep 2021 21:36:44 +0300
Subject: [PATCH 19/27] Removed unnecessary condition
---
lms/lmsweb/views.py | 5 +----
1 file changed, 1 insertion(+), 4 deletions(-)
diff --git a/lms/lmsweb/views.py b/lms/lmsweb/views.py
index 94139508..5079aad7 100644
--- a/lms/lmsweb/views.py
+++ b/lms/lmsweb/views.py
@@ -109,10 +109,7 @@ def login(login_message: Optional[str] = None):
login_user(user)
return get_next_url(next_page)
- elif (
- user is None or not user.is_password_valid(password)
- or user.role.is_unverified
- ):
+ elif user is None or not user.is_password_valid(password):
login_message = _('שם המשתמש או הסיסמה שהוזנו לא תקינים')
error_details = {'next': next_page, 'login_message': login_message}
return redirect(url_for('login', **error_details))
From eb717f18d0f6fa73230f6fd5157a4498cd6be468 Mon Sep 17 00:00:00 2001
From: Or Ronai
Date: Sat, 11 Sep 2021 21:40:26 +0300
Subject: [PATCH 20/27] Added role attribute
---
lms/lmsweb/views.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lms/lmsweb/views.py b/lms/lmsweb/views.py
index 5079aad7..2ead24f5 100644
--- a/lms/lmsweb/views.py
+++ b/lms/lmsweb/views.py
@@ -114,7 +114,7 @@ def login(login_message: Optional[str] = None):
error_details = {'next': next_page, 'login_message': login_message}
return redirect(url_for('login', **error_details))
- elif user.is_unverified:
+ elif user.role.is_unverified:
login_message = _('עליך לאשר את המייל')
error_details = {'next': next_page, 'login_message': login_message}
return redirect(url_for('login', **error_details))
From 7e87dadee94fb24906cc3a08ecf3308aefa9aa1b Mon Sep 17 00:00:00 2001
From: Or Ronai
Date: Sun, 12 Sep 2021 11:22:51 +0300
Subject: [PATCH 21/27] - Fixed babel translations - Added few _('...') instead
of some english phrases - Moved out the auth logic into models/users - Added
MAIL_DEFAULT_SENDER variable to the config - Changed the retrieve_salt method
---
lms/lmsweb/config.py.example | 1 +
lms/lmsweb/tools/validators.py | 9 ++-
.../translations/en/LC_MESSAGES/messages.po | 63 +++++++++-------
lms/lmsweb/views.py | 71 ++++++++-----------
lms/models/errors.py | 8 +--
lms/models/register.py | 18 +----
lms/models/users.py | 28 ++++++++
lms/templates/signup.html | 12 ++--
8 files changed, 118 insertions(+), 92 deletions(-)
create mode 100644 lms/models/users.py
diff --git a/lms/lmsweb/config.py.example b/lms/lmsweb/config.py.example
index ed899797..58917cf8 100644
--- a/lms/lmsweb/config.py.example
+++ b/lms/lmsweb/config.py.example
@@ -19,6 +19,7 @@ MAIL_USE_SSL = True
MAIL_USE_TLS = False
MAIL_USERNAME = 'username@gmail.com'
MAIL_PASSWORD = 'password'
+MAIL_DEFAULT_SENDER = 'username@gmail.com'
# ADMIN PANEL
FLASK_ADMIN_FLUID_LAYOUT = True
diff --git a/lms/lmsweb/tools/validators.py b/lms/lmsweb/tools/validators.py
index a3272dc3..2f502b60 100644
--- a/lms/lmsweb/tools/validators.py
+++ b/lms/lmsweb/tools/validators.py
@@ -1,16 +1,21 @@
from flask_babel import gettext as _ # type: ignore
+from wtforms.fields.core import StringField
from wtforms.validators import ValidationError
from lms.lmsdb.models import User
-def UniqueUsernameRequired(form, field):
+def UniqueUsernameRequired(
+ _form: 'RegisterForm', field: StringField, # type: ignore # NOQA: F821
+) -> None:
username_exists = User.get_or_none(User.username == field.data)
if username_exists:
raise ValidationError(_('שם המשתמש כבר נמצא בשימוש'))
-def UniqueEmailRequired(form, field):
+def UniqueEmailRequired(
+ _form: 'RegisterForm', field: StringField, # type: ignore # NOQA: F821
+) -> None:
email_exists = User.get_or_none(User.mail_address == field.data)
if email_exists:
raise ValidationError(_('האימייל כבר נמצא בשימוש'))
diff --git a/lms/lmsweb/translations/en/LC_MESSAGES/messages.po b/lms/lmsweb/translations/en/LC_MESSAGES/messages.po
index 4041d7b1..d224a88e 100644
--- a/lms/lmsweb/translations/en/LC_MESSAGES/messages.po
+++ b/lms/lmsweb/translations/en/LC_MESSAGES/messages.po
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: lmsweb-1.0\n"
"Report-Msgid-Bugs-To: bugs@mesicka.com\n"
-"POT-Creation-Date: 2021-09-11 19:16+0300\n"
+"POT-Creation-Date: 2021-09-12 11:15+0300\n"
"PO-Revision-Date: 2020-09-16 18:29+0300\n"
"Last-Translator: Or Ronai\n"
"Language: en\n"
@@ -45,27 +45,19 @@ msgstr "The automatic checker couldn't run your code."
msgid "אחי, בדקת את הקוד שלך?"
msgstr "Bro, did you check your code?"
-#: lmsweb/views.py:115
-#, fuzzy
-msgid "שם המשתמש או הסיסמה שהוזנו לא תקינים"
-msgstr "Invalid username or password"
-
-#: lmsweb/views.py:120
-msgid "עליך לאשר את המייל"
-msgstr "You have to confirm your registration with the link sent to your email"
-
-#: lmsweb/views.py:144
+#: lmsweb/views.py:134
msgid "ההרשמה בוצעה בהצלחה"
msgstr "Registration successfully"
-#: lmsweb/views.py:168
-msgid "המשתמש שלך אומת בהצלחה, כעת הינך יכול להתחבר למערכת"
-msgstr "Your user has been successfully confirmed, you can now login"
-
-#: lmsweb/views.py:176
+#: lmsweb/views.py:156
msgid "קישור האימות פג תוקף, קישור חדש נשלח אל תיבת המייל שלך"
msgstr "The confirmation link is expired, new link has been sent to your email"
+#: lmsweb/views.py:169
+#, fuzzy
+msgid "המשתמש שלך אומת בהצלחה, כעת אתה יכול להתחבר למערכת"
+msgstr "Your user has been successfully confirmed, you can now login"
+
#: lmsweb/forms/register.py:14
msgid "אימייל לא תקין"
msgstr "Invalid email"
@@ -78,22 +70,22 @@ msgstr "The passwords are not identical"
msgid "מערכת הגשת התרגילים"
msgstr "Exercuse submission system"
-#: lmsweb/tools/validators.py:10
+#: lmsweb/tools/validators.py:13
msgid "שם המשתמש כבר נמצא בשימוש"
msgstr "The username already in use"
-#: lmsweb/tools/validators.py:16
+#: lmsweb/tools/validators.py:21
msgid "האימייל כבר נמצא בשימוש"
msgstr "The email already in use"
-#: models/register.py:31
+#: models/register.py:20
#, python-format
msgid "מייל אימות - %(site_name)s"
msgstr "Confirmation mail - %(site_name)s"
-#: models/register.py:39
-#, python-format
-msgid "שלום %(fullname)s,\n לינק האימות שלך למערכת הוא: %(link)s"
+#: models/register.py:25
+#, fuzzy, python-format
+msgid "שלום %(fullname)s,\nלינק האימות שלך למערכת הוא: %(link)s"
msgstr "Hello %(fullname)s,\n Your confirmation link is: %(link)s"
#: models/solutions.py:50
@@ -111,6 +103,16 @@ msgstr "%(checker)s replied for \"%(subject)s\"."
msgid "הפתרון שלך לתרגיל \"%(subject)s\" נבדק."
msgstr "Your solution for the \"%(subject)s\" exercise has been checked."
+#: models/users.py:25
+#, fuzzy
+msgid "שם המשתמש או הסיסמה שהוזנו לא תקינים"
+msgstr "Invalid username or password"
+
+#: models/users.py:27
+#, fuzzy
+msgid "עליך לאשר את מייל האימות"
+msgstr "You have to confirm your registration with the link sent to your email"
+
#: templates/banned.html:8 templates/login.html:7 templates/signup.html:8
msgid "תמונת הפרופיל של קורס פייתון"
msgstr "Profile picture of the Python Course"
@@ -163,11 +165,11 @@ msgstr "Welcome to the exercise system!"
msgid "הזינו את שם המשתמש והסיסמה שלכם:"
msgstr "Insert your username and password:"
-#: templates/login.html:22 templates/login.html:24
+#: templates/login.html:22 templates/login.html:24 templates/signup.html:16
msgid "שם משתמש"
msgstr "Username"
-#: templates/login.html:28 templates/login.html:30
+#: templates/login.html:28 templates/login.html:30 templates/signup.html:18
msgid "סיסמה"
msgstr "Password"
@@ -224,6 +226,19 @@ msgstr "Registration"
msgid "הזינו אימייל וסיסמה לצורך רישום למערכת:"
msgstr "Insert your email and password for registration:"
+#: templates/signup.html:15
+msgid "כתובת אימייל"
+msgstr "Email Address"
+
+#: templates/signup.html:17
+msgid "שם מלא"
+msgstr "Full Name"
+
+#: templates/signup.html:19
+#, fuzzy
+msgid "אימות סיסמה"
+msgstr "Password Confirmation"
+
#: templates/signup.html:25
msgid "חזרה לדף ההתחברות"
msgstr "Back to login page"
diff --git a/lms/lmsweb/views.py b/lms/lmsweb/views.py
index 2ead24f5..6a89f376 100644
--- a/lms/lmsweb/views.py
+++ b/lms/lmsweb/views.py
@@ -34,10 +34,12 @@
from lms.models import (
comments, notes, notifications, share_link, solutions, upload,
)
-from lms.models.errors import FileSizeError, LmsError, UploadError, fail
-from lms.models.register import (
- SERIALIZER, retrieve_salt, send_confirmation_mail,
+from lms.models.errors import (
+ FileSizeError, ForbiddenPermission, LmsError,
+ UnauthorizedError, UploadError, fail,
)
+from lms.models.register import SERIALIZER, send_confirmation_mail
+from lms.models.users import auth, retrieve_salt
from lms.utils.consts import RTL_LANGUAGES
from lms.utils.files import (
get_language_name_by_extension, get_mime_type_by_extention,
@@ -91,7 +93,6 @@ def ratelimit_handler(e):
deduct_when=lambda response: response.status_code != 200,
)
def login(login_message: Optional[str] = None):
-
if current_user.is_authenticated:
return get_next_url(request.args.get('next'))
@@ -99,26 +100,18 @@ def login(login_message: Optional[str] = None):
password = request.form.get('password')
next_page = request.form.get('next')
login_message = request.args.get('login_message')
- user = User.get_or_none(username=username)
if request.method == 'POST':
- if (
- user is not None and user.is_password_valid(password)
- and not user.role.is_unverified
- ):
+ try:
+ user = auth(username, password)
+ except (ForbiddenPermission, UnauthorizedError) as e:
+ error_message, _ = e.args
+ error_details = {'next': next_page, 'login_message': error_message}
+ return redirect(url_for('login', **error_details))
+ else:
login_user(user)
return get_next_url(next_page)
- elif user is None or not user.is_password_valid(password):
- login_message = _('שם המשתמש או הסיסמה שהוזנו לא תקינים')
- error_details = {'next': next_page, 'login_message': login_message}
- return redirect(url_for('login', **error_details))
-
- elif user.role.is_unverified:
- login_message = _('עליך לאשר את המייל')
- error_details = {'next': next_page, 'login_message': login_message}
- return redirect(url_for('login', **error_details))
-
return render_template('login.html', login_message=login_message)
@@ -128,16 +121,15 @@ def signup():
if not form.validate_on_submit():
return render_template('signup.html', form=form)
- user = User.get_or_create(**{
+ user = User.create(**{
User.mail_address.name: form.email.data,
User.username.name: form.username.data,
- }, defaults={
User.fullname.name: form.fullname.data,
User.role.name: Role.get_unverified_role(),
User.password.name: form.password.data,
User.api_key.name: User.random_password(),
})
- send_confirmation_mail(user[0])
+ send_confirmation_mail(user)
return redirect(url_for(
'login', login_message=_('ההרשמה בוצעה בהצלחה'),
))
@@ -145,29 +137,17 @@ def signup():
@webapp.route('/confirm-email//')
def confirm_email(user_id: int, token: str):
- try:
- user = User.get_or_none(User.id == user_id)
- if user is None:
- return fail(404, f'No such user with id {user_id}.')
+ user = User.get_or_none(User.id == user_id)
+ if user is None:
+ return fail(404, f'No such user with id {user_id}.')
- if not user.role.is_unverified:
- return fail(
- 403, f'User has been already confirmed {user.username}',
- )
+ if not user.role.is_unverified:
+ return fail(403, f'User has been already confirmed {user.username}')
+ try:
SERIALIZER.loads(
token, salt=retrieve_salt(user), max_age=CONFIRMATION_TIME,
)
- update = User.update(
- role=Role.get_student_role(),
- ).where(User.username == user.username)
- update.execute()
-
- return redirect(url_for(
- 'login', login_message=(
- _('המשתמש שלך אומת בהצלחה, כעת הינך יכול להתחבר למערכת'),
- ),
- ))
except SignatureExpired:
send_confirmation_mail(user)
@@ -179,6 +159,17 @@ def confirm_email(user_id: int, token: str):
except BadSignature:
return fail(404, 'No such signature')
+ else:
+ update = User.update(
+ role=Role.get_student_role(),
+ ).where(User.username == user.username)
+ update.execute()
+ return redirect(url_for(
+ 'login', login_message=(
+ _('המשתמש שלך אומת בהצלחה, כעת אתה יכול להתחבר למערכת'),
+ ),
+ ))
+
@webapp.route('/logout')
@login_required
diff --git a/lms/models/errors.py b/lms/models/errors.py
index ef13bc6e..1a2ec15d 100644
--- a/lms/models/errors.py
+++ b/lms/models/errors.py
@@ -13,10 +13,6 @@ class BadUploadFile(LmsError):
pass
-class EmptyPasswordError(LmsError):
- pass
-
-
class FileSizeError(LmsError):
pass
@@ -33,6 +29,10 @@ class NotValidRequest(LmsError): # Error 400
pass
+class UnauthorizedError(LmsError): # Error 401
+ pass
+
+
class ForbiddenPermission(LmsError): # Error 403
pass
diff --git a/lms/models/register.py b/lms/models/register.py
index 3eee1a39..f19937cb 100644
--- a/lms/models/register.py
+++ b/lms/models/register.py
@@ -1,5 +1,3 @@
-import re
-
from flask import url_for
from flask_babel import gettext as _ # type: ignore
from flask_mail import Message # type: ignore
@@ -7,21 +5,12 @@
from lms.lmsdb.models import User
from lms.lmsweb import config, webapp, webmail
-from lms.models.errors import EmptyPasswordError, UnhashedPasswordError
+from lms.models.users import retrieve_salt
SERIALIZER = URLSafeTimedSerializer(config.SECRET_KEY)
-def retrieve_salt(user: User) -> str:
- try:
- re.findall(r'\$(.*)\$', user.password)[0]
- except IndexError:
- if user.password.name:
- raise UnhashedPasswordError
- raise EmptyPasswordError
-
-
def generate_confirmation_token(user: User) -> str:
return SERIALIZER.dumps(user.mail_address, salt=retrieve_salt(user))
@@ -29,10 +18,7 @@ def generate_confirmation_token(user: User) -> str:
def send_confirmation_mail(user: User) -> None:
token = generate_confirmation_token(user)
subject = _('מייל אימות - %(site_name)s', site_name=config.SITE_NAME)
- msg = Message(
- subject, sender=f'lms@{config.MAILGUN_DOMAIN}',
- recipients=[user.mail_address],
- )
+ msg = Message(subject, recipients=[user.mail_address])
link = url_for(
'confirm_email', user_id=user.id, token=token, _external=True,
)
diff --git a/lms/models/users.py b/lms/models/users.py
new file mode 100644
index 00000000..9a3df17e
--- /dev/null
+++ b/lms/models/users.py
@@ -0,0 +1,28 @@
+import re
+
+from flask_babel import gettext as _ # type: ignore
+
+from lms.lmsdb.models import User
+from lms.models.errors import (
+ ForbiddenPermission, UnauthorizedError, UnhashedPasswordError,
+)
+
+
+def retrieve_salt(user: User) -> str:
+ HASHED_PASSWORD = re.compile(
+ r'^pbkdf2.+?\$(?P.+?)\$(?P.+)',
+ )
+ password = HASHED_PASSWORD.match(user.password)
+ try:
+ return password.groupdict().get('salt')
+ except AttributeError: # should never happen
+ raise UnhashedPasswordError
+
+
+def auth(username: str, password: str) -> User:
+ user = User.get_or_none(username=username)
+ if user is None or not user.is_password_valid(password):
+ raise UnauthorizedError(_('שם המשתמש או הסיסמה שהוזנו לא תקינים'), 400)
+ elif user.role.is_unverified:
+ raise ForbiddenPermission(_('עליך לאשר את מייל האימות'), 403)
+ return user
diff --git a/lms/templates/signup.html b/lms/templates/signup.html
index 72e5c413..b888068b 100644
--- a/lms/templates/signup.html
+++ b/lms/templates/signup.html
@@ -11,12 +11,12 @@ {{ _('הרשמה') }}
{{ _('ברוכים הבאים למערכת התרגילים!') }}
{{ _('הזינו אימייל וסיסמה לצורך רישום למערכת:') }}
-
+ {% if config.REGISTRATION_OPEN %}
{{ _('הירשם') }}
+ {% endif %}
diff --git a/tests/conftest.py b/tests/conftest.py
index 27f8620c..93caf4b9 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -75,6 +75,11 @@ def disable_mail_sending():
webapp.config['TESTING'] = True
+@pytest.fixture(autouse=True, scope='session')
+def enable_registration():
+ webapp.config['REGISTRATION_OPEN'] = True
+
+
def disable_shareable_solutions():
webapp.config['SHAREABLE_SOLUTIONS'] = False
@@ -87,6 +92,10 @@ def enable_users_comments():
webapp.config['USERS_COMMENTS'] = True
+def disable_registration():
+ webapp.config['REGISTRATION_OPEN'] = False
+
+
def use_limiter(func):
@wraps(func)
def wrapper(*args, **kwargs):
diff --git a/tests/test_registration.py b/tests/test_registration.py
index df1e2e48..f09b4d49 100644
--- a/tests/test_registration.py
+++ b/tests/test_registration.py
@@ -6,6 +6,7 @@
from lms.lmsweb.config import CONFIRMATION_TIME
from lms.lmsdb.models import User
from lms.models.register import generate_confirmation_token
+from tests import conftest
class TestRegistration:
@@ -157,3 +158,21 @@ def test_expired_token(client: FlaskClient):
}, follow_redirects=True)
success_login_response = client.get('/exercises')
assert success_login_response.status_code == 200
+
+ @staticmethod
+ def test_registartion_closed(client: FlaskClient, captured_templates):
+ conftest.disable_registration()
+ client.post('/signup', data={
+ 'email': 'some_user123@mail.com',
+ 'username': 'some_user',
+ 'fullname': 'some_name',
+ 'password': 'some_password',
+ 'confirm': 'some_password',
+ }, follow_redirects=True)
+ user = User.get_or_none(User.username == 'some_user')
+ assert user is None
+
+ response = client.get('/signup')
+ template, _ = captured_templates[-1]
+ assert template.name == 'login.html'
+ assert '/signup' not in response.get_data(as_text=True)
From d34d19d63cc66d27066ef4adda3fe7493c83fece Mon Sep 17 00:00:00 2001
From: Or Ronai
Date: Tue, 14 Sep 2021 18:22:15 +0300
Subject: [PATCH 26/27] Added flask limits and fixed some messages
---
lms/lmsweb/views.py | 8 +++++---
lms/templates/login.html | 2 +-
2 files changed, 6 insertions(+), 4 deletions(-)
diff --git a/lms/lmsweb/views.py b/lms/lmsweb/views.py
index a80e687c..0d7d2655 100644
--- a/lms/lmsweb/views.py
+++ b/lms/lmsweb/views.py
@@ -116,6 +116,7 @@ def login(login_message: Optional[str] = None):
@webapp.route('/signup', methods=['GET', 'POST'])
+@limiter.limit(f'{LIMITS_PER_MINUTE}/minute;{LIMITS_PER_HOUR}/hour')
def signup():
if not webapp.config.get('REGISTRATION_OPEN', False):
return redirect(url_for(
@@ -141,13 +142,14 @@ def signup():
@webapp.route('/confirm-email//')
+@limiter.limit(f'{LIMITS_PER_MINUTE}/minute;{LIMITS_PER_HOUR}/hour')
def confirm_email(user_id: int, token: str):
user = User.get_or_none(User.id == user_id)
if user is None:
- return fail(404, f'No such user with id {user_id}.')
+ return fail(404, f'The authentication code is invalid.')
if not user.role.is_unverified:
- return fail(403, f'User has been already confirmed {user.username}')
+ return fail(403, f'User has been already confirmed.')
try:
SERIALIZER.loads(
@@ -162,7 +164,7 @@ def confirm_email(user_id: int, token: str):
),
))
except BadSignature:
- return fail(404, 'No such signature')
+ return fail(404, 'The authentication code is invalid.')
else:
update = User.update(
diff --git a/lms/templates/login.html b/lms/templates/login.html
index 789d1723..b57f9238 100644
--- a/lms/templates/login.html
+++ b/lms/templates/login.html
@@ -35,7 +35,7 @@ {{ _('התחברות') }}
{% if config.REGISTRATION_OPEN %}
-
+
{{ _('הירשם') }}
{% endif %}
From f44f1a29dab7a7e6fbf47fde61712350a46b2974 Mon Sep 17 00:00:00 2001
From: Or Ronai
Date: Tue, 14 Sep 2021 18:23:09 +0300
Subject: [PATCH 27/27] Removed formats from strings
---
lms/lmsweb/views.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/lms/lmsweb/views.py b/lms/lmsweb/views.py
index 0d7d2655..eecc397d 100644
--- a/lms/lmsweb/views.py
+++ b/lms/lmsweb/views.py
@@ -146,10 +146,10 @@ def signup():
def confirm_email(user_id: int, token: str):
user = User.get_or_none(User.id == user_id)
if user is None:
- return fail(404, f'The authentication code is invalid.')
+ return fail(404, 'The authentication code is invalid.')
if not user.role.is_unverified:
- return fail(403, f'User has been already confirmed.')
+ return fail(403, 'User has been already confirmed.')
try:
SERIALIZER.loads(