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 %} +
+
+
+ +

{{ _('הרשמה') }}

+

+ {{ _('ברוכים הבאים למערכת התרגילים!') }}
+ {{ _('הזינו אימייל וסיסמה לצורך רישום למערכת:') }} +

+
+ {{ render_field(form.email, cls="form-control form-control-lg", placeholder="Email Address") }} + {{ render_field(form.username, cls="form-control form-control-lg", placeholder="User Name") }} + {{ render_field(form.fullname, cls="form-control form-control-lg", placeholder="Full Name") }} + {{ render_field(form.password, cls="form-control form-control-lg", placeholder="Password") }} + {{ render_field(form.confirm, cls="form-control form-control-lg", placeholder="Password Verification") }} + + + +
+
+ {{ _('חזרה לדף ההתחברות') }} +
+
+
+{% 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 @@

{{ _('הרשמה') }}

{{ _('ברוכים הבאים למערכת התרגילים!') }}
{{ _('הזינו אימייל וסיסמה לצורך רישום למערכת:') }}

-
- {{ render_field(form.email, cls="form-control form-control-lg", placeholder="Email Address") }} - {{ render_field(form.username, cls="form-control form-control-lg", placeholder="User Name") }} - {{ render_field(form.fullname, cls="form-control form-control-lg", placeholder="Full Name") }} - {{ render_field(form.password, cls="form-control form-control-lg", placeholder="Password") }} - {{ render_field(form.confirm, cls="form-control form-control-lg", placeholder="Password Verification") }} + + {{ render_field(form.email, cls="form-control form-control-lg", placeholder=_('כתובת אימייל')) }} + {{ render_field(form.username, cls="form-control form-control-lg", placeholder=_('שם משתמש')) }} + {{ render_field(form.fullname, cls="form-control form-control-lg", placeholder=_('שם מלא')) }} + {{ render_field(form.password, cls="form-control form-control-lg", placeholder=_('סיסמה')) }} + {{ render_field(form.confirm, cls="form-control form-control-lg", placeholder=_('אימות סיסמה')) }} From 19ecb65b8c40b3c51493dd9cb8abbdb84573ed3e Mon Sep 17 00:00:00 2001 From: Or Ronai Date: Sun, 12 Sep 2021 11:37:40 +0300 Subject: [PATCH 22/27] Fixed a test to check bad signature token --- lms/lmsweb/views.py | 1 + tests/test_registration.py | 10 +++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/lms/lmsweb/views.py b/lms/lmsweb/views.py index 6a89f376..64fc5eed 100644 --- a/lms/lmsweb/views.py +++ b/lms/lmsweb/views.py @@ -157,6 +157,7 @@ def confirm_email(user_id: int, token: str): ), )) except BadSignature: + print('here') return fail(404, 'No such signature') else: diff --git a/tests/test_registration.py b/tests/test_registration.py index dd5188c0..df1e2e48 100644 --- a/tests/test_registration.py +++ b/tests/test_registration.py @@ -82,9 +82,17 @@ def test_successful_registration(client: FlaskClient, captured_templates): @staticmethod def test_bad_token_or_id(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') bad_token = "fake-token43@$@" fail_confirm_response = client.get( - f'/confirm-email/1/{bad_token}', follow_redirects=True, + f'/confirm-email/{user.id}/{bad_token}', follow_redirects=True, ) assert fail_confirm_response.status_code == 404 From 71fbb2f26a6577d28546bf4c1ddbfe8a8a77b0e2 Mon Sep 17 00:00:00 2001 From: Or Ronai Date: Sun, 12 Sep 2021 11:38:12 +0300 Subject: [PATCH 23/27] Fixed a test --- lms/lmsweb/views.py | 1 - 1 file changed, 1 deletion(-) diff --git a/lms/lmsweb/views.py b/lms/lmsweb/views.py index 64fc5eed..6a89f376 100644 --- a/lms/lmsweb/views.py +++ b/lms/lmsweb/views.py @@ -157,7 +157,6 @@ def confirm_email(user_id: int, token: str): ), )) except BadSignature: - print('here') return fail(404, 'No such signature') else: From d4bd32a0e5ff035ae437380e6977f83771791ece Mon Sep 17 00:00:00 2001 From: Or Ronai Date: Sun, 12 Sep 2021 12:58:12 +0300 Subject: [PATCH 24/27] Moved out the HASHED_PASSWORD in order to be global variable, and added an error message to the UnhashedPasswordError --- lms/models/users.py | 8 ++++---- tests/conftest.py | 2 +- tests/samples/config.py.example | 1 - 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/lms/models/users.py b/lms/models/users.py index 9a3df17e..bb2720a6 100644 --- a/lms/models/users.py +++ b/lms/models/users.py @@ -8,15 +8,15 @@ ) +HASHED_PASSWORD = re.compile(r'^pbkdf2.+?\$(?P.+?)\$(?P.+)') + + 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 + raise UnhashedPasswordError('Password format is invalid.') def auth(username: str, password: str) -> User: diff --git a/tests/conftest.py b/tests/conftest.py index 7dad6fda..27f8620c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -178,7 +178,7 @@ def admin_user(): ) -@pytest.fixture(autouse=True, scope='session') +@pytest.fixture(autouse=True, scope='function') def captured_templates(): recorded = [] diff --git a/tests/samples/config.py.example b/tests/samples/config.py.example index abdf7fc2..dde2b013 100644 --- a/tests/samples/config.py.example +++ b/tests/samples/config.py.example @@ -14,7 +14,6 @@ FEATURE_FLAG_CHECK_IDENTICAL_CODE_ON = os.getenv( ) - MAIL_WELCOME_MESSAGE = 'welcome-email' USERS_CSV = 'users.csv' ERRORS_CSV = 'errors.csv' From 436fc9fc855f22e2d44245532f70f71cc7229fb5 Mon Sep 17 00:00:00 2001 From: Or Ronai Date: Sun, 12 Sep 2021 15:13:41 +0300 Subject: [PATCH 25/27] Added a configuration of registration open, and a test --- lms/lmsweb/config.py.example | 2 ++ .../translations/en/LC_MESSAGES/messages.po | 14 +++++++++----- lms/lmsweb/views.py | 5 +++++ lms/templates/login.html | 2 ++ tests/conftest.py | 9 +++++++++ tests/test_registration.py | 19 +++++++++++++++++++ 6 files changed, 46 insertions(+), 5 deletions(-) diff --git a/lms/lmsweb/config.py.example b/lms/lmsweb/config.py.example index 58917cf8..d1c500f5 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' +# REGISTRATION CONFIGURATIONS +REGISTRATION_OPEN = True CONFIRMATION_TIME = 3600 # MAIL CONFIGURATION diff --git a/lms/lmsweb/translations/en/LC_MESSAGES/messages.po b/lms/lmsweb/translations/en/LC_MESSAGES/messages.po index d224a88e..0c35ce13 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-12 11:15+0300\n" +"POT-Creation-Date: 2021-09-12 15:10+0300\n" "PO-Revision-Date: 2020-09-16 18:29+0300\n" "Last-Translator: Or Ronai\n" "Language: en\n" @@ -45,15 +45,19 @@ msgstr "The automatic checker couldn't run your code." msgid "אחי, בדקת את הקוד שלך?" msgstr "Bro, did you check your code?" -#: lmsweb/views.py:134 +#: lmsweb/views.py:122 +msgid "לא ניתן להירשם כעת" +msgstr "Can not register now" + +#: lmsweb/views.py:139 msgid "ההרשמה בוצעה בהצלחה" msgstr "Registration successfully" -#: lmsweb/views.py:156 +#: lmsweb/views.py:161 msgid "קישור האימות פג תוקף, קישור חדש נשלח אל תיבת המייל שלך" msgstr "The confirmation link is expired, new link has been sent to your email" -#: lmsweb/views.py:169 +#: lmsweb/views.py:174 #, fuzzy msgid "המשתמש שלך אומת בהצלחה, כעת אתה יכול להתחבר למערכת" msgstr "Your user has been successfully confirmed, you can now login" @@ -177,7 +181,7 @@ msgstr "Password" msgid "התחבר" msgstr "Login" -#: templates/login.html:38 templates/signup.html:22 +#: templates/login.html:39 templates/signup.html:22 msgid "הירשם" msgstr "Register" diff --git a/lms/lmsweb/views.py b/lms/lmsweb/views.py index 6a89f376..a80e687c 100644 --- a/lms/lmsweb/views.py +++ b/lms/lmsweb/views.py @@ -117,6 +117,11 @@ def login(login_message: Optional[str] = None): @webapp.route('/signup', methods=['GET', 'POST']) def signup(): + if not webapp.config.get('REGISTRATION_OPEN', False): + return redirect(url_for( + 'login', login_message=_('לא ניתן להירשם כעת'), + )) + form = RegisterForm() if not form.validate_on_submit(): return render_template('signup.html', form=form) diff --git a/lms/templates/login.html b/lms/templates/login.html index b1dde490..789d1723 100644 --- a/lms/templates/login.html +++ b/lms/templates/login.html @@ -34,8 +34,10 @@

{{ _('התחברות') }}

+ {% 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(