diff --git a/mo/web/org_contest.py b/mo/web/org_contest.py
index 90692599af26ad01b6ffa9955c5020560562fc4b..c02bd849a1704548e27dc77db9b4331e075ddff1 100644
--- a/mo/web/org_contest.py
+++ b/mo/web/org_contest.py
@@ -5,6 +5,7 @@ import locale
 import os
 import secrets
 from sqlalchemy.orm import joinedload
+from sqlalchemy.orm.query import Query
 from typing import List, Tuple, Optional, Sequence
 import werkzeug.exceptions
 import wtforms
@@ -16,7 +17,8 @@ import mo.imports
 import mo.rights
 import mo.util
 from mo.web import app
-from mo.web.table import Table, Column, cell_place_link, cell_user_link, cell_email_link
+from mo.web.util import PagerForm
+from mo.web.table import CellCheckbox, Table, Column, cell_place_link, cell_user_link, cell_email_link
 import wtforms.validators as validators
 
 
@@ -25,6 +27,132 @@ class ImportForm(FlaskForm):
     submit = wtforms.SubmitField('Importovat')
 
 
+class ParticipantsFilterForm(PagerForm):
+    school = wtforms.StringField("Škola")
+    participation_place = wtforms.StringField("Soutěžní místo")
+    contest_place = wtforms.StringField("Soutěžní oblast")
+    participation_state = wtforms.SelectField('Stav účasti', choices=[('*', '*')] + list(db.PartState.choices()), default='*')
+
+    # format = wtforms.RadioField(choices=[('', 'Zobrazit'), ('csv', 'Stáhnout vše v CSV'), ('tsv', 'Stáhnout vše v TSV')])
+    submit = wtforms.SubmitField("Zobrazit")
+    download_csv = wtforms.SubmitField("↓ CSV")
+    download_tsv = wtforms.SubmitField("↓ TSV")
+
+
+class ParticipantsActionForm(FlaskForm):
+    action_on = wtforms.RadioField(
+        "Provést akci na", validators=[validators.DataRequired()],
+        choices=[('all', 'všech vyfiltrovaných soutěžících'), ('checked', 'označených soutěžících')]
+        # checkboxes are handled not through FlaskForm, see below
+    )
+
+    participation_state = wtforms.SelectField('Stav účasti', choices=list(db.PartState.choices()))
+    set_participation_state = wtforms.SubmitField("Nastavit stav účasti")
+
+    participation_place = wtforms.StringField(
+        'Soutěžní místo', description='Zadejte kód nebo ID ve tvaru <code>#123</code>'
+    )
+    set_participation_place = wtforms.SubmitField("Nastavit soutěžní místo")
+
+    contest_place = wtforms.StringField(
+        'Soutěžní oblast',
+        description='Musí existovat soutěž v dané oblasti pro stejné kolo. Oblast zadejte pomocí kódu nebo ID ve tvaru <code>#123</code>.'
+    )
+    set_contest = wtforms.SubmitField("Přesunout do jiné soutěžní oblasti")
+
+    remove_participation = wtforms.SubmitField("Smazat záznam o účasti")
+
+    def do_action(self, round: db.Round, rights: mo.rights.Rights, query: Query) -> bool:
+        """Do participation modification on partipations from given query
+        (possibly filtered by checkboxes). Expects that rights for round/contest
+        are checked before calling this function, `rights` param are used only
+        for checking that we can move participation to another contest."""
+
+        if not self.validate_on_submit():
+            return False
+
+        sess = db.get_session()
+
+        # Check that operation is valid
+        if self.set_participation_state.data:
+            pass
+        elif self.set_participation_place.data:
+            participation_place = db.get_place_by_code(self.participation_place.data)
+            if not participation_place:
+                flash('Nenalezeno místo s daným kódem', 'danger')
+                return False
+        elif self.set_contest.data:
+            contest_place = db.get_place_by_code(self.contest_place.data)
+            if not contest_place:
+                flash("Nepovedlo se najít zadanou soutěžní oblast", 'danger')
+                return False
+            contest = sess.query(db.Contest).filter_by(round_id=round.round_id, place_id=contest_place.place_id).one_or_none()
+            if not contest:
+                flash(f"Nepovedlo se najít soutěž v kole {round.round_code()} v oblasti {contest_place.name}", 'danger')
+                return False
+            rights.get_for_contest(contest)
+            if not rights.have_right(mo.rights.Right.manage_contest):
+                flash(f"Nemáte právo ke správě soutěže v kole {round.round_code()} v oblasti {contest_place.name}, nelze do ní přesunout soutěžící", 'danger')
+        elif self.remove_participation.data:
+            pass
+        else:
+            flash('Neznámá operace', 'danger')
+            return False
+
+        try:
+            user_ids = list(map(int, request.form.getlist('checked')))
+        except ValueError:
+            flash('Data v checkboxech nelze převést na čísla, kontaktujte správce', 'danger')
+            return False
+
+        count = 0
+        ctants = query.all()
+        for pion, _, _ in ctants:
+            u = pion.user
+            if self.action_on.data == 'checked' and u.user_id not in user_ids:
+                continue
+
+            if self.remove_participation.data:
+                sess.delete(pion)
+                app.logger.info(f"Participation of user {u.user_id} in contest {pion.contest} removed")
+                mo.util.log(
+                    type=db.LogType.participant,
+                    what=u.user_id,
+                    details={'action': 'participation-removed', 'participation': db.row2dict(pion)},
+                )
+            else:
+                if self.set_participation_state.data:
+                    pion.state = self.participation_state.data
+                elif self.set_participation_place.data:
+                    pion.place = participation_place
+                elif self.set_contest.data:
+                    pion.contest = contest
+
+                changes = db.get_object_changes(pion)
+                app.logger.info(f"Participation of user {u.user_id} modified, changes: {changes}")
+                mo.util.log(
+                    type=db.LogType.participant,
+                    what=u.user_id,
+                    details={'action': 'participation-changed', 'changes': changes},
+                )
+                sess.flush()
+            count += 1
+
+        sess.commit()
+        if count == 0:
+            flash('Žádní vybraní soutěžící', 'warning')
+        elif self.set_participation_state.data:
+            flash(f'Nastaven stav {db.part_state_names[self.participation_state.data]} {count} řešitelům', 'success')
+        elif self.set_participation_place.data:
+            flash(f'Nastaveno soutěžní místo {participation_place.name} {count} řešitelům', 'success')
+        elif self.set_contest.data:
+            flash(f'{count} řešitelů přesunuto do soutěže v oblasti {contest_place.name}', 'success')
+        elif self.remove_participation.data:
+            flash(f'Odstraněno {count} soutěžících z této soutěže', 'success')
+
+        return True
+
+
 @app.route('/org/contest/')
 def org_contest_root():
     sess = db.get_session()
