Skip to content

Commit

Permalink
Merge pull request #550 from OpenUpSA/feature/scheduled-committee-mee…
Browse files Browse the repository at this point in the history
…ting-times

Feature/scheduled committee meeting times
  • Loading branch information
paulmwatson authored Jul 15, 2024
2 parents 4647727 + 266ee2f commit 4f93c56
Show file tree
Hide file tree
Showing 10 changed files with 126 additions and 57 deletions.
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM python:3.7.17-slim-bullseye
FROM python:3.8-slim-bullseye

ENV PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1 \
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile-dev
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM python:3.7
FROM python:3.8
ENV PYTHONUNBUFFERED 1
RUN mkdir /app
COPY requirements.txt /app/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""Add scheduled_start_time and actual_end_time to CommitteeMeeting
Revision ID: 33e499db410
Revises: e02ab1542a
Create Date: 2024-07-15 09:38:43.690415
"""

# revision identifiers, used by Alembic.
revision = '33e499db410'
down_revision = 'e02ab1542a'

from alembic import op
import sqlalchemy as sa


def upgrade():
### commands auto generated by Alembic - please adjust! ###
op.add_column('event', sa.Column('scheduled_end_time', sa.Time(timezone=True), nullable=True))
op.add_column('event', sa.Column('scheduled_start_time', sa.Time(timezone=True), nullable=True))
op.alter_column('user', 'fs_uniquifier',
existing_type=sa.VARCHAR(length=64),
nullable=True,
existing_server_default=sa.text("''::character varying"))
### end Alembic commands ###


def downgrade():
### commands auto generated by Alembic - please adjust! ###
op.alter_column('user', 'fs_uniquifier',
existing_type=sa.VARCHAR(length=64),
nullable=False,
existing_server_default=sa.text("''::character varying"))
op.drop_column('event', 'scheduled_start_time')
op.drop_column('event', 'scheduled_end_time')
### end Alembic commands ###
86 changes: 57 additions & 29 deletions pmg/admin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@
from sqlalchemy import func
from sqlalchemy.sql.expression import or_, and_
from sqlalchemy import exc
from jinja2 import Markup
from markupsafe import Markup
import humanize
import psycopg2
import pytz
import flask_wtf

from pmg import app, db
Expand All @@ -42,7 +42,7 @@

logger = logging.getLogger(__name__)

SAST = psycopg2.tz.FixedOffsetTimezone(offset=120, name=None)
SAST = pytz.timezone("Africa/Johannesburg")


def strip_filter(value):
Expand All @@ -54,7 +54,7 @@ def strip_filter(value):
# Our base form extends flask_wtf.Form to get CSRF support,
# and adds the _obj property required by Flask Admin
class BaseForm(flask_wtf.Form):
def __init__(self, formdata=None, obj=None, prefix=u"", **kwargs):
def __init__(self, formdata=None, obj=None, prefix="", **kwargs):
self._obj = obj
super(BaseForm, self).__init__(
formdata=formdata, obj=obj, prefix=prefix, **kwargs
Expand Down Expand Up @@ -156,9 +156,9 @@ def xlsx(self, users, filename):
builder = XLSXBuilder()
xlsx = builder.from_orgs(users)
resp = make_response(xlsx)
resp.headers[
"Content-Type"
] = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
resp.headers["Content-Type"] = (
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
)
resp.headers["Content-Disposition"] = "attachment;filename=" + filename
return resp

Expand Down Expand Up @@ -193,7 +193,7 @@ def frontend_url(self, model):
return None

def alert_url(self, model):
""" If we support sending an email alert about this model, what's the URL? """
"""If we support sending an email alert about this model, what's the URL?"""
if model.id and hasattr(model, "alert_template"):
template = model.alert_template
if template:
Expand All @@ -210,8 +210,7 @@ def _validate_form_instance(self, *args, **kwargs):
pass

