diff --git a/apollo/frontend/templates/frontend/macros/submission_list_filter.html b/apollo/frontend/templates/frontend/macros/submission_list_filter.html
index fd6d8e23d..9eaa0e573 100644
--- a/apollo/frontend/templates/frontend/macros/submission_list_filter.html
+++ b/apollo/frontend/templates/frontend/macros/submission_list_filter.html
@@ -70,6 +70,10 @@
{{ filter_form.participant_role(class_='form-control custom-select') }}
diff --git a/apollo/frontend/templates/frontend/participant_list.html b/apollo/frontend/templates/frontend/participant_list.html
index 0d7beb31b..5ee3868ce 100644
--- a/apollo/frontend/templates/frontend/participant_list.html
+++ b/apollo/frontend/templates/frontend/participant_list.html
@@ -356,10 +356,6 @@
{{ _('Finalize') }}
LocationOptions.placeholder = { id: '-1', text: '{{ _("Location") }}'};
$('select.select2-locations').select2(LocationOptions);
- $('#group.select2').select2({
- theme: 'bootstrap4',
- placeholder: "{{ _('All Groups') }}"
- });
});
diff --git a/apollo/helpers.py b/apollo/helpers.py
index 333835862..3c129a28d 100644
--- a/apollo/helpers.py
+++ b/apollo/helpers.py
@@ -7,6 +7,7 @@
import pkgutil
CSV_MIMETYPES = [
+ "text/plain",
"text/csv",
"application/csv",
"text/x-csv",
diff --git a/apollo/models.py b/apollo/models.py
index cf6375a38..207040ca9 100644
--- a/apollo/models.py
+++ b/apollo/models.py
@@ -7,9 +7,10 @@
LocationTypePath, LocationGroup, locations_groups)
from apollo.messaging.models import Message # noqa
from apollo.participants.models import ( # noqa
- ParticipantSet, ParticipantDataField,
- Participant, ParticipantPartner, ParticipantRole, PhoneContact,
- ContactHistory, Sample, samples_participants)
+ ParticipantGroup, ParticipantGroupType, ParticipantSet,
+ ParticipantDataField, Participant, ParticipantPartner,
+ ParticipantRole, PhoneContact, ContactHistory, Sample,
+ groups_participants, samples_participants)
from apollo.submissions.models import ( # noqa
Submission, SubmissionComment, SubmissionImageAttachment,
SubmissionVersion)
diff --git a/apollo/participants/filters.py b/apollo/participants/filters.py
index cb28b0563..2f139f29d 100644
--- a/apollo/participants/filters.py
+++ b/apollo/participants/filters.py
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
from cgi import escape
+from collections import OrderedDict
from flask_babelex import lazy_gettext as _
from sqlalchemy import func, or_, text, true
@@ -12,9 +13,11 @@
from apollo.core import CharFilter, ChoiceFilter, FilterSet
from apollo.helpers import _make_choices
from apollo.locations.models import Location, LocationPath
+from apollo.wtforms_ext import ExtendedSelectField
-from .models import Participant, ParticipantRole, ParticipantPartner
-from .models import PhoneContact, Sample
+from .models import Participant, ParticipantGroup, ParticipantGroupType
+from .models import ParticipantPartner, ParticipantRole
+from .models import PhoneContact, Sample, groups_participants
class ParticipantIDFilter(CharFilter):
@@ -94,6 +97,45 @@ def queryset_(self, query, value):
return ParticipantRoleFilter
+def make_participant_group_filter(participant_set_id):
+ class ParticipantGroupFilter(ChoiceFilter):
+ field_class = ExtendedSelectField
+
+ def __init__(self, *args, **kwargs):
+ choices = OrderedDict()
+ choices[''] = _('Group')
+ for group_type in services.participant_group_types.find(
+ participant_set_id=participant_set_id
+ ).order_by(
+ ParticipantGroupType.name):
+ for group in services.participant_groups.find(
+ group_type=group_type
+ ).order_by(ParticipantGroup.name):
+ choices.setdefault(group_type.name, []).append(
+ (group.id, group.name)
+ )
+
+ kwargs['choices'] = [(k, choices[k]) for k in choices]
+ kwargs['coerce'] = int
+ super(ParticipantGroupFilter, self).__init__(*args, **kwargs)
+
+ def queryset_(self, query, value):
+ if value:
+ query2 = query.join(groups_participants).join(
+ ParticipantGroup)
+ return query2.filter(
+ Participant.id ==
+ models.groups_participants.c.participant_id, # noqa
+ ParticipantGroup.id ==
+ groups_participants.c.group_id,
+ ParticipantGroup.id == value,
+ )
+
+ return query
+
+ return ParticipantGroupFilter
+
+
def make_participant_partner_filter(participant_set_id):
class ParticipantPartnerFilter(ChoiceFilter):
def __init__(self, *args, **kwargs):
@@ -201,6 +243,7 @@ def participant_filterset(participant_set_id, location_set_id=None):
'name': ParticipantNameFilter(),
'phone': ParticipantPhoneFilter(),
'role': make_participant_role_filter(participant_set_id)(),
+ 'group': make_participant_group_filter(participant_set_id)(),
'partner': make_participant_partner_filter(participant_set_id)()
}
diff --git a/apollo/participants/models.py b/apollo/participants/models.py
index f531a2477..5b1b1d40b 100644
--- a/apollo/participants/models.py
+++ b/apollo/participants/models.py
@@ -1,5 +1,4 @@
# -*- coding: utf-8 -*-
-from itertools import chain
import re
from sqlalchemy import func
@@ -51,6 +50,7 @@ def get_import_fields(self):
'phone': _('Phone'),
'partner': _('Partner'),
'location': _('Location code'),
+ 'group': _('Group'),
'gender': _('Gender'),
'email': _('Email'),
'password': _('Password')
@@ -94,6 +94,20 @@ def get_import_fields(self):
),
)
+groups_participants = db.Table(
+ 'participant_group_participants',
+ db.Column(
+ 'group_id', db.Integer,
+ db.ForeignKey('participant_group.id', ondelete='CASCADE'),
+ nullable=False
+ ),
+ db.Column(
+ 'participant_id', db.Integer,
+ db.ForeignKey('participant.id', ondelete='CASCADE'),
+ nullable=False
+ ),
+)
+
class Sample(BaseModel):
__tablename__ = "sample"
@@ -152,6 +166,56 @@ def __str__(self):
return self.name or ''
+class ParticipantGroupType(BaseModel):
+ __tablename__ = 'participant_group_type'
+
+ id = db.Column(db.Integer, primary_key=True)
+ name = db.Column(db.String)
+ participant_set_id = db.Column(
+ db.Integer,
+ db.ForeignKey('participant_set.id', ondelete='CASCADE'),
+ nullable=False
+ )
+
+ participant_set = db.relationship(
+ 'ParticipantSet', backref=db.backref(
+ 'participant_group_types', cascade='all, delete',
+ )
+ )
+
+ def __str__(self):
+ return self.name or ''
+
+
+class ParticipantGroup(BaseModel):
+ __tablename__ = 'participant_group'
+
+ id = db.Column(db.Integer, primary_key=True)
+ name = db.Column(db.String, nullable=False)
+ group_type_id = db.Column(
+ db.Integer,
+ db.ForeignKey('participant_group_type.id', ondelete='CASCADE'),
+ nullable=False
+ )
+ participant_set_id = db.Column(
+ db.Integer,
+ db.ForeignKey('participant_set.id', ondelete='CASCADE'),
+ nullable=False
+ )
+
+ group_type = db.relationship(
+ 'ParticipantGroupType',
+ backref=db.backref('participant_groups', cascade='all, delete'),
+ )
+ participant_set = db.relationship(
+ 'ParticipantSet',
+ backref=db.backref('participant_groups', cascade='all, delete'),
+ )
+
+ def __str__(self):
+ return self.name or ''
+
+
class ParticipantPartner(BaseModel):
__tablename__ = 'participant_partner'
@@ -227,6 +291,10 @@ class Participant(BaseModel):
backref="participants",
secondary=samples_participants,
)
+ groups = db.relationship(
+ 'ParticipantGroup', secondary=groups_participants,
+ backref='participants',
+ )
def __str__(self):
return self.name or ''
@@ -334,7 +402,8 @@ class PhoneContact(BaseModel):
onupdate=utils.current_timestamp)
verified = db.Column(db.Boolean, default=False)
- participant = db.relationship('Participant', back_populates='phone_contacts')
+ participant = db.relationship(
+ 'Participant', back_populates='phone_contacts')
def touch(self):
self.updated = utils.current_timestamp()
diff --git a/apollo/participants/serializers.py b/apollo/participants/serializers.py
index 8d108f253..206ebd26a 100644
--- a/apollo/participants/serializers.py
+++ b/apollo/participants/serializers.py
@@ -7,8 +7,9 @@
from apollo.dal.serializers import ArchiveSerializer
from apollo.locations.models import Location, LocationSet
from apollo.participants.models import (
- Participant, ParticipantDataField, ParticipantPartner, ParticipantRole,
- ParticipantSet, PhoneContact)
+ Participant, ParticipantDataField, ParticipantGroup,
+ ParticipantGroupType, ParticipantPartner, ParticipantRole,
+ ParticipantSet, PhoneContact, groups_participants)
class ParticipantSerializer(object):
@@ -105,6 +106,63 @@ def serialize_one(self, obj):
}
+class ParticipantGroupTypeSerializer(object):
+ __model__ = ParticipantGroupType
+
+ def deserialize_one(self, data):
+ participant_set_id = ParticipantSet.query.filter_by(
+ uuid=data['participant_set']
+ ).with_entities(ParticipantSet.id).scalar()
+
+ kwargs = data.copy()
+ kwargs.pop('participant_set')
+ kwargs['participant_set_id'] = participant_set_id
+
+ return self.__model__(**kwargs)
+
+ def serialize_one(self, obj):
+ if not isinstance(obj, self.__model__):
+ raise TypeError('Object is not instance of ParticipantGroupType')
+
+ return {
+ 'uuid': obj.uuid.hex,
+ 'name': obj.name,
+ 'participant_set': obj.participant_set.uuid.hex
+ }
+
+
+class ParticipantGroupSerializer(object):
+ __model__ = ParticipantGroup
+
+ def deserialize_one(self, data):
+ participant_set_id = ParticipantSet.query.filter_by(
+ uuid=data['participant_set']
+ ).with_entities(ParticipantSet.id).scalar()
+
+ group_type_id = ParticipantGroupType.query.filter_by(
+ uuid=data['group_type']
+ ).with_entitites(ParticipantGroupType.id).scalar()
+
+ kwargs = data.copy()
+ kwargs.pop('participant_set')
+ kwargs.pop('group_type')
+ kwargs['group_type_id'] = group_type_id
+ kwargs['participant_set_id'] = participant_set_id
+
+ return self.__model__(**kwargs)
+
+ def serialize_one(self, obj):
+ if not isinstance(obj, self.__model__):
+ raise TypeError('Object is not instance of ParticipantGroup')
+
+ return {
+ 'uuid': obj.uuid.hex,
+ 'name': obj.name,
+ 'participant_set': obj.participant_set.uuid.hex,
+ 'group_type': obj.group_type.uuid.hex
+ }
+
+
class ParticipantDataFieldSerializer(object):
__model__ = ParticipantDataField
@@ -195,6 +253,9 @@ def serialize(self, event, zip_file):
self.serialize_partners(participant_set.participant_partners,
zip_file)
self.serialize_roles(participant_set.participant_roles, zip_file)
+ self.serialize_group_types(participant_set.participant_group_types,
+ zip_file)
+ self.serialize_groups(participant_set.participant_groups, zip_file)
self.serialize_participants(participant_set.participants, zip_file)
self.serialize_participant_phones(participant_set, zip_file)
@@ -205,6 +266,35 @@ def serialize_participant_set(self, obj, zip_file):
with zip_file.open('participant_set.ndjson', 'w') as f:
f.write(json.dumps(data).encode('utf-8'))
+ def serialize_group_types(self, group_types, zip_file):
+ serializer = ParticipantGroupTypeSerializer()
+
+ with zip_file.open('group_types.ndjson', 'w') as f:
+ for group_type in group_types:
+ data = serializer.serialize_one(group_type)
+ line = f'{json.dumps(data)}\n'
+ f.write(line.encode('utf-8'))
+
+ def serialize_groups(self, groups, zip_file):
+ serializer = ParticipantGroupSerializer()
+
+ with zip_file.open('groups.ndjson', 'w') as f:
+ for group in groups:
+ data = serializer.serialize_one(group)
+ line = f'{json.dumps(data)}\n'
+ f.write(line.encode('utf-8'))
+
+ def serialize_participant_groups(self, participant_set, zip_file):
+ query = db.session.query(groups_participants).join(
+ Participant).join(ParticipantGroup).with_entities(
+ cast(Participant.uuid, String),
+ cast(ParticipantGroup.uuid, String))
+
+ with zip_file.open('participant-groups.ndjson', 'w') as f:
+ for pair in query:
+ line = f'{json.dumps(pair)}\n'
+ f.write(line.encode('utf-8'))
+
def serialize_extra_fields(self, extra_fields, zip_file):
serializer = ParticipantDataFieldSerializer()
diff --git a/apollo/participants/services.py b/apollo/participants/services.py
index b2b35c75f..cae1ccfeb 100644
--- a/apollo/participants/services.py
+++ b/apollo/participants/services.py
@@ -8,8 +8,8 @@
from apollo import constants
from apollo.dal.service import Service
from apollo.participants.models import (
- ParticipantSet, Participant, ParticipantPartner, ParticipantRole,
- PhoneContact)
+ ParticipantSet, Participant, ParticipantGroup, ParticipantGroupType,
+ ParticipantPartner, ParticipantRole, PhoneContact)
number_regex = re.compile('[^0-9]')
@@ -99,6 +99,14 @@ def export_list(self, query):
output_buffer.close()
+class ParticipantGroupService(Service):
+ __model__ = ParticipantGroup
+
+
+class ParticipantGroupTypeService(Service):
+ __model__ = ParticipantGroupType
+
+
class ParticipantPartnerService(Service):
__model__ = ParticipantPartner
diff --git a/apollo/participants/tasks.py b/apollo/participants/tasks.py
index 5d410be7e..40c6365d0 100644
--- a/apollo/participants/tasks.py
+++ b/apollo/participants/tasks.py
@@ -55,6 +55,17 @@ def create_partner(name, participant_set):
name=name, participant_set_id=participant_set.id)
+def create_group_type(name, participant_set):
+ return services.participant_group_types.create(
+ name=name, participant_set_id=participant_set.id)
+
+
+def create_group(name, group_type, participant_set):
+ return services.participant_groups.create(
+ name=name, group_type_id=group_type.id,
+ participant_set_id=participant_set.id)
+
+
def create_role(name, participant_set):
return services.participant_roles.create(
name=name, participant_set_id=participant_set.id)
@@ -86,6 +97,8 @@ def update_participants(dataframe, header_map, participant_set, task):
password - the participant's password.
phone - a prefix for columns starting with this string that contain
numbers
+ group - a prefix for columns starting with this string that contain
+ participant group names
"""
index = dataframe.index
@@ -128,6 +141,7 @@ def update_participants(dataframe, header_map, participant_set, task):
EMAIL_COL = header_map.get('email')
PASSWORD_COL = header_map.get('password')
phone_columns = header_map.get('phone', [])
+ group_columns = header_map.get('group', [])
sample_columns = header_map.get('sample', [])
full_name_columns = [
header_map.get(col) for col in full_name_column_keys]
@@ -371,6 +385,33 @@ def update_participants(dataframe, header_map, participant_set, task):
number=mobile_num, participant_id=participant.id,
verified=True)
+ groups = []
+ # fix up groups
+ if group_columns:
+ for column in group_columns:
+ if not _is_valid(record[column]):
+ continue
+
+ group_type = services.participant_group_types.find(
+ name=column,
+ participant_set=participant_set
+ ).first()
+
+ if not group_type:
+ group_type = create_group_type(
+ column, participant_set)
+
+ group = services.participant_groups.find(
+ name=record[column],
+ group_type=group_type,
+ participant_set=participant_set).first()
+
+ if not group:
+ group = create_group(
+ record[column], group_type, participant_set)
+
+ groups.append(group)
+
if sample_columns:
for column in sample_columns:
if not _is_valid(record[column]):
@@ -408,6 +449,13 @@ def update_participants(dataframe, header_map, participant_set, task):
services.participants.find(id=participant.id).update(
{'extra_data': extra_data}, synchronize_session=False)
+ if groups:
+ if participant.groups:
+ participant.groups.extend(groups)
+ else:
+ participant.groups = groups
+ participant.save()
+
task.update_task_info(
total_records=total_records,
error_records=error_records,
diff --git a/apollo/services.py b/apollo/services.py
index d0d74de34..f53d7fd73 100644
--- a/apollo/services.py
+++ b/apollo/services.py
@@ -5,8 +5,9 @@
LocationService, LocationSetService, LocationTypeService)
from apollo.messaging.services import MessageService
from apollo.participants.services import (
+ ParticipantService, ParticipantGroupService, ParticipantGroupTypeService,
ParticipantSetService, ParticipantPartnerService, ParticipantRoleService,
- ParticipantService, PhoneContactService)
+ PhoneContactService)
from apollo.submissions.services import (
SubmissionService, SubmissionCommentService, SubmissionVersionService)
from apollo.users.services import UserService, UserUploadService
@@ -21,6 +22,8 @@
participant_partners = ParticipantPartnerService()
participant_roles = ParticipantRoleService()
participant_sets = ParticipantSetService()
+participant_groups = ParticipantGroupService()
+participant_group_types = ParticipantGroupTypeService()
phone_contacts = PhoneContactService()
submissions = SubmissionService()
submission_comments = SubmissionCommentService()
diff --git a/apollo/submissions/filters.py b/apollo/submissions/filters.py
index 81896e82f..7af97c629 100644
--- a/apollo/submissions/filters.py
+++ b/apollo/submissions/filters.py
@@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
+from collections import OrderedDict
from itertools import chain
from operator import itemgetter
@@ -13,7 +14,7 @@
from wtforms.widgets import html_params, HTMLString
from wtforms_alchemy.fields import QuerySelectField
-from apollo import models
+from apollo import models, services
from apollo.core import BooleanFilter, CharFilter, ChoiceFilter, FilterSet
from apollo.helpers import _make_choices
from apollo.settings import TIMEZONE
@@ -21,6 +22,7 @@
from apollo.submissions.qa.query_builder import (
build_expression, generate_qa_query
)
+from apollo.wtforms_ext import ExtendedSelectField
APP_TZ = gettz(TIMEZONE)
@@ -150,7 +152,8 @@ def __init__(self, *args, **kwargs):
).all()
self.location_set_id = location_set_id
- kwargs['choices'] = _make_choices(group_choices, _('Group'))
+ kwargs['choices'] = _make_choices(
+ group_choices, _('Location Group'))
super().__init__(*args, **kwargs)
def queryset_(self, query, value, **kwargs):
@@ -170,6 +173,48 @@ def queryset_(self, query, value, **kwargs):
return SubmissionLocationGroupFilter
+def make_participant_group_filter(participant_set_id):
+ class ParticipantGroupFilter(ChoiceFilter):
+ field_class = ExtendedSelectField
+
+ def __init__(self, *args, **kwargs):
+ self.participant_set_id = participant_set_id
+
+ choices = OrderedDict()
+ choices[''] = _('Participant Group')
+ for group_type in services.participant_group_types.find(
+ participant_set_id=participant_set_id
+ ).order_by(models.ParticipantGroupType.name):
+ for group in services.participant_groups.find(
+ group_type=group_type
+ ).order_by(models.ParticipantGroup.name):
+ choices.setdefault(group_type.name, []).append(
+ (group.id, group.name)
+ )
+
+ kwargs['choices'] = [(k, choices[k]) for k in choices]
+ print(kwargs['choices'])
+ kwargs['coerce'] = int
+ super(ParticipantGroupFilter, self).__init__(*args, **kwargs)
+
+ def queryset_(self, queryset, value):
+ if value:
+ participant_ids = models.Participant.query.join(
+ models.Participant.groups
+ ).filter(
+ models.Participant.participant_set_id == self.participant_set_id, # noqa
+ models.ParticipantGroup.id == value
+ ).with_entities(models.Participant.id)
+
+ return queryset.filter(
+ models.Submission.participant_id.in_(participant_ids)
+ )
+
+ return queryset
+
+ return ParticipantGroupFilter
+
+
def make_base_submission_filter(event, filter_on_locations=False):
class BaseSubmissionFilterSet(FilterSet):
sample = make_submission_sample_filter(
@@ -676,6 +721,8 @@ def make_dashboard_filter(event, filter_on_locations=False):
event.participant_set_id, filter_on_locations=filter_on_locations)()
attributes['location_group'] = make_submission_location_group_filter(
event.location_set_id)()
+ attributes['participant_group'] = make_participant_group_filter(
+ event.participant_set_id)()
return type(
'SubmissionFilterSet',
@@ -748,6 +795,8 @@ def make_submission_list_filter(event, form, filter_on_locations=False):
attributes['fsn'] = FormSerialNumberFilter()
attributes['participant_role'] = make_participant_role_filter(
event.participant_set_id)()
+ attributes['participant_group'] = make_participant_group_filter(
+ event.participant_set_id)()
return type(
'SubmissionFilterSet',
@@ -806,6 +855,8 @@ class QualityAssuranceConditionsForm(Form):
attributes['fsn'] = FormSerialNumberFilter()
attributes['participant_role'] = make_participant_role_filter(
event.participant_set_id)()
+ attributes['participant_group'] = make_participant_group_filter(
+ event.participant_set_id)()
return type(
'QualityAssuranceFilterSet',
diff --git a/apollo/wtforms_ext.py b/apollo/wtforms_ext.py
index e41a67b5a..60c87f016 100644
--- a/apollo/wtforms_ext.py
+++ b/apollo/wtforms_ext.py
@@ -25,10 +25,8 @@ def __call__(self, field, **kwargs):
group_items = item2
html.append('')
else:
val = item1
diff --git a/migrations/versions/3679afe4da46_add_participant_groups.py b/migrations/versions/3679afe4da46_add_participant_groups.py
new file mode 100644
index 000000000..720b429c8
--- /dev/null
+++ b/migrations/versions/3679afe4da46_add_participant_groups.py
@@ -0,0 +1,68 @@
+"""add participant groups
+
+Revision ID: 3679afe4da46
+Revises: c4166678fb79
+Create Date: 2022-04-06 13:17:44.946820
+
+"""
+from alembic import op
+import sqlalchemy as sa
+from sqlalchemy.dialects import postgresql
+
+# revision identifiers, used by Alembic.
+revision = "3679afe4da46"
+down_revision = "c4166678fb79"
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.create_table(
+ "participant_group_type",
+ sa.Column("id", sa.Integer(), nullable=False),
+ sa.Column("name", sa.String(), nullable=True),
+ sa.Column("participant_set_id", sa.Integer(), nullable=False),
+ sa.Column("uuid", postgresql.UUID(as_uuid=True), nullable=False),
+ sa.ForeignKeyConstraint(
+ ["participant_set_id"], ["participant_set.id"], ondelete="CASCADE"
+ ),
+ sa.PrimaryKeyConstraint("id"),
+ )
+ op.create_table(
+ "participant_group",
+ sa.Column("id", sa.Integer(), nullable=False),
+ sa.Column("name", sa.VARCHAR(), nullable=False),
+ sa.Column("group_type_id", sa.Integer(), nullable=False),
+ sa.Column("participant_set_id", sa.Integer(), nullable=False),
+ sa.Column("uuid", postgresql.UUID(as_uuid=True), nullable=False),
+ sa.ForeignKeyConstraint(
+ ["group_type_id"],
+ ["participant_group_type.id"],
+ ondelete="CASCADE",
+ ),
+ sa.ForeignKeyConstraint(
+ ["participant_set_id"], ["participant_set.id"], ondelete="CASCADE"
+ ),
+ sa.PrimaryKeyConstraint("id"),
+ )
+ op.create_table(
+ "participant_group_participants",
+ sa.Column("group_id", sa.Integer(), nullable=False),
+ sa.Column("participant_id", sa.Integer(), nullable=False),
+ sa.ForeignKeyConstraint(
+ ["group_id"], ["participant_group.id"], ondelete="CASCADE"
+ ),
+ sa.ForeignKeyConstraint(
+ ["participant_id"], ["participant.id"], ondelete="CASCADE"
+ ),
+ )
+ # ### end Alembic commands ###
+
+
+def downgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.drop_table("participant_group_participants")
+ op.drop_table("participant_group")
+ op.drop_table("participant_group_type")
+ # ### end Alembic commands ###