diff --git a/mo/util.py b/mo/util.py
index a72c5bd6fdf34800d2c58501adec3d719c18aa32..10c2d63896d8f70cb31de877a5a4ced9458a4e7f 100644
--- a/mo/util.py
+++ b/mo/util.py
@@ -13,7 +13,7 @@ import re
 import secrets
 import subprocess
 import sys
-from typing import Any, Optional, NoReturn, Tuple
+from typing import Any, Optional, NoReturn, Tuple, List
 import textwrap
 import urllib.parse
 
@@ -247,3 +247,24 @@ def check_points(points: decimal.Decimal, for_task: Optional[db.Task] = None, fo
         else:
             return f'Podle nastavení kola zadat body jen s krokem {points_step} (hodnota {points} je neplatná)'
     return None
+
+
+def parse_int_list(a: str, maxim: int = 200) -> List[int]:
+    """Parsuje "1-3,5,7-9" na [1,2,3,5,7,9].
+    Aby nešlo generovat moc velká pole (obrana proti DDoSu),
+    existuje omezení na velikost."""
+    r: List[int] = []
+    for i in a.split(","):
+        b = i.split("-")
+        if len(b) > 2:
+            raise mo.CheckError("Nadměrný počet pomlček")
+        try:
+            c = list(map(int, b))
+        except ValueError:
+            raise mo.CheckError("Převod na číslo se nezdařil")
+        if any(x < 0 or x > maxim for x in c):
+            raise mo.CheckError("Překročen limit na velikost čísla")
+        if len(c) == 2 and c[0] > c[1]:
+            raise mo.CheckError("Větší číslo nemůže být před menším")
+        r += [c[0]] if len(c) == 1 else range(c[0], c[1] + 1)
+    return r
diff --git a/mo/web/org_users.py b/mo/web/org_users.py
index c29beed486be54a99e4d3b6c3ce72667168f608c..db44ea01379bbdde91ecfeb5d973cfb09d7975a9 100644
--- a/mo/web/org_users.py
+++ b/mo/web/org_users.py
@@ -1,9 +1,10 @@
-from typing import Optional
+from typing import Optional, List
 from flask import render_template, g, redirect, url_for, flash, request
 from flask_wtf import FlaskForm
 import werkzeug.exceptions
 import wtforms
 from sqlalchemy import or_
+import flask_sqlalchemy
 from sqlalchemy.orm import joinedload, subqueryload
 
 from wtforms import validators
@@ -147,50 +148,149 @@ def org_users():
         can_add=rr.have_right(Right.add_users),
     )
 
-
 class OrgsFilterForm(PagerForm):
     # user
     search_name = wtforms.TextField("Jméno/příjmení", render_kw={'autofocus': True})
     search_email = wtforms.TextField("E-mail")
 
-    # TODO: filtering by roles?
+    search_role = wtforms.SelectMultipleField('Role',  choices=db.RoleType.choices(), coerce=db.RoleType.coerce, validators=[validators.Optional()])
+    search_right_for_place_code  = wtforms.StringField('Právo pro oblast', validators=[validators.Optional()])
+    search_in_place_code  = wtforms.StringField('V oblasti', validators=[validators.Optional()])
+    search_place_level = wtforms.SelectMultipleField("Úroveň oblasti", choices=[(i.level, i.name) for i in db.place_levels], validators=[validators.Optional()], coerce=int)
+    search_year = wtforms.StringField('Ročník', validators=[validators.Optional()])
+    search_category = wtforms.StringField("Kategorie", validators=[validators.Optional()])
+    search_seq = wtforms.StringField("Kolo", validators=[validators.Optional()])
+
     submit = wtforms.SubmitField("Filtrovat")
+    show_role_filter = wtforms.SubmitField("Zobrazit filtrování dle rolí")
+    hide_role_filter = wtforms.SubmitField("Skrýt filtrování dle rolí")
 
-    # Výstupní hodnoty filtru, None při nepoužitém filtru, prázdná db hodnota při
-    # nepovedené filtraci (neexistující místo a podobně)
+    is_role_filter = wtforms.HiddenField(default="") # "" -> skrýt. "yes" -> zobrazit
+
+    # Výstupní hodnoty filtru, None při nepoužitém filtru nebo špatné hodnotě (takové filtry ignorujeme)
     f_search_name: Optional[str] = None
     f_search_email: Optional[str] = None
+    f_search_role: Optional[List[db.RoleType]] = None
+    f_search_right_for_place: Optional[db.Place] = None
+    f_search_in_place: Optional[db.Place] = None
+    f_search_year: Optional[List[int]] = None
+    f_search_category: Optional[List[str]] = None
+    f_search_place_level: Optional[List[int]] = None
+    f_search_seq: Optional[List[int]] = None
+
+    def validate_search_name(self, field):
+        self.f_search_name = f"%{field.data}%"
+
+    def validate_search_email(self, field):
+        self.f_search_email = f"%{field.data}%"
+
+    def validate_search_role(self, field):
+        self.f_search_role = field.data
+
+    def validate_search_right_for_place_code(self, field):
+        self.f_search_right_for_place = db.get_place_by_code(field.data)
+        if self.f_search_right_for_place is None:
+            raise wtforms.ValidationError("Chybné označení oblasti")
+
+    def validate_search_in_place_code(self, field):
+        self.f_search_in_place = db.get_place_by_code(field.data)
+        if self.f_search_in_place is None:
+            raise wtforms.ValidationError("Chybné označení oblasti")
+
+    def validate_search_year(self, field):
+        try:
+            self.f_search_year = mo.util.parse_int_list(field.data)
+        except mo.CheckError as e:
+            raise wtforms.ValidationError(str(e))
 
-    def validate(self):
-        self.f_search_name = f"%{self.search_name.data}%" if self.search_name.data else None
-        self.f_search_email = f"%{self.search_email.data}%" if self.search_email.data else None
+    def validate_search_category(self, field):
+        self.f_search_category = field.data.split(",")
 
+    def validate_search_place_level(self, field):
+        self.f_search_place_level = field.data
 
-@app.route('/org/org/')
+    def validate_search_seq(self, field):
+        try:
+            self.f_search_seq = mo.util.parse_int_list(field.data)
+        except mo.CheckError as e:
+            raise wtforms.ValidationError(str(e))
+
+    def prepare_role_filter(self):
+        if self.show_role_filter.data:
+            self.is_role_filter.data = "yes"
+        if self.hide_role_filter.data:
+            self.is_role_filter.data = ""
+        if self.is_role_filter.data:
+            del self.show_role_filter
+        else:
+            del self.hide_role_filter
+            del self.search_role
+            del self.search_right_for_place_code
+            del self.search_in_place_code
+            del self.search_place_level
+            del self.search_year
+            del self.search_category
+            del self.search_seq
+
+
+@app.route('/org/org/', methods=('GET', 'POST'))
 def org_orgs():
     sess = db.get_session()
     rr = g.gatekeeper.rights_generic()
-
     q = sess.query(db.User).filter(or_(db.User.is_admin, db.User.is_org)).options(
         subqueryload(db.User.roles).joinedload(db.UserRole.place)
     )
-    filter = OrgsFilterForm(request.args)
-    filter.validate()
+    filter = OrgsFilterForm()
+    filter.validate_on_submit()
+    filter.prepare_role_filter()
 
     if filter.f_search_name:
         q = q.filter(or_(
             db.User.first_name.ilike(filter.f_search_name),
             db.User.last_name.ilike(filter.f_search_name)
         ))
-    if filter.f_search_email:
-        q = q.filter(db.User.email.ilike(filter.f_search_email))
+
+    def query_filter_role(qr: flask_sqlalchemy.BaseQuery) -> flask_sqlalchemy.BaseQuery:
+        if filter.f_search_role is not None:
+            qr = qr.filter(db.UserRole.role.in_(filter.f_search_role))
+        if filter.f_search_category is not None:
+            qr = qr.filter(or_(db.UserRole.category.in_(filter.f_search_category), db.UserRole.category == None))
+        if filter.f_search_seq is not None:
+            qr = qr.filter(or_(db.UserRole.seq.in_(filter.f_search_seq), db.UserRole.seq == None))
+        if filter.f_search_year is not None:
+            qr = qr.filter(or_(db.UserRole.year.in_(filter.f_search_year), db.UserRole.year == None))
+        if filter.f_search_in_place is not None:
+            qr = qr.filter(db.UserRole.place_id.in_(db.place_descendant_cte(filter.f_search_in_place)))
+        if filter.f_search_right_for_place is not None:
+            qr = qr.filter(db.UserRole.place_id.in_([x.place_id for x in db.get_place_parents(filter.f_search_right_for_place)]))
+                    # Po n>3 hodinách v mo.db jsem dospěl k závěru, že to hezčeji neumím (neumím vyrobit place_parents_cte)
+        if filter.f_search_place_level is not None:
+            qr = qr.filter(db.UserRole.place_id.in_(
+                sess.query(db.Place.place_id).filter(db.Place.level.in_(filter.f_search_place_level))
+                ))
+        return qr
+
+    if filter.is_role_filter.data:
+        qr = sess.query(db.UserRole.user_id)
+        qr = query_filter_role(qr)
+        q = q.filter(db.User.user_id.in_(qr))
+
+    q = q.order_by(db.User.user_id)
 
     (count, q) = filter.apply_limits(q, pagesize=50)
     users = q.all()
 
+    marked_roles_id: Set[int] = set()
+
+    if filter.is_role_filter.data:
+        qmr = sess.query(db.UserRole.user_role_id).filter(db.UserRole.user_id.in_([i.user_id for i in users]))
+        qmr = query_filter_role(qmr)
+        marked_roles_id = set([i[0] for i in qmr.all()])
+
     return render_template(
         'org_orgs.html', users=users, count=count,
         filter=filter,
+        marked_roles_id=marked_roles_id,
         can_edit=rr.have_right(Right.edit_orgs),
         can_add=rr.have_right(Right.add_orgs),
     )
diff --git a/mo/web/templates/org_orgs.html b/mo/web/templates/org_orgs.html
index dbd6cc7e52c2f7550237dd434fd46e35a19e1605..3d093c5389df2b03400c6a251f419fedc9beaa07 100644
--- a/mo/web/templates/org_orgs.html
+++ b/mo/web/templates/org_orgs.html
@@ -7,7 +7,9 @@
 {% endif %}
 
 <div class="form-frame">
-<form action="" method="GET" role="form">
+<form action="" method="POST" role="form">
+	{{ filter.csrf_token }}
+	{{ filter.is_role_filter }}
 	<div class="row">
 		<div class='col-sm-2'><strong>Filtr organizátorů</strong></div>
 		<div class="col-sm-3">
@@ -17,8 +19,35 @@
 			{{ wtf.form_field(filter.search_email, placeholder='Libovolná část e-mailu') }}
 		</div>
 	</div>
+	{% if filter.is_role_filter.data %}
+	<div class="row">
+		<div class='col-sm-2'><strong>Filtr podle rolí</strong><p>Hledá pouze organizátory, kteří mají přidělenu nějakou roli.</p></div>
+		<div class="col-sm-2">
+			{{ wtf.form_field(filter.search_role, size=filter.search_role.choices|length, class="form-control no-scroll") }}
+		</div>
+		<div class="col-sm-2">
+			{{ wtf.form_field(filter.search_year, placeholder='Např. 65-67,70') }}
+			{{ wtf.form_field(filter.search_category, placeholder='Např. A,P,Z9') }}
+		</div>
+		<div class="col-sm-2">
+			{{ wtf.form_field(filter.search_seq, placeholder='Např. 1,3-4') }}
+		</div>
+		<div class="col-sm-2">
+			<span title="Omezí role na ty, které jsou přiděleny k dané oblasti a nebo její podoblasti.">{{ wtf.form_field(filter.search_in_place_code, placeholder='Kód oblasti') }}</span>
+			<span title="Omezí role na ty, které mají právo k dané oblasti. Tedy mohou být přiděleny i k nadřazené oblasti.">{{ wtf.form_field(filter.search_right_for_place_code, placeholder='Kód oblasti') }}</span>
+		</div>
+		<div class="col-sm-2">
+			{{ wtf.form_field(filter.search_place_level, size=filter.search_place_level.choices|length, class="form-control no-scroll" ) }}
+		</div>
+	</div>
+	{% endif %}
 	<div class="btn-group">
 			{{ wtf.form_field(filter.submit, class='btn btn-primary') }}
+			{% if filter.is_role_filter.data %}
+				{{ wtf.form_field(filter.hide_role_filter) }}
+			{% else %}
+				{{ wtf.form_field(filter.show_role_filter) }}
+			{% endif %}
 			{% if filter.offset.data > 0 %}
 				{{ wtf.form_field(filter.previous) }}
 			{% else %}
@@ -52,10 +81,15 @@
 	<tr>
 		<td>{{ user.first_name }}</td><td>{{ user.last_name }}</td>
 		<td>{{ user.email|mailto }}{{ user|user_flags }}</td>
-		<td>{% if user.is_admin %}správce{% elif user.roles|count == 0 %}<i>žádná role</i>{% else %}
+		<td>{% if user.is_admin %}správce{% elif user.roles|count == 0 %}<i>žádná role</i>{% endif %}
+			{% if user.roles|count > 0 %}
 			<ul>
 			{% for role in user.roles %}
-				<li>{{ role }}</li>
+				{% if role.user_role_id in marked_roles_id %}
+					<li><b>{{ role }}</b></li>
+				{% else %}
+					<li>{{ role }}</li>
+				{% endif %}
 			{%- endfor %}
 			</ul>
 		{% endif %}</td>
diff --git a/static/mo.css b/static/mo.css
index 3c5df7742a44ae139d1ac48fd4c6886280bdfc0b..7dcf41627fcf49589e0437c14bdfd8833da9d03f 100644
--- a/static/mo.css
+++ b/static/mo.css
@@ -179,6 +179,10 @@ nav#main-menu a.active {
 	border-radius: 4px 4px;
 }
 
+select.no-scroll::-webkit-scrollbar {
+	display: none;
+}
+
 .checked_toggle input.toggle:checked ~ .checked_hide {
 	display: none;
 }