@@ -73,20 +201,39 @@ def org_round(id: int):
     )
 
 
-@app.route('/org/contest/r/<int:id>/list')
+@app.route('/org/contest/r/<int:id>/list', methods=('GET', 'POST'))
 def org_round_list(id: int):
     round, rr = get_round_rr(id, mo.rights.Right.manage_contest)
     format = request.args.get('format', "")
 
-    table = make_contestant_table(round, None)
+    filter = ParticipantsFilterForm(request.args)
+    filter.validate()
+    query = get_contestants_query(
+        round=round,
+        school=db.get_place_by_code(filter.school.data),
+        contest_place=db.get_place_by_code(filter.contest_place.data),
+        participation_place=db.get_place_by_code(filter.participation_place.data),
+        participation_state=None if filter.participation_state.data == '*' else filter.participation_state.data
+    )
+
+    action_form = ParticipantsActionForm()
+    if action_form.do_action(round=round, rights=rr, query=query):
+        # Action happened, redirect
+        return redirect(request.url)
+
+    (count, query) = filter.apply_limits(query, pagesize=50)
+    # count = query.count()
 
     if format == "":
+        table = make_contestant_table(query, add_contest_column=True, add_checkbox=True)
         return render_template(
             'org_round_list.html',
             round=round,
             table=table,
+            filter=filter, count=count, action_form=action_form,
         )
     else:
+        table = make_contestant_table(query)
         return table.send_as(format)
 
 
@@ -191,20 +338,39 @@ def org_contest_import_template():
     return resp
 
 
-@app.route('/org/contest/c/<int:id>/ucastnici')
+@app.route('/org/contest/c/<int:id>/ucastnici', methods=('GET', 'POST'))
 def org_contest_list(id: int):
     contest, rr = get_contest_rr(id, mo.rights.Right.manage_contest)
     format = request.args.get('format', "")
 
-    table = make_contestant_table(contest.round, contest)
+    filter = ParticipantsFilterForm(request.args)
+    filter.validate()
+    query = get_contestants_query(
+        round=contest.round, contest=contest,
+        school=db.get_place_by_code(filter.school.data),
+        # contest_place=db.get_place_by_code(filter.contest_place.data),
+        participation_place=db.get_place_by_code(filter.participation_place.data),
+        participation_state=None if filter.participation_state.data == '*' else filter.participation_state.data
+    )
+
+    action_form = ParticipantsActionForm()
+    if action_form.do_action(round=contest.round, rights=rr, query=query):
+        # Action happened, redirect
+        return redirect(request.url)
+
+    # (count, query) = filter.apply_limits(query, pagesize=50)
+    count = query.count()
 
     if format == "":
+        table = make_contestant_table(query, add_checkbox=True)
         return render_template(
             'org_contest_list.html',
             contest=contest,
             table=table,
+            filter=filter, count=count, action_form=action_form,
         )
     else:
+        table = make_contestant_table(query)
         return table.send_as(format)
 
 
@@ -221,7 +387,12 @@ contest_list_columns = (
 )
 
 
-def make_contestant_table(round: db.Round, contest: Optional[db.Contest]) -> Table:
+def get_contestants_query(
+        round: db.Round, contest: Optional[db.Contest] = None,
+        contest_place: Optional[db.Place] = None,
+        participation_place: Optional[db.Place] = None,
+        participation_state: Optional[db.PartState] = None,
+        school: Optional[db.Place] = None) -> Query:
     query = (db.get_session()
              .query(db.Participation, db.Participant, db.Contest)
              .select_from(db.Participation)
@@ -233,10 +404,22 @@ def make_contestant_table(round: db.Round, contest: Optional[db.Contest]) -> Tab
         query = query.filter(db.Contest.round == round)
         query = query.options(joinedload(db.Contest.place))
     query = query.filter(db.Participation.contest_id == db.Contest.contest_id)
+    if contest_place:
+        query = query.filter(db.Contest.place_id == contest_place.place_id)
+    if participation_place:
+        query = query.filter(db.Participation.place_id == participation_place.place_id)
+    if school:
+        query = query.filter(db.Participant.school == school.place_id)
+    if participation_state:
+        query = query.filter(db.Participation.state == participation_state)
     query = query.options(joinedload(db.Participation.user),
                           joinedload(db.Participation.place),
                           joinedload(db.Participant.school_place))
 
+    return query
+
+
+def make_contestant_table(query: Query, add_checkbox: bool = False, add_contest_column: bool = False):
     ctants = query.all()
 
     rows: List[dict] = []
@@ -254,18 +437,22 @@ def make_contestant_table(round: db.Round, contest: Optional[db.Contest]) -> Tab
             'region_code': cell_place_link(ct.place, ct.place.get_code()),
             'place_code': cell_place_link(pion.place, pion.place.get_code()),
             'status': pion.state.friendly_name(),
+            'checkbox': CellCheckbox('checked', u.user_id, False),
         })
 
     rows.sort(key=lambda r: r['sort_key'])
 
     cols: Sequence[Column] = contest_list_columns
-    if not contest:
+    if add_checkbox:
+        cols = [Column(key='checkbox', name=' ', title=' ')] + list(cols)
+    if add_contest_column:
         cols = list(cols) + [Column(key='region_code', name='kod_oblasti', title='Oblast')]
 
     return Table(
         columns=cols,
         rows=rows,
         filename='ucastnici',
+        show_downlink=False,  # downlinks are in filter
     )
 
 
diff --git a/mo/web/templates/org_contest_list.html b/mo/web/templates/org_contest_list.html
index cb61b0768013d09f4da6ab873bc2a5a53861781c..4448ebaa832f99df59a4d133be6212b0ad675bba 100644
--- a/mo/web/templates/org_contest_list.html
+++ b/mo/web/templates/org_contest_list.html
@@ -1,7 +1,21 @@
 {% extends "base.html" %}
+{% import "bootstrap/wtf.html" as wtf %}
 {% block body %}
 <h2>Účastníci {{ contest.round.round_code() }}: {{ contest.place.name }}</h2>
 
-{{ table.to_html() }}
+<form action="" method="GET" class="form form-inline" role="form">
+	{{ wtf.form_field(filter.participation_place, placeholder='Kód / #ID', size=8) }}
+	{{ wtf.form_field(filter.school, placeholder='Kód / #ID', size=8) }}
+	{{ wtf.form_field(filter.participation_state) }}
+	<div class="btn-group">
+		{{ wtf.form_field(filter.submit, class='btn btn-primary') }}
+		<button class="btn btn-default" name="format" value="csv" title="Stáhnout celý výsledek v CSV">↓ CSV</button>
+		<button class="btn btn-default" name="format" value="tsv" title="Stáhnout celý výsledek v TSV">↓ TSV</button>
+	</div>
+</form>
+
+<p>Celkem <b>{{count}} nalezených soutěžících</b>.</p>
+
+{% include 'parts/org_participants_table_actions.html' %}
 
 {% endblock %}
diff --git a/mo/web/templates/org_round_list.html b/mo/web/templates/org_round_list.html
index fa958549d713fdf241eed2e9c364b30c678bdd24..59108cf521d1c573ecedbb10dda547edcf361a9f 100644
--- a/mo/web/templates/org_round_list.html
+++ b/mo/web/templates/org_round_list.html
@@ -1,7 +1,41 @@
 {% extends "base.html" %}
+{% import "bootstrap/wtf.html" as wtf %}
 {% block body %}
 <h2>Účastníci kola {{ round.round_code() }}</h2>
 
-{{ table.to_html() }}
+<form action="" method="GET" class="form form-inline" role="form">
+	{{ wtf.form_field(filter.contest_place, placeholder='Kód / #ID', size=8) }}
+	{{ wtf.form_field(filter.participation_place, placeholder='Kód / #ID', size=8) }}
+	{{ wtf.form_field(filter.school, placeholder='Kód / #ID', size=8) }}
+	{{ wtf.form_field(filter.participation_state) }}
+	<div class="btn-group">
+		{{ wtf.form_field(filter.submit, class='btn btn-primary') }}
+		<button class="btn btn-default" name="format" value="csv" title="Stáhnout celý výsledek v CSV">↓ CSV</button>
+		<button class="btn btn-default" name="format" value="tsv" title="Stáhnout celý výsledek v TSV">↓ TSV</button>
+	</div>
+
+	<div style="float: right">
+		Stránka {{ filter.offset.data // filter.limit.data + 1}} z {{ (count / filter.limit.data)|round(0, 'ceil')|int }}:
+		<div class="btn-group">
+			{% if filter.offset.data > 0 %}
+				{{ wtf.form_field(filter.previous) }}
+			{% else %}
+				<button class="btn" disabled>Předchozí</button>
+			{% endif %}
+			{% if count > filter.offset.data + filter.limit.data %}
+				{{ wtf.form_field(filter.next) }}
+			{% else %}
+				<button class="btn" disabled>Další</button>
+			{% endif %}
+		</div>
+		<input type="hidden" name="offset" value="{{filter.offset.data}}">
+		<input type="hidden" name="limit" value="{{filter.limit.data}}">
+	</div>
+</form>
+
+{% set max = filter.offset.data + filter.limit.data if filter.offset.data + filter.limit.data < count else count %}
+<p>Zobrazuji záznamy <b>{{filter.offset.data + 1}}</b> až <b>{{ max }}</b> z <b>{{count}} nalezených soutěžících</b>.</p>
+
+{% include 'parts/org_participants_table_actions.html' %}
 
 {% endblock %}
diff --git a/mo/web/templates/parts/org_participants_table_actions.html b/mo/web/templates/parts/org_participants_table_actions.html
new file mode 100644
index 0000000000000000000000000000000000000000..56f33a324dc9d677d61999e1f8c7cfadd8395838
--- /dev/null
+++ b/mo/web/templates/parts/org_participants_table_actions.html
@@ -0,0 +1,34 @@
+<form action="" method="POST" class="form form-horizontal" role="form">
+	{{ table.to_html() }}
+
+	{{ action_form.csrf_token }}
+	<h3>Provést akci</h3>
+	<div class="form-group">
+		<label class="col-form-label col-sm-2">Provést akci na:</label>
+		<div class="col-sm-10">
+			<div class="radio">
+				<label>
+					<input id="action_on-0" name="action_on" type="radio" value="all" required{% if action_form.action_on.data == 'all' %} checked{% endif %}>
+					všech vyfiltrovaných soutěžících
+				</label>
+			</div>
+			<div class="radio">
+				<label>
+					<input id="action_on-1" name="action_on" type="radio" value="checked" required{% if action_form.action_on.data == 'all' %} checked{% endif %}>
+					označených soutěžících
+				</label>
+			</div>
+		</div>
+	</div>
+	<hr>
+	{{ wtf.form_field(action_form.participation_state, form_type='horizontal', horizontal_columns=('sm', 2, 10)) }}
+	{{ wtf.form_field(action_form.set_participation_state, form_type='horizontal', class='btn btn-primary', horizontal_columns=('sm', 2, 10)) }}
+	<hr>
+	{{ wtf.form_field(action_form.participation_place, form_type='horizontal', horizontal_columns=('sm', 2, 10)) }}
+	{{ wtf.form_field(action_form.set_participation_place, form_type='horizontal', class='btn btn-primary', horizontal_columns=('sm', 2, 10)) }}
+	<hr>
+	{{ wtf.form_field(action_form.contest_place, form_type='horizontal', horizontal_columns=('sm', 2, 10)) }}
+	{{ wtf.form_field(action_form.set_contest, form_type='horizontal', class='btn btn-primary', horizontal_columns=('sm', 2, 10)) }}
+	<hr>
+	{{ wtf.form_field(action_form.remove_participation, form_type='horizontal', class='btn btn-danger', horizontal_columns=('sm', 2, 10)) }}
+</form>