Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Register, admin, mod views #8

Open
wants to merge 62 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
4700204
local-configs
willyao99 Oct 4, 2023
daa6557
fix broken pwd validation
willyao99 Oct 4, 2023
ccedde0
register step 2 template
willyao99 Oct 12, 2023
fa5399d
registration-wip
willyao99 Oct 17, 2023
5685bdd
fix-register-templates
willyao99 Oct 17, 2023
860c929
reg-flow-operational
willyao99 Oct 17, 2023
a8b505a
reg-redirect
willyao99 Oct 17, 2023
d362b21
tapir navbar wip
willyao99 Oct 18, 2023
27bf236
tapir styles and tapir landing wip
willyao99 Oct 18, 2023
da927e8
approved blocked list tooltip et al
willyao99 Dec 6, 2023
81ce5de
user-profile-etc
willyao99 Jan 12, 2024
25250d2
mod-model-fix
willyao99 Jan 24, 2024
d9834ed
moderators-table
willyao99 Jan 24, 2024
4b02e93
clean
willyao99 Jan 24, 2024
1eb62b9
audit log and mods features
willyao99 Feb 6, 2024
8c1baad
endorsement controller
willyao99 Feb 6, 2024
a33d0a1
endorsement wip
willyao99 Feb 9, 2024
f50b043
email-template-mgmt-wip
willyao99 Feb 21, 2024
594ee0f
search route and more email temps
willyao99 Feb 22, 2024
69d060e
search wip
willyao99 Feb 23, 2024
8206087
wip profile
willyao99 Mar 4, 2024
1e639a6
paper detail
willyao99 Mar 6, 2024
9b45552
profile updates
willyao99 Mar 7, 2024
19d1f89
suspects join fix
willyao99 Mar 7, 2024
d4e0096
htmx requests
willyao99 Mar 19, 2024
71705cb
emails-wip
willyao99 Apr 8, 2024
683a73e
tapir sessions wireframing
willyao99 Dec 20, 2023
97b0dce
end html wireframe wip
willyao99 Apr 20, 2024
dd88072
approved and blocked list wip
willyao99 Dec 4, 2023
773ebeb
email templ index link
willyao99 Apr 29, 2024
076c11f
susp link fix
willyao99 May 2, 2024
75d9fc8
index page links
willyao99 May 1, 2024
62fe2e3
flag placeholders wip
willyao99 May 9, 2024
67ce1f0
search function char
willyao99 May 4, 2024
9796891
flask gitignore
willyao99 May 13, 2024
985b1ea
endorsement controllers
willyao99 Apr 15, 2024
af16749
tapir interactive features merge
willyao99 May 13, 2024
03eacdb
endorsement listing feature fix
willyao99 May 13, 2024
2c6afab
landing links
willyao99 May 13, 2024
1d02a7e
redirect links
willyao99 May 13, 2024
64bcb4e
confirm pages wip
willyao99 May 14, 2024
954e6f8
confirm screen wip
willyao99 May 16, 2024
773b75f
Update README.md
willyao99 Jun 12, 2024
7e46f30
Update README.md
willyao99 Jun 12, 2024
1021232
Update README.md
willyao99 Jun 12, 2024
d1db1f6
Update README.md
willyao99 Jun 12, 2024
0f75aed
email temp misc
willyao99 Jun 12, 2024
410a37c
Webapp runs with new base including auth, no arxiv_db, and no flask_s…
mnazzaro Jun 24, 2024
86fc58a
Refactor config, and now pages are actually working
mnazzaro Jun 24, 2024
8ffcff5
A few tests still failing due to db weirdness
mnazzaro Jun 26, 2024
a95e5f3
Just weird domain thing remaining
mnazzaro Jun 27, 2024
d98651b
Switch domain to arxiv.org from .arxiv.org in Domain cookie
mnazzaro Jun 27, 2024
a7b30be
Leaving off for today
mnazzaro Jul 1, 2024
fead199
In cloud run with all routes admin scoped
mnazzaro Jul 3, 2024
bffba2c
One still failing
mnazzaro Jul 8, 2024
a9d6367
Fix @anonymous to add cookies manually
mnazzaro Jul 8, 2024
5dfbdd8
Add some creature comfort for get things going.
ntai-arxiv Jul 8, 2024
4a0a1f0
Add some creature comfort for get things going.
ntai-arxiv Jul 8, 2024
f62b553
Better (and json) logging.
ntai-arxiv Jul 12, 2024
66b05ab
just a doc update
ntai-arxiv Jul 15, 2024
9bf4f26
merge doc
ntai-arxiv Jul 17, 2024
9ff5344
The test password should be string, not bytes.
ntai-arxiv Jul 22, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions admin_webapp/app.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Provides application for development purposes."""
from .factory import create_web_app
import arxiv_db

app = create_web_app()
# with app.app_context():
Expand Down
8 changes: 5 additions & 3 deletions admin_webapp/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@



WTF_CSRF_ENABLED = os.environ.get('WTF_CSRF_ENABLED', True)
WTF_CSRF_ENABLED = os.environ.get('WTF_CSRF_ENABLED', False)
"""Enable CSRF.

Do not disable in production."""
Expand All @@ -119,8 +119,10 @@
FLASK_DEBUG=True
DEBUG=True
if not SQLALCHEMY_DATABASE_URI:
SQLALCHEMY_DATABASE_URI = 'sqlite:///../locahost_dev.db'
CLASSIC_DATABASE_URI = SQLALCHEMY_DATABASE_URI
# SQLALCHEMY_DATABASE_URI = 'sqlite:///../locahost_dev.db'
SQLALCHEMY_DATABASE_URI='mysql+mysqldb://root:root@localhost:3306/arXiv'

# CLASSIC_DATABASE_URI = SQLALCHEMY_DATABASE_URI

DEFAULT_LOGIN_REDIRECT_URL='/protected'
# Need to use this funny name where we have a DNS entry to 127.0.0.1
Expand Down
45 changes: 45 additions & 0 deletions admin_webapp/controllers/endorsement.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"""arXiv endorsements controllers."""
from collections import defaultdict
from datetime import datetime, timedelta
import logging
from admin_webapp.routes import endorsement

from flask import Blueprint, render_template, request, \
make_response, current_app, Response, abort

from flask_sqlalchemy import Pagination

from sqlalchemy import select, func, case, text, insert, update, desc
from sqlalchemy.dialects.mysql import JSON
from sqlalchemy.orm import joinedload, selectinload, aliased
from arxiv.base import logging

from arxiv_auth.auth.decorators import scoped

from arxiv_db.models import Endorsements, EndorsementsAudit, TapirUsers
from arxiv_db.models.associative_tables import t_arXiv_paper_owners

from admin_webapp.extensions import get_csrf, get_db
from admin_webapp.admin_log import audit_admin
logger = logging.getLogger(__file__)

# blueprint = Blueprint('ownership', __name__, url_prefix='/ownership')
"""
All endorsements listing
"""
def endorsement_listing(per_page:int, page: int) -> dict:
session = get_db(current_app).session
report_stmt = (select(Endorsements)
# TODO: do I need a joinedload to prevent N+1 queries
# .options(joinedload(TapirUsers.tapir_nicknames))
.limit(per_page).offset((page -1) * per_page)).join(EndorsementsAudit, isouter=True)

count_stmt = (select(func.count(Endorsements.endorsement_id)))

endorsements = session.scalars(report_stmt)
count = session.execute(count_stmt).scalar_one()
pagination = Pagination(query=None, page=page, per_page=per_page, total=count, items=None)


return dict(pagination=pagination, count=count, endorsements=endorsements)

96 changes: 80 additions & 16 deletions admin_webapp/controllers/registration.py
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that it is. (which is good)

Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
affiliation information, and links to external identities such as GitHub and
ORCID.
"""
from admin_webapp import config

from typing import Dict, Tuple, Any, Optional
from werkzeug.datastructures import MultiDict
Expand All @@ -21,17 +22,19 @@

from wtforms import StringField, PasswordField, SelectField, \
BooleanField, Form, HiddenField
from wtforms.validators import DataRequired, Email, Length, URL, optional, \
from wtforms.validators import DataRequired, Email, Length, URL, optional, EqualTo, \
ValidationError
from flask import url_for, Markup
from flask import session as flask_session
import pycountry

from arxiv import taxonomy
from .util import MultiCheckboxField, OptGroupSelectField

from .. import stateless_captcha

from arxiv_auth import legacy
from arxiv_auth.legacy import sessions as legacy_sessions

from arxiv_auth.legacy import accounts
from arxiv_auth.legacy.exceptions import RegistrationFailed, \
SessionCreationFailed, SessionDeletionFailed
Expand All @@ -44,8 +47,11 @@
def _login_classic(user: domain.User, auth: domain.Authorizations,
ip: Optional[str]) -> Tuple[domain.Session, str]:
try:
c_session = legacy.create(auth, ip, ip, user=user)
c_cookie = legacy.generate_cookie(c_session)
# c_session = legacy.create(auth, ip, ip, user=user)
# c_cookie = legacy.generate_cookie(c_session)
# no tracking cookie used
c_session = legacy_sessions.create(auth, ip, ip, '', user=user)
c_cookie = legacy_sessions.generate_cookie(c_session)
logger.debug('Created classic session: %s', c_session.session_id)
except SessionCreationFailed as ee:
logger.debug('Could not create classic session: %s', ee)
Expand Down Expand Up @@ -87,7 +93,7 @@ def register(method: str, params: MultiDict, captcha_secret: str, ip: str,
form.configure_captcha(captcha_secret, ip)
data = {'form': form, 'next_page': next_page}
elif method == 'POST':
logger.debug('Registration form submitted')
logger.debug('Registration form advancing to step 2')
form = RegistrationForm(params, next_page=next_page)
data = {'form': form, 'next_page': next_page}
form.configure_captcha(captcha_secret, ip)
Expand All @@ -97,9 +103,62 @@ def register(method: str, params: MultiDict, captcha_secret: str, ip: str,
return data, status.HTTP_400_BAD_REQUEST, {}

logger.debug('Registration form is valid')
password = form.password.data
# password = form.password.data

# Perform the actual registration.

# user, auth = accounts.register(form.to_domain(), password, ip, ip)

# try:
# user, auth = accounts.register(form.to_domain(), password, ip, ip)
# except RegistrationFailed as e:
# msg = 'Registration failed'
# raise InternalServerError(msg) from e # type: ignore

# Log the user in.
# session, cookie = _login(user, auth, ip)
# c_session, c_cookie = _login_classic(user, auth, ip)
# data.update({
# 'cookies': {
# 'session_cookie': (cookie, session.expires),
# 'classic_cookie': (c_cookie, c_session.expires)
# },
# 'user_id': user.user_id
# })


# print(session)

return data, status.HTTP_303_SEE_OTHER, {'Location': next_page}
return data, status.HTTP_200_OK, {}

def register2(method: str, params: MultiDict, ip: str,
next_page: str) -> ResponseData:
"""Handle requests for the registration view step 2."""
data: Dict[str, Any]

print("session data", flask_session)
if method == 'GET':
form = ProfileForm(params)
data = {'form': form, 'next_page': next_page}

elif method == 'POST':
logger.debug('Registration form submitted')
form = ProfileForm(params, next_page=next_page)
data = {'form': form, 'next_page': next_page}
# form.configure_captcha(captcha_secret, ip)

if not form.validate():
logger.debug('Registration form not valid')
return data, status.HTTP_400_BAD_REQUEST, {}

logger.debug('Registration form is valid')
password = flask_session['password']

# Perform the actual registration.
print(password, form.forename.data)
# user, auth = accounts.register(form.to_domain(), password, ip, ip)

try:
user, auth = accounts.register(form.to_domain(), password, ip, ip)
except RegistrationFailed as e:
Expand All @@ -116,10 +175,11 @@ def register(method: str, params: MultiDict, captcha_secret: str, ip: str,
},
'user_id': user.user_id
})
return data, status.HTTP_303_SEE_OTHER, {'Location': next_page}
return data, status.HTTP_200_OK, {}

# next_page = next_page if good_next_page(next_page) else config.DEFAULT_LOGIN_REDIRECT_URL
return data, status.HTTP_303_SEE_OTHER, {'Location': next_page}

return data, status.HTTP_200_OK, {}


def edit_profile(method: str, user_id: str, session: domain.Session,
Expand Down Expand Up @@ -232,10 +292,14 @@ def from_domain(cls, user: domain.User) -> 'ProfileForm':

def to_domain(self) -> domain.User:
"""Generate a :class:`.User` from this form's data."""
print(self.default_category.data.split('.'))
print(domain.Category('test'))
print(self.default_category.data)
return domain.User(
user_id=self.user_id.data if self.user_id.data else None,
username=self.username.data,
email=self.email.data,
# use flask session data for step 1 fields
username=flask_session['username'],
email=flask_session['email'],
name=domain.UserFullName(
forename=self.forename.data,
surname=self.surname.data,
Expand All @@ -247,7 +311,7 @@ def to_domain(self) -> domain.User:
rank=int(self.status.data), # WTF can't handle int values.
submission_groups=self.groups.data,
default_category=domain.Category(
*self.default_category.data.split('.')
self.default_category.data
),
homepage_url=self.url.data,
remember_me=self.remember_me.data
Expand Down Expand Up @@ -276,7 +340,7 @@ class RegistrationForm(Form):

password = PasswordField(
'Password',
validators=[Length(min=8, max=20), DataRequired()],
validators=[Length(min=8, max=20), DataRequired(), EqualTo('password2', message="Passwords must match.")],
description="Please choose a password that is between 8 and 20"
" characters in length. Longer passwords are more secure."
" You may use alphanumeric characters, as well as"
Expand Down Expand Up @@ -341,10 +405,10 @@ def validate_captcha_value(self, field: StringField) -> None:
self.captcha_value.data = '' # Clear the field.
raise ValidationError('Please try again') from e

def validate_password(self) -> None:
"""Verify that the password is the same in both fields."""
if self.password.data != self.password2.data:
raise ValidationError('Passwords must match')
# def validate_password(self) -> None:
# """Verify that the password is the same in both fields."""
# if self.password.data != self.password2.data:
# raise ValidationError('Passwords must match')

@classmethod
def from_domain(cls, user: domain.User) -> 'RegistrationForm':
Expand Down
52 changes: 52 additions & 0 deletions admin_webapp/controllers/search.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
from flask import current_app, Response
from flask_sqlalchemy import Pagination
from admin_webapp.extensions import get_db
from sqlalchemy import select, or_, func
from arxiv_db.models import TapirUsers, TapirNicknames

"""
Basic search logic that does some loose similarity checking:

"""
def general_search(search_string: str, per_page:int, page: int) -> Response:
session = get_db(current_app).session

# check if the string is numeric
if search_string.isdigit():
# Check if unique user exists based on user ID
unique_user_id = session.query(TapirUsers).filter(TapirUsers.user_id==search_string).all()
if len(unique_user_id) == 1:
return dict(count=1, unique_id=search_string)

# Check if unique user exists based on nickname
unique_user_nickname = session.query(TapirNicknames).filter(TapirNicknames.nickname==search_string).all()
if len(unique_user_nickname) == 1:
return dict(count=1, unique_id=unique_user_nickname[0].user_id)

# General search logic
stmt = (select(TapirUsers).join(TapirNicknames)
.filter(
or_(TapirUsers.user_id.like(f'%{search_string}%'),
TapirUsers.first_name.like(f'%{search_string}%'),
TapirUsers.last_name.like(f'%{search_string}%'),
TapirNicknames.nickname.like(f'%{search_string}%')
))
.limit(per_page).offset((page -1) * per_page))

count_stmt = (select(func.count(TapirUsers.user_id)).where(
or_(TapirUsers.user_id.like(f'%{search_string}%'),
TapirUsers.first_name.like(f'%{search_string}%'),
TapirUsers.last_name.like(f'%{search_string}%'),
)))

users = session.scalars(stmt)
count = session.execute(count_stmt).scalar_one()

pagination = Pagination(query=None, page=page, per_page=per_page, total=count, items=None)

return dict(pagination=pagination, count=count, users=users)

def advanced_search(options):
session = get_db(current_app).session

return
29 changes: 29 additions & 0 deletions admin_webapp/controllers/tapir_functions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from flask import current_app, Response
from admin_webapp.extensions import get_db
from sqlalchemy import select, func
from arxiv_db.models import TapirEmailTemplates
import json

"""
Tapir email templates
"""
def manage_email_templates() -> Response:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe a different name here? maybe all_email_templates()

session = get_db(current_app).session

stmt = (select(TapirEmailTemplates))
count_stmt = (select(func.count(TapirEmailTemplates.template_id)))

email_templates = session.scalars(stmt)
count = session.execute(count_stmt).scalar_one()

# return json.dumps(email_templates)
return dict(count=count, email_templates=email_templates)

def email_template(template_id: int) -> Response:
session = get_db(current_app).session

stmt = (select(TapirEmailTemplates).where(TapirEmailTemplates.template_id == template_id))

template = session.scalar(stmt)

return dict(template=template)
Loading