Skip to content

Commit

Permalink
Merge pull request #4354 from magfest/fix-magdev1210
Browse files Browse the repository at this point in the history
Add expiration date for watchlist entries
  • Loading branch information
kitsuta committed May 6, 2024
2 parents 4eb4867 + ef41294 commit 1bb51dc
Show file tree
Hide file tree
Showing 13 changed files with 298 additions and 74 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"""Add expiration date to WatchList entries
Revision ID: 7c43e4352bb0
Revises: 2288360ee15d
Create Date: 2024-05-03 19:56:25.359068
"""


# revision identifiers, used by Alembic.
revision = '7c43e4352bb0'
down_revision = '2288360ee15d'
branch_labels = None
depends_on = None

from alembic import op
import sqlalchemy as sa



try:
is_sqlite = op.get_context().dialect.name == 'sqlite'
except Exception:
is_sqlite = False

if is_sqlite:
op.get_context().connection.execute('PRAGMA foreign_keys=ON;')
utcnow_server_default = "(datetime('now', 'utc'))"
else:
utcnow_server_default = "timezone('utc', current_timestamp)"

def sqlite_column_reflect_listener(inspector, table, column_info):
"""Adds parenthesis around SQLite datetime defaults for utcnow."""
if column_info['default'] == "datetime('now', 'utc')":
column_info['default'] = utcnow_server_default

sqlite_reflect_kwargs = {
'listeners': [('column_reflect', sqlite_column_reflect_listener)]
}

# ===========================================================================
# HOWTO: Handle alter statements in SQLite
#
# def upgrade():
# if is_sqlite:
# with op.batch_alter_table('table_name', reflect_kwargs=sqlite_reflect_kwargs) as batch_op:
# batch_op.alter_column('column_name', type_=sa.Unicode(), server_default='', nullable=False)
# else:
# op.alter_column('table_name', 'column_name', type_=sa.Unicode(), server_default='', nullable=False)
#
# ===========================================================================


def upgrade():
op.add_column('watch_list', sa.Column('expiration', sa.Date(), nullable=True))


def downgrade():
op.drop_column('watch_list', 'expiration')
1 change: 1 addition & 0 deletions uber/forms/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -455,3 +455,4 @@ def getlist(self, arg):

from uber.forms.attendee import * # noqa: F401,E402,F403
from uber.forms.group import * # noqa: F401,E402,F403
from uber.forms.security import * # noqa: F401,E402,F403
51 changes: 51 additions & 0 deletions uber/forms/security.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import cherrypy
from datetime import date

from markupsafe import Markup
from wtforms import (BooleanField, DateField, DateTimeField, EmailField,
HiddenField, SelectField, SelectMultipleField, IntegerField,
StringField, TelField, validators, TextAreaField)
from wtforms.validators import ValidationError, StopValidation

from uber.config import c
from uber.forms import (AddressForm, MultiCheckbox, MagForm, SelectAvailableField, SwitchInput, NumberInputGroup,
HiddenBoolField, HiddenIntField, CustomValidation)
from uber.custom_tags import popup_link
from uber.badge_funcs import get_real_badge_type
from uber.models import Attendee, Session, PromoCodeGroup
from uber.model_checks import invalid_phone_number
from uber.utils import get_age_conf_from_birthday


__all__ = ['WatchListEntry']


class WatchListEntry(MagForm):
field_validation, new_or_changed_validation = CustomValidation(), CustomValidation()

first_names = StringField('First Names', render_kw={'placeholder': 'Use commas to separate possible first names.'})
last_name = StringField('Last Name')
email = EmailField('Email Address', validators=[
validators.Optional(),
validators.Length(max=255, message="Email addresses cannot be longer than 255 characters."),
validators.Email(granular_message=True),
],
render_kw={'placeholder': '[email protected]'})
birthdate = DateField('Date of Birth', validators=[validators.Optional()])
reason = TextAreaField('Reason', validators=[
validators.DataRequired("Please enter the reason this attendee is on the watchlist."),
])
action = TextAreaField('Action', validators=[
validators.DataRequired("Please describe what, if anything, an attendee should do before "
"they can check in."),
])
expiration = DateField('Expiration Date', validators=[validators.Optional()])
active = BooleanField('Automatically place matching attendees in the On Hold status.')

@field_validation.birthdate
def birthdate_format(form, field):
# TODO: Make WTForms use this message instead of the generic DateField invalid value message
if field.data and not isinstance(field.data, date):
raise StopValidation('Please use the format YYYY-MM-DD for the date of birth.')
elif field.data and field.data > date.today():
raise ValidationError('Attendees cannot be born in the future.')
3 changes: 2 additions & 1 deletion uber/migration.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ def get_plugin_head_revision(plugin_name):
alembic_config = create_alembic_config()
script = ScriptDirectory.from_config(alembic_config)
branch_labels = script.get_revision(plugin_name).branch_labels
other_plugins = set(plugin_dirs.keys()).difference(branch_labels)
plugin_dirs = [x.name for x in (pathlib.Path(__file__).parents[1] / "plugins").iterdir() if x.is_dir()]
other_plugins = set(plugin_dirs).difference(branch_labels)

def _recursive_get_head_revision(revision_text):
revision = script.get_revision(revision_text)
Expand Down
17 changes: 9 additions & 8 deletions uber/model_checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
on success and a string error message on validation failure.
"""
import re
from datetime import datetime, timedelta
from functools import wraps
from urllib.request import urlopen

Expand Down Expand Up @@ -137,22 +138,22 @@ def with_skipping(attendee):
return with_skipping


WatchList.required = [
('reason', 'Reason'),
('action', 'Action')
]


@validation.WatchList
def include_a_name(entry):
if not entry.first_names and not entry.last_name:
return 'A first or last name is required.'
return ('', 'A first or last name is required.')


@validation.WatchList
def include_other_details(entry):
if not entry.birthdate and not entry.email:
return 'Email or date of birth is required.'
return ('', 'Email or date of birth is required.')


@validation.WatchList
def not_active_after_expiration(entry):
if entry.active and localized_now().date() > entry.expiration:
return ('expiration', 'An entry cannot be active with an expiration date in the past.')


@validation.MPointsForCash
Expand Down
3 changes: 2 additions & 1 deletion uber/models/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,7 @@ class WatchList(MagModel):
birthdate = Column(Date, nullable=True, default=None)
reason = Column(UnicodeText)
action = Column(UnicodeText)
expiration = Column(Date, nullable=True, default=None)
active = Column(Boolean, default=True)
attendees = relationship('Attendee', backref=backref('watch_list', load_on_pending=True))

Expand All @@ -326,7 +327,7 @@ def first_name_list(self):
return [name.strip().lower() for name in self.first_names.split(',')]

@presave_adjustment
def _fix_birthdate(self):
def fix_birthdate(self):
if self.birthdate == '':
self.birthdate = None

Expand Down
2 changes: 1 addition & 1 deletion uber/site_sections/preregistration.py
Original file line number Diff line number Diff line change
Expand Up @@ -844,7 +844,7 @@ def prereg_payment(self, session, message='', **params):
all_errors = validate_model(forms, attendee)
if all_errors:
# Flatten the errors as we don't have fields on this page
' '.join([item for sublist in all_errors.values() for item in sublist])
message = ' '.join([item for sublist in all_errors.values() for item in sublist])
if message:
message += f" Please click 'Edit' next to {attendee.full_name}'s registration to fix any issues."
break
Expand Down
35 changes: 27 additions & 8 deletions uber/site_sections/security_admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,29 @@
from uber.config import c
from uber.decorators import ajax, all_renderable, log_pageview
from uber.errors import HTTPRedirect
from uber.forms import load_forms
from uber.models import WatchList
from uber.utils import check
from uber.utils import validate_model


@all_renderable()
class Root:
@log_pageview
def index(self, session, message='', **params):
watchlist_entries = session.query(WatchList).order_by(WatchList.last_name).all()
for entry in watchlist_entries:
if entry.active:
entry.attendee_guesses = session.guess_watchentry_attendees(entry)
active_entries = session.query(WatchList).filter(WatchList.active == True # noqa: E712
).order_by(WatchList.last_name).all()
for entry in active_entries:
entry.attendees_and_guesses = entry.attendees
for attendee in session.guess_watchentry_attendees(entry):
if attendee not in entry.attendees_and_guesses:
entry.attendees_and_guesses.append(attendee)

inactive_entries = session.query(WatchList).filter(WatchList.active == False # noqa: E712
).order_by(WatchList.last_name).all()

return {
'watchlist_entries': watchlist_entries,
'active_entries': active_entries,
'inactive_entries': inactive_entries,
'message': message
}

Expand All @@ -28,12 +36,22 @@ def watchlist_form(self, session, message='', **params):
last_name=attendee.last_name,
email=attendee.email,
birthdate=attendee.birthdate)
elif params.get('id') not in [None, '', 'None']:
entry = session.watch_list(params.get('id'))
else:
entry = session.watch_list(params, bools=['active'])
entry = WatchList()

forms = load_forms(params, entry, ['WatchListEntry'])
for form in forms.values():
form.populate_obj(entry)

entry.attendee_guesses = session.guess_watchentry_attendees(entry)

if cherrypy.request.method == 'POST':
message = check(entry)
all_errors = validate_model(forms, entry, WatchList(**entry.to_dict()))
if all_errors:
message = ' '.join([item for sublist in all_errors.values() for item in sublist])

changed_attendees = 0

if not message:
Expand Down Expand Up @@ -67,6 +85,7 @@ def watchlist_form(self, session, message='', **params):
raise HTTPRedirect('index?message={}{}', message, changed_message)

return {
'forms': forms,
'entry': entry,
'message': message
}
Expand Down
1 change: 1 addition & 0 deletions uber/tasks/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,5 @@ def run_startup_tasks(*args, **kwargs):
from uber.tasks import panels # noqa: F401, E402
from uber.tasks import redis # noqa: F401, E402
from uber.tasks import registration # noqa: F401, E402
from uber.tasks import security # noqa: F401, E402
from uber.tasks import sms # noqa: F401, E402
22 changes: 22 additions & 0 deletions uber/tasks/security.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from datetime import date, timedelta

from uber.models import WatchList, Session
from uber.tasks import celery


__all__ = ['deactivate_expired_watchlist_entries']


@celery.schedule(timedelta(hour=12))
def deactivate_expired_watchlist_entries():
with Session() as session:
expired_entries = session.query(WatchList).filter(WatchList.active == True, # noqa: E712
WatchList.expiration <= date.today())

expired_count = expired_entries.count()

for entry in expired_entries:
entry.active = False
session.add(entry)

return f"Deactivated {expired_count} expired watchlist entries."
2 changes: 1 addition & 1 deletion uber/templates/registration/attendee_watchlist.html
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
},
callback: function (result) {
if (result) {
updateWatchlist();
formToSubmit.submit();
}
}
});
Expand Down
Loading

0 comments on commit 1bb51dc

Please sign in to comment.