def get_export_columns(self):
""" Export all columns by default.
"""
"""Export all columns by default."""
return self.get_column_names(
only_columns=self.scaffold_list_columns(),
excluded_columns=self.column_export_exclude_list,
Expand Down Expand Up @@ -321,7 +320,9 @@ class UserView(MyModelView):
"query_factory": Committee.premium_for_select,
"widget": widgets.CheckboxSelectWidget(multiple=True),
},
"confirmed_at": {"widget": wtforms_widgets.TextInput(),},
"confirmed_at": {
"widget": wtforms_widgets.TextInput(),
},
}
form_widget_args = {
"confirmed_at": {"readonly": True},
Expand Down Expand Up @@ -451,7 +452,9 @@ class CommitteeView(MyModelView):
)
column_default_sort = (Committee.name, False)
column_searchable_list = ("name",)
column_formatters = dict(memberships=macro("render_membership_count"),)
column_formatters = dict(
memberships=macro("render_membership_count"),
)
form_columns = (
"name",
"ad_hoc",
Expand Down Expand Up @@ -494,7 +497,7 @@ def delete_view(self):


class ViewWithFiles:
""" Mixin to pre-fill inline file forms. """
"""Mixin to pre-fill inline file forms."""

form_args = {
"files": {"widget": widgets.InlineFileWidget()},
Expand All @@ -507,7 +510,7 @@ def on_form_prefill(self, form, id):


class InlineFile(InlineFormAdmin):
""" Inline file admin for all views that allow file attachments.
"""Inline file admin for all views that allow file attachments.
It allows the user to choose an existing file to link as
an attachment, or upload a new one. It also allows the user
to edit the title of an already-attached file.
Expand All @@ -520,7 +523,12 @@ class InlineFile(InlineFormAdmin):
column_labels = {
"file": "Existing file",
}
form_ajax_refs = {"file": {"fields": ("title", "file_path"), "page_size": 10,}}
form_ajax_refs = {
"file": {
"fields": ("title", "file_path"),
"page_size": 10,
}
}

def postprocess_form(self, form_class):
# add a field for handling the file upload
Expand Down Expand Up @@ -591,17 +599,17 @@ def format(self, model):
return None

if model.house:
model_unicode = u"%s (%s)" % (model.name, model.house.name)
model_unicode = "%s (%s)" % (model.name, model.house.name)
else:
model_unicode = u"%s" % model.name
model_unicode = "%s" % model.name
return (getattr(model, self.pk), model_unicode)

def get_list(self, term, offset=0, limit=DEFAULT_PAGE_SIZE):
query = self.session.query(self.model)
# Only show currently active members
query = query.filter(Member.current == True)

filters = (field.ilike(u"%%%s%%" % term) for field in self._cached_fields)
filters = (field.ilike("%%%s%%" % term) for field in self._cached_fields)
query = query.filter(or_(*filters))

if self.order_by:
Expand Down Expand Up @@ -665,7 +673,13 @@ class CommitteeMeetingView(EventView):
"body",
"files",
rules.FieldSet(
["actual_start_time", "actual_end_time", "attendance"],
[
"actual_start_time",
"actual_end_time",
"scheduled_start_time",
"scheduled_end_time",
"attendance",
],
"Member Attendance Record",
),
)
Expand Down Expand Up @@ -845,8 +859,7 @@ def on_model_change(self, form, model, is_created):

@expose("/attendance/")
def attendance(self):
"""
"""
""" """
mem_id = request.args.get("id")
url = "/admin/committeemeetingattendance/?member_id={0}".format(mem_id)
return redirect(url)
Expand Down Expand Up @@ -881,7 +894,10 @@ class CommitteeMeetingAttendanceView(MyModelView):
column_formatters = {
"meeting.title": lambda v, c, m, n: Markup(
"<a href='%s'>%s</a>"
% (url_for("committee_meeting", event_id=m.meeting_id), m.meeting.title,),
% (
url_for("committee_meeting", event_id=m.meeting_id),
m.meeting.title,
),
),
"meeting.date": lambda v, c, m, n: m.meeting.date.date().isoformat(),
}
Expand Down Expand Up @@ -950,7 +966,10 @@ class CommitteeQuestionView(MyModelView):
"answer": {"class": "pmg_ckeditor"},
}
form_ajax_refs = {
"source_file": {"fields": ("title", "file_path"), "page_size": 10,},
"source_file": {
"fields": ("title", "file_path"),
"page_size": 10,
},
"asked_by_member": {"fields": ("name",), "page_size": 25},
}
inline_models = [InlineFile(CommitteeQuestionFile)]
Expand Down Expand Up @@ -1162,7 +1181,7 @@ class InlineBillEventsForm(InlineFormAdmin):
"The NA granted permission",
"The NCOP granted permission",
"Bill lapsed",
"Bill withdrawn"
"Bill withdrawn",
]
form_columns = (
"id",
Expand All @@ -1180,7 +1199,14 @@ class InlineBillEventsForm(InlineFormAdmin):
'<div class="help-event-title-content">When event type is "Bill passed", '
'event title must be one of: <ul>%s</ul>When event type is "Bill updated", '
"event title must be one of: <ul>%s</ul></div></div>"
% ("".join(("<li>%s</li>" % title for title in ALLOWED_BILL_PASSED_TITLES)), "".join(("<li>%s</li>" % title for title in ALLOWED_BILL_UPDATED_TITLES)))
% (
"".join(
("<li>%s</li>" % title for title in ALLOWED_BILL_PASSED_TITLES)
),
"".join(
("<li>%s</li>" % title for title in ALLOWED_BILL_UPDATED_TITLES)
),
)
},
}

Expand Down Expand Up @@ -1223,7 +1249,7 @@ class BillHouseAjaxModelLoader(QueryAjaxModelLoader):
def get_list(self, term, offset=0, limit=DEFAULT_PAGE_SIZE):
query = self.session.query(self.model)

filters = list((field.ilike(u"%%%s%%" % term) for field in self._cached_fields))
filters = list((field.ilike("%%%s%%" % term) for field in self._cached_fields))
query = query.filter(or_(*filters))
query = query.filter(and_(House.sphere == "national"))

Expand Down Expand Up @@ -1293,9 +1319,11 @@ class FileView(MyModelView):
column_default_sort = "file_path"
column_labels = {"file_bytes": "Size"}
column_formatters = {
"file_bytes": lambda v, c, m, n: "-"
if m.file_bytes is None
else Markup("<nobr>%s</nobr>" % humanize.naturalsize(m.file_bytes)),
"file_bytes": lambda v, c, m, n: (
"-"
if m.file_bytes is None
else Markup("<nobr>%s</nobr>" % humanize.naturalsize(m.file_bytes))
),
}

class SizeRule(rules.BaseRule):
Expand Down
4 changes: 2 additions & 2 deletions pmg/admin/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from wtforms import widgets, fields
from wtforms.widgets.core import html_params, HTMLString
from wtforms.compat import text_type
from cgi import escape
import html


class CheckboxSelectWidget(widgets.Select):
Expand All @@ -30,7 +30,7 @@ def render_option(cls, value, label, selected, **kwargs):
options["checked"] = True
return HTMLString(
'<div class="checkbox"><label><input %s> %s</label></div>'
% (html_params(**options), escape(text_type(label)))
% (html_params(**options), html.escape(text_type(label)))
)


Expand Down
2 changes: 2 additions & 0 deletions pmg/models/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,8 @@ class CommitteeMeeting(Event):
__mapper_args__ = {"polymorphic_identity": "committee-meeting"}
actual_start_time = db.Column(db.Time(timezone=True))
actual_end_time = db.Column(db.Time(timezone=True))
scheduled_start_time = db.Column(db.Time(timezone=True))
scheduled_end_time = db.Column(db.Time(timezone=True))
pmg_monitor = db.Column(db.String(255))

attendance = db.relationship(
Expand Down
6 changes: 3 additions & 3 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ jdcal==1.4.1
Jinja2==3.1.4
python_magic==0.4.24
Mako==1.0.2
mammoth==0.3.23
mammoth==1.8.0
MarkupSafe==2.0.0
marshmallow==2.16.3
marshmallow-polyfield==3.1
Expand All @@ -69,7 +69,7 @@ passlib==1.7.1
pathlib==1.0.1
pathtools==0.1.2
pbr==1.8.1
psycopg2==2.7.3.2
psycopg2==2.9.9
pycparser==2.19
pycryptodome==3.19.1
pyelasticsearch==0.7.1
Expand Down Expand Up @@ -105,4 +105,4 @@ xlrd==1.0.0
XlsxWriter==0.6.6
xlwt==1.3.0
redis==3.5.3
-e git+https://github.com/Code4SA/za-parliament-scrapers.git@ba77a50d4fc8e83a158328fdea581e143c7631ab#egg=za_parliament_scrapers
-e git+https://github.com/Code4SA/za-parliament-scrapers.git@1fe8b9612f44f55485ff0a3b98c418dfdbc71694#egg=za_parliament_scrapers
18 changes: 9 additions & 9 deletions tests/views/test_admin_bill_page.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
HouseData,
BillTypeData,
)
from flask import escape
import html


class TestAdminBillPage(PMGLiveServerTestCase):
Expand Down Expand Up @@ -74,16 +74,14 @@ def test_admin_create_bill_event_titles_help_section(self):
url = "/admin/bill/new"
response = self.make_request(url, self.user, follow_redirects=True)

self.assertIn(escape("Help?"), self.html)
self.assertIn(html.escape("Help?"), self.html)
self.assertIn(
escape('When event type is "Bill passed", event title must be one of'),
"When event type is &#34;Bill passed&#34;, event title must be one of",
self.html,
)
self.assertIn(
escape(
"Bill passed by the National Assembly and transmitted to the "
"NCOP for concurrence"
),
"Bill passed by the National Assembly and transmitted to the "
"NCOP for concurrence",
self.html,
)

Expand All @@ -96,7 +94,9 @@ def test_admin_action_bill(self):
data = {
"url": "/admin/bill/",
"action": "delete",
"rowid": [str(self.fx.BillData.food.id),],
"rowid": [
str(self.fx.BillData.food.id),
],
}
response = self.make_request(url, self.user, data=data, method="POST")
after_count = len(Bill.query.all())
Expand All @@ -105,7 +105,7 @@ def test_admin_action_bill(self):

def test_admin_delete_bill(self):
"""
Delete a bill on the admin interface
Delete a bill on the admin interface
(/admin/bill/delete/)
"""
before_count = len(Bill.query.all())
Expand Down
Loading

0 comments on commit 4f93c56

Please sign in to comment.