diff --git a/bin/create-round b/bin/create-round
index a65a32d732791b61055cf4501a020756b48fb95b..762b37d547c9a9164c7f41e050b30f8c97611d43 100755
--- a/bin/create-round
+++ b/bin/create-round
@@ -10,6 +10,7 @@ parser = argparse.ArgumentParser(description='Založí soutěžní kolo')
 parser.add_argument('-y', '--year', type=int, required=True, help='ročník')
 parser.add_argument('-c', '--cat', type=str, required=True, help='kategorie')
 parser.add_argument('-s', '--seq', type=int, required=True, help='pořadí kola')
+parser.add_argument('-C', '--code', type=str, help='kód kola (default: roven pořadí)')
 parser.add_argument('-l', '--level', type=int, required=True, help='úroveň v hierarchii oblastí')
 parser.add_argument('-p', '--part', type=int, default=0, help='část v rámci skupiny kol (default: 0)')
 parser.add_argument('-n', '--name', type=str, required=True, help='název kola')
@@ -33,6 +34,7 @@ rnd = db.Round(
     year=args.year,
     category=args.cat,
     seq=args.seq,
+    code=args.code or str(args.seq),
     part=args.part,
     level=args.level,
     name=args.name,
diff --git a/bin/import-points b/bin/import-points
index 17055911b7a7e5f2d95f4ca0c481a67b98b3d62e..14227e981031282456cd5d01b6de61c81b574ac9 100755
--- a/bin/import-points
+++ b/bin/import-points
@@ -5,7 +5,7 @@ import sys
 
 from mo.csv import FileFormat
 import mo.db as db
-from mo.imports import create_import, ImportType
+from mo.imports import PointsImport
 import mo.users
 import mo.util
 from mo.util import die
@@ -37,14 +37,13 @@ user = mo.users.user_by_email(args.user)
 if user is None:
     die(f"Uživatel {args.user} neexistuje")
 
-imp = create_import(
+imp = PointsImport(
     user,
-    type=ImportType.points,
-    fmt=FileFormat.tsv,
     round=round,
     task=task,
     allow_add_del=args.add_del,
 )
+imp.fmt = FileFormat.tsv
 
 if args.import_file:
     if not imp.run(args.import_file):
diff --git a/db/db.ddl b/db/db.ddl
index 1ffb18fa577c3342598437c3e2b2b7eeff4747a1..f08596aa1ff776cde48dd17b9160a1e1e20db801 100644
--- a/db/db.ddl
+++ b/db/db.ddl
@@ -113,10 +113,10 @@ CREATE TABLE rounds (
 	master_round_id	int		DEFAULT NULL REFERENCES rounds(round_id),
 	year		int		NOT NULL,			-- ročník MO
 	category	varchar(2)	NOT NULL,			-- "A", "Z5" apod.
-	seq		int		NOT NULL,			-- 1=domácí kolo atd.
+	seq		int		NOT NULL,			-- pořadí kola v kategorii (od 1)
 	part		int		NOT NULL DEFAULT 0,		-- část kola (nenulová u dělených kol)
 	level		int		NOT NULL,			-- úroveň hierarchie míst
-	code		varchar(255)	NOT NULL DEFAULT '',		-- kód kola ("1", "S" apod.), prázdný=podle seq
+	code		varchar(255)	NOT NULL,			-- kód kola ("1", "S" apod.)
 	name		varchar(255)	NOT NULL,			-- zobrazované jméno ("Krajské kolo" apod.)
 	state		round_state	NOT NULL DEFAULT 'preparing',	-- stav kola
 	tasks_file	varchar(255)	DEFAULT NULL,			-- jméno souboru se zadáním úloh
@@ -131,7 +131,8 @@ CREATE TABLE rounds (
 	has_messages	boolean		NOT NULL DEFAULT false,		-- má zprávičky
 	enroll_mode	enroll_mode	NOT NULL DEFAULT 'manual',	-- režim přihlašování (pro vyšší kola vždy 'manual')
 	enroll_advert	varchar(255)	NOT NULL DEFAULT '',		-- popis v přihlašovacím formuláři
-	UNIQUE (year, category, seq, part)
+	UNIQUE (year, category, seq, part),
+	UNIQUE (year, category, code, part)
 );
 
 CREATE INDEX rounds_master_round_id_index ON rounds (master_round_id);
diff --git a/db/upgrade-20211006.sql b/db/upgrade-20211006.sql
new file mode 100644
index 0000000000000000000000000000000000000000..3e2dcd65c35ce923493265f34f90c5f361e26eb3
--- /dev/null
+++ b/db/upgrade-20211006.sql
@@ -0,0 +1,7 @@
+SET ROLE mo_osmo;
+
+ALTER TABLE rounds ALTER COLUMN code DROP DEFAULT;
+
+UPDATE rounds SET code=seq WHERE code='';
+
+ALTER TABLE rounds ADD UNIQUE(year, category, code, part);
diff --git a/mo/db.py b/mo/db.py
index 1b3e5ff9fe2c8d728718de6f5b3e7f7ae40fbf06..674be18e85bbec96b69f033baf852afea8c66964 100644
--- a/mo/db.py
+++ b/mo/db.py
@@ -9,7 +9,7 @@ import re
 from sqlalchemy import \
     Boolean, Column, DateTime, ForeignKey, Integer, String, Text, UniqueConstraint, \
     text, func, \
-    create_engine, inspect, select
+    create_engine, inspect, select, or_, and_
 from sqlalchemy.engine import Engine
 from sqlalchemy.orm import relationship, sessionmaker, Session, class_mapper, joinedload, aliased
 from sqlalchemy.orm.attributes import get_history
@@ -136,7 +136,8 @@ class Place(Base):
         return place_levels[self.level].in_name() + " " + name
 
 
-def get_root_place():
+def get_root_place() -> Place:
+    """Obvykle voláme mo.rights.Gatekeeper.get_root_place(), kterékešuje."""
     return get_session().query(Place).filter_by(parent=None).one()
 
 
@@ -249,7 +250,7 @@ class Round(Base):
     seq = Column(Integer, nullable=False)
     part = Column(Integer, nullable=False)
     level = Column(Integer, nullable=False)
-    code = Column(String(255), nullable=False, server_default=text("''::text"))
+    code = Column(String(255), nullable=False)
     name = Column(String(255), nullable=False)
     state = Column(Enum(RoundState, name='round_state'), nullable=False, server_default=text("'preparing'::round_state"))
     tasks_file = Column(String(255))
@@ -269,7 +270,7 @@ class Round(Base):
 
     def round_code_short(self):
         """ Pro samostatné kolo ekvivalentní s `round_code()`, pro skupinu kol společná část kódu. """
-        return f"{self.year}-{self.category}-{self.code or self.seq}"
+        return f"{self.year}-{self.category}-{self.code}"
 
     def part_code(self):
         return chr(ord('a') + self.part - 1) if self.part > 0 else ""
@@ -317,6 +318,25 @@ class Round(Base):
         return " ".join(times)
 
 
+def find_master_round(year: Optional[int], category: Optional[str], code: str) -> Round:
+    if not year:
+        raise mo.CheckError('Neuveden ročník pro nalezení kola')
+    if not category:
+        raise mo.CheckError('Neuvedena kategorie pro nalezení kola')
+
+    r = (
+        get_session().query(Round)
+        .filter_by(year=year, category=category, code=code)
+        .filter(Round.master_round_id == Round.round_id)
+        .all()
+    )
+    if len(r) < 1:
+        raise mo.CheckError(f'Kolo {year}-{category}-{code} nenalezeno')
+    if len(r) > 1:
+        raise mo.CheckError(f'Kolo {year}-{category}-{code} nelze určit jednoznačně')
+    return r[0]
+
+
 class User(Base):
     __tablename__ = 'users'
 
@@ -575,10 +595,10 @@ class UserRole(Base):
         # Některé role mají omezení na úroveň hierarchie.
         level = self.place.level if self.place else -1
         rt = self.role
-        if not (rt == RoleType.garant and level <= 0
-                or rt == RoleType.garant_kraj and level == 1
-                or rt == RoleType.garant_okres and level == 2
-                or rt == RoleType.garant_skola and level >= 3):
+        if (rt == RoleType.garant and not level <= 0
+                or rt == RoleType.garant_kraj and not level == 1
+                or rt == RoleType.garant_okres and not level == 2
+                or rt == RoleType.garant_skola and not level >= 3):
             return False
 
         return True
diff --git a/mo/imports.py b/mo/imports.py
index 8e894710bdca8d88b1a5d845b368d1d40a2c33e0..05f7508c5088f27e537f99b41781820b304bade0 100644
--- a/mo/imports.py
+++ b/mo/imports.py
@@ -5,9 +5,8 @@ import io
 import re
 from sqlalchemy import and_
 from sqlalchemy.orm import joinedload, Query
-from typing import List, Optional, Any, Dict, Type, Union, Set
+from typing import List, Optional, Any, Dict, Type, Union
 
-import mo.config as config
 import mo.csv
 from mo.csv import FileFormat, MissingHeaderError
 import mo.db as db
@@ -19,24 +18,6 @@ from mo.util import logger
 from mo.util_format import format_decimal
 
 
-class ImportType(db.MOEnum):
-    participants = auto()
-    proctors = auto()
-    judges = auto()
-    points = auto()
-
-    def friendly_name(self) -> str:
-        return import_type_names[self]
-
-
-import_type_names = {
-    ImportType.participants.name: 'účastníci',
-    ImportType.proctors.name: 'dozor',
-    ImportType.judges.name: 'opravovatelé',
-    ImportType.points.name: 'body',
-}
-
-
 class Import:
     # Výsledek importu
     errors: List[str]
@@ -49,22 +30,25 @@ class Import:
     cnt_set_points: int = 0
     cnt_add_sols: int = 0
     cnt_del_sols: int = 0
+    cnt_change_user_to_org: int = 0  # pro Import orgů: Počet provedených/požadovaných změn účastnka na orga
 
     # Veřejné vlastnosti importu
     template_basename: str = "sablona"
+    fmt: FileFormat
 
     # Interní: Co a jak zrovna importujeme
     user: db.User
-    round: Optional[db.Round]
-    contest: Optional[db.Contest]
-    only_region: Optional[db.Place]
-    task: Optional[db.Task]         # pro Import bodů
-    allow_add_del: bool             # pro Import bodů: je povoleno zakládat/mazat řešení
-    fmt: FileFormat
+    round: Optional[db.Round] = None
+    contest: Optional[db.Contest] = None
+    only_region: Optional[db.Place] = None
+    task: Optional[db.Task] = None        # pro Import bodů
+    default_place: Optional[db.Place] = None
+    category: Optional[str] = None
     row_class: Type[mo.csv.Row]
     row_example: mo.csv.Row
     log_msg_prefix: str
     log_details: Any
+    allow_change_user_to_org: bool = False  # pro Import orgů: je povoleno vyrobit orga z účastníka
 
     # Interní: Stav importu
     place_cache: Dict[str, db.Place]
@@ -74,17 +58,14 @@ class Import:
     line_number: int = 0
     row_name: Optional[str] = None
 
-    def __init__(self):
+    def __init__(self, user: db.User):
         self.errors = []
         self.warnings = []
-        self.rr = None
         self.place_cache = {}
         self.school_place_cache = {}
         self.new_user_ids = []
-
-    def setup(self):
-        # Definováno odvozenými třídami
-        assert NotImplementedError()
+        self.gatekeeper = mo.rights.Gatekeeper(user)
+        self.user = user
 
     def error(self, msg: str) -> Any:
         if self.line_number > 0:
@@ -127,9 +108,7 @@ class Import:
 
         return name
 
-    def check_rights(self, place: db.Place) -> bool:
-        round = self.round
-        assert round is not None
+    def check_rights(self, round: db.Round, place: db.Place) -> bool:
         rights = self.gatekeeper.rights_for(place, round.year, round.category, round.seq)
         return rights.have_right(mo.rights.Right.manage_contest)
 
@@ -142,15 +121,18 @@ class Import:
 
         place = db.get_place_by_code(kod)
         if not place:
-            return self.error(f'{what.title()} s kódem "{kod}" neexistuje'+
-                    ('. Nechybí vám # na začátku?' if re.fullmatch(r'\d+', kod) else ''))
-
-        if not self.check_rights(place):
-            return self.error(f'Nemáte práva na správu soutěže {place.name_locative()}')
+            return self.error(f'{what.title()} s kódem "{kod}" neexistuje' +
+                              ('. Nechybí vám # na začátku?' if re.fullmatch(r'\d+', kod) else ''))
 
         self.place_cache[kod] = place
         return place
 
+    def parse_role(self, name: str) -> Optional[db.RoleType]:
+        if name not in db.RoleType.__members__:
+            return self.error(f"Role {name} neexistuje. Podívejte se do manuálu na existující role.")
+
+        return db.RoleType[name]
+
     def parse_school(self, kod: str) -> Optional[db.Place]:
         if kod in self.school_place_cache:
             return self.school_place_cache[kod]
@@ -190,9 +172,29 @@ class Import:
 
         return r
 
+    def parse_category(self, kategorie: Optional[str]) -> Optional[str]:
+        if not kategorie:
+            return None
+        kategorie = kategorie.upper()
+        if not (kategorie[0].isalpha() and kategorie.isalnum() and len(kategorie) <= 2):
+            return self.error(f"Zadána chybná kategorie {kategorie}")
+        return kategorie
+
+    def parse_master_round(self, year: Optional[int], category: Optional[str], code: str) -> Optional[db.Round]:
+        try:
+            return mo.db.find_master_round(year, category, code)
+        except mo.CheckError as e:
+            return self.error(str(e))
+
+
     def find_or_create_user(self, email: str, krestni: Optional[str], prijmeni: Optional[str], is_org: bool) -> Optional[db.User]:
         try:
-            user, is_new = mo.users.find_or_create_user(email, krestni, prijmeni, is_org, reason='import')
+            try:
+                user, is_new, is_user_to_org = mo.users.find_or_create_user(email, krestni, prijmeni, is_org, allow_change_user_to_org=self.allow_change_user_to_org, reason='import')
+                self.cnt_change_user_to_org += is_user_to_org
+            except mo.users.CheckErrorOrgIsUser as e:
+                self.cnt_change_user_to_org += 1
+                raise mo.CheckError(str(e) + " Změnu můžete povolit ve formuláři.")
         except mo.CheckError as e:
             return self.error(str(e))
         if is_new:
@@ -239,8 +241,8 @@ class Import:
             return False
         return True
 
-    def obtain_contest(self, oblast: Optional[db.Place], allow_none: bool = False):
-        assert self.round
+    def obtain_contest(self, round: db.Round, oblast: Optional[db.Place], allow_none: bool = False) -> Optional[db.Contest]:
+        contest: Optional[db.Contest]
         if oblast is not None and not self.place_is_allowed(oblast):
             return self.error('Oblast neodpovídá té, do které se importuje')
         if self.contest:
@@ -253,29 +255,45 @@ class Import:
                 if not allow_none:
                     self.error('Je nutné uvést kód oblasti')
                 return None
-            contest = db.get_session().query(db.Contest).filter_by(round=self.round, place=oblast).one_or_none()
+            contest = db.get_session().query(db.Contest).filter_by(round=round, place=oblast).one_or_none()
             if contest is None:
                 return self.error('V uvedené oblasti toto kolo neprobíhá')
 
         return contest
 
-    def add_role(self, user: db.User, place: db.Place, role: db.RoleType):
+    def add_role(self, user: db.User, role: db.RoleType, place: Optional[db.Place], year: Optional[int], category: Optional[str], round: Optional[db.Round]):
         sess = db.get_session()
-        round = self.round
-        assert round is not None
+
+        if year and round and round.year != year:
+            return self.error('Ročník neodpovídá zadanému kolu.')
+        if category and round and round.category != category:
+            return self.error('Kategorie neodpovídá zadanému kolu.')
+        seq = None
+        if round:
+            category = round.category
+            year = round.year
+            seq = round.seq
+
         if (sess.query(db.UserRole)
                 .filter_by(user=user, place=place, role=role,
-                           category=round.category, year=round.year, seq=round.seq)
+                           category=category, year=year, seq=seq)
                 .with_for_update()
                 .first()):
             pass
         else:
             ur = db.UserRole(user=user, place=place, role=role,
-                             category=round.category, year=round.year, seq=round.seq,
+                             category=category, year=year, seq=seq,
                              assigned_by_user=self.user)
+
+            if not (ur.is_legal()):
+                return self.error('Tato kombinace role a místa není povolena')
+
+            if not (self.gatekeeper.can_set_role(ur)):
+                return self.error(f'Roli "{ur}" nelze přidělit, není podmnožinou žádné vaší role')
+
             sess.add(ur)
             sess.flush()
-            logger.info(f'Import: {role.name.title()} user=#{user.user_id} place=#{place.place_id} user_role=#{ur.user_role_id}')
+            logger.info(f'Import: {role.name.title()} user=#{user.user_id} place=#{ place.place_id if place else "null" } user_role=#{ur.user_role_id}')
             mo.util.log(
                 type=db.LogType.user_role,
                 what=ur.user_role_id,
@@ -283,11 +301,11 @@ class Import:
             )
             self.cnt_new_roles += 1
 
-    def import_row(self, r: mo.csv.Row):
+    def import_row(self, r: mo.csv.Row) -> None:
         # Definováno odvozenými třídami
         assert NotImplementedError()
 
-    def log_start(self, path):
+    def log_start(self, path) -> None:
         args = [f'user=#{self.user.user_id}', f'fmt={self.fmt.name}']
         if self.round is not None:
             args.append(f'round=#{self.round.round_id}')
@@ -300,7 +318,7 @@ class Import:
 
         logger.info('Import: %s ze souboru %s: %s', self.log_msg_prefix, path, " ".join(args))
 
-    def log_end(self):
+    def log_end(self) -> None:
         args = [f'rows=#{self.cnt_rows}']
         for key, val in [
             ('users', self.cnt_new_users),
@@ -332,7 +350,8 @@ class Import:
                 details=details,
             )
         else:
-            assert False
+            pass
+            # TODO
 
     def check_utf8(self, path: str) -> bool:
         # Není pěkné, že soubory čteme dvakrát, ale ve srovnání s pasekou, kterou by
@@ -347,8 +366,8 @@ class Import:
 
     def get_row_name(self, row: mo.csv.Row) -> Optional[str]:
         if hasattr(row, 'email'):
-            return row.email # type: ignore
-                # čtení prvku potomka
+            # čtení prvku potomka
+            return row.email  # type: ignore
         return None
 
     def generic_import(self, path: str) -> bool:
@@ -382,7 +401,7 @@ class Import:
 
         return len(self.errors) == 0
 
-    def notify_users(self):
+    def notify_users(self) -> None:
         # Projde všechny uživatele a těm, kteří ještě nemají nastavené heslo,
         # ani nepožádali o jeho reset, pošle mail s odkazem na reset. Každého
         # uživatele zpracováváme ve zvlášť transakci, aby se po chybě neztratila
@@ -417,6 +436,9 @@ class Import:
         self.notify_users()
         return True
 
+    def get_after_import_message(self) -> str:
+        return "Import proveden."
+
 
 @dataclass
 class ContestImportRow(mo.csv.Row):
@@ -444,11 +466,24 @@ class ContestImport(Import):
     log_details = {'action': 'import'}
     template_basename = 'sablona-ucast'
 
-    def setup(self):
-        assert self.round is not None
+    def __init__(
+         self,
+         user: db.User,
+         round: db.Round,
+         contest: Optional[db.Contest] = None,
+         only_region: Optional[db.Place] = None,
+         default_place: Optional[db.Place] = None
+    ):
+        super().__init__(user)
+        self.user = user
+        self.round = round
+        self.contest = contest
+        self.only_region = only_region
+        self.default_place = default_place
         assert not self.round.is_subround()
 
-    def import_row(self, r: mo.csv.Row):
+    def import_row(self, r: mo.csv.Row) -> None:
+        assert self.round
         assert isinstance(r, ContestImportRow)
         num_prev_errs = len(self.errors)
         email = self.parse_email(r.email)
@@ -458,7 +493,12 @@ class ContestImport(Import):
         rocnik = self.parse_grade(r.rocnik, (school_place.school if school_place else None)) if r.rocnik else None
         rok_naroz = self.parse_born(r.rok_naroz) if r.rok_naroz else None
         misto = self.parse_opt_place(r.kod_mista, 'místo')
+        if misto and not self.check_rights(self.round, misto):
+            return self.error(f'Nemáte práva na správu soutěže {misto.name_locative()}')
+
         oblast = self.parse_opt_place(r.kod_oblasti, 'oblast')
+        if oblast and not self.check_rights(self.round, oblast):
+            return self.error(f'Nemáte práva na správu soutěže {oblast.name_locative()}')
 
         if (len(self.errors) > num_prev_errs
                 or email is None):
@@ -468,113 +508,130 @@ class ContestImport(Import):
         if user is None:
             return
 
-        part = self.find_or_create_participant(user, config.CURRENT_YEAR, school_place.place_id if school_place else None, rok_naroz, rocnik)
+        part = self.find_or_create_participant(user, self.round.year, school_place.place_id if school_place else None, rok_naroz, rocnik)
         if part is None:
             return
 
-        contest = self.obtain_contest(oblast)
+        contest = self.obtain_contest(self.round, oblast)
         if contest is None:
             return
 
         self.find_or_create_participation(user, contest, misto)
 
+    def get_after_import_message(self) -> str:
+        return f'Importováno ({self.cnt_rows} řádků, založeno {self.cnt_new_users} uživatelů, {self.cnt_new_participations} účastí, {self.cnt_new_roles} rolí)'
+
 
 @dataclass
-class ProctorImportRow(mo.csv.Row):
+class OrgsImportRow(mo.csv.Row):
     email: str = ""
     krestni: str = ""
     prijmeni: str = ""
-    kod_mista: str = ""
+    kod_oblasti: str = ""
+    role: str = ""
 
 
-class ProctorImport(Import):
-    row_class = ProctorImportRow
-    row_example = ProctorImportRow(
+class OrgsImport(Import):
+    row_class = OrgsImportRow
+    row_example = OrgsImportRow(
         email='nekdo@example.org',
         krestni='Pokusný',
         prijmeni='Králík',
-        kod_mista='#3333',
+        kod_oblasti='#3333',
+        role='dozor',
     )
-    log_msg_prefix = 'Dozor'
-    log_details = {'action': 'import-proctors'}
-    template_basename = 'sablona-dozor'
-
-    def setup(self):
-        assert self.round is not None
+    log_msg_prefix = 'Organizátoři'
+    log_details = {'action': 'import-orgs'}
+    template_basename = 'sablona-organizatori'
+
+    default_cat: Optional[str]
+    default_code: Optional[str]
+
+    def __init__(
+        self,
+        user: db.User,
+        round: Optional[db.Round] = None,  # Prázdné může být pouze když se jedná o GlobalOrgsImport
+        contest: Optional[db.Contest] = None,
+        only_region: Optional[db.Place] = None,
+        allow_change_user_to_org: bool = False,
+        default_place: Optional[db.Place] = None,
+        default_cat: Optional[str] = None,
+        default_code: Optional[str] = None,
+        year: Optional[int] = None
+    ):
+        super().__init__(user)
+        self.round = round
+        self.contest = contest
+        self.only_region = only_region
+        self.default_place = default_place
+        self.default_cat = self.parse_category(default_cat)
+        self.default_code = default_code
+        self.default_place = default_place
+        self.allow_change_user_to_org = allow_change_user_to_org
+        self.root_place = db.get_root_place()
+        self.year = round.year if round else year
+        assert hasattr(self.row_class, "kolo") or self.round
 
-    def import_row(self, r: mo.csv.Row):
-        assert isinstance(r, ProctorImportRow)
+    def import_row(self, r: mo.csv.Row) -> None:
+        assert isinstance(r, OrgsImportRow) or isinstance(r, GlobalOrgsImportRow)
         num_prev_errs = len(self.errors)
         email = self.parse_email(r.email)
         krestni = self.parse_name(r.krestni)
         prijmeni = self.parse_name(r.prijmeni)
-        misto = self.parse_opt_place(r.kod_mista, 'místo')
+        role = self.parse_role(r.role)
+        kat: str = getattr(r, "kategorie", "")
+        if kat:
+            kategorie = self.parse_category(kat)
+        else:
+            kategorie = self.default_cat
+        code = getattr(r, "kolo", "") or self.default_code
+        if code:
+            kolo = self.parse_master_round(self.year, kategorie, code)
+            if not kolo:
+                return
+        else:
+            kolo = self.round
 
-        if misto is None:
-            return self.error('Kód místa je povinné uvést')
+        oblast = self.parse_opt_place(r.kod_oblasti, 'oblast')
+        if oblast is None:
+            oblast = self.default_place
+        if oblast is None:
+            return self.error('Chybí oblast.')
 
         if (len(self.errors) > num_prev_errs
                 or email is None
                 or krestni is None
-                or prijmeni is None):
+                or prijmeni is None
+                or role is None):
             return
 
         user = self.find_or_create_user(email, krestni, prijmeni, is_org=True)
         if user is None:
             return
 
-        self.add_role(user, misto, db.RoleType.dozor)
+        self.add_role(user, role, oblast, self.year, kategorie, kolo)
+
+    def get_after_import_message(self) -> str:
+        return f'Importováno ({self.cnt_rows} řádků, založeno {self.cnt_new_users} uživatelů, {self.cnt_new_roles} rolí)'
 
 
 @dataclass
-class JudgeImportRow(mo.csv.Row):
-    email: str = ""
-    krestni: str = ""
-    prijmeni: str = ""
-    kod_oblasti: str = ""
+class GlobalOrgsImportRow(OrgsImportRow):
+    kategorie: str = ""
+    kolo: str = ""
 
 
-class JudgeImport(Import):
-    row_class = JudgeImportRow
-    row_example = JudgeImportRow(
+class GlobalOrgsImport(OrgsImport):
+    row_class = GlobalOrgsImportRow
+    row_example = GlobalOrgsImportRow(
         email='nekdo@example.org',
         krestni='Pokusný',
         prijmeni='Králík',
-        kod_oblasti='B',
+        kod_oblasti='#3333',
+        role='dozor',
+        kategorie='Z',
+        kolo='',
     )
-    log_msg_prefix = 'Opravovatelé'
-    log_details = {'action': 'import-judges'}
-    template_basename = 'sablona-oprav'
-    root_place: db.Place
-
-    def setup(self):
-        assert self.round is not None
-        self.root_place = db.get_root_place()
-
-    def import_row(self, r: mo.csv.Row):
-        assert isinstance(r, JudgeImportRow)
-        num_prev_errs = len(self.errors)
-        email = self.parse_email(r.email)
-        krestni = self.parse_name(r.krestni)
-        prijmeni = self.parse_name(r.prijmeni)
-        oblast = self.parse_opt_place(r.kod_oblasti, 'oblast')
-
-        if (len(self.errors) > num_prev_errs
-                or email is None
-                or krestni is None
-                or prijmeni is None):
-            return
-
-        user = self.find_or_create_user(email, krestni, prijmeni, is_org=True)
-        if user is None:
-            return
-
-        contest = self.obtain_contest(oblast, allow_none=True)
-        place = contest.place if contest else self.root_place
-        if not self.check_rights(place):
-            return self.error(f'Nemáte práva na správu soutěže {place.name_locative()}')
-
-        self.add_role(user, place, db.RoleType.opravovatel)
 
 
 @dataclass
@@ -589,7 +646,23 @@ class PointsImport(Import):
     row_class = PointsImportRow
     log_msg_prefix = 'Body'
 
-    def setup(self):
+    allow_add_del: bool             # je povoleno zakládat/mazat řešení
+
+    def __init__(
+        self,
+        user: db.User,
+        round: db.Round,
+        task: db.Task,
+        contest: Optional[db.Contest] = None,
+        only_region: Optional[db.Place] = None,
+        allow_add_del: bool = False,
+    ):
+        super().__init__(user)
+        self.round = round
+        self.contest = contest
+        self.task = task
+        self.only_region = only_region
+        self.allow_add_del = allow_add_del
         assert self.round is not None
         assert self.task is not None
         self.log_details = {'action': 'import-points', 'task': self.task.code}
@@ -613,7 +686,7 @@ class PointsImport(Import):
 
         return query
 
-    def import_row(self, r: mo.csv.Row):
+    def import_row(self, r: mo.csv.Row) -> None:
         assert isinstance(r, PointsImportRow)
         num_prev_errs = len(self.errors)
         user_id = self.parse_user_id(r.user_id)
@@ -726,35 +799,5 @@ class PointsImport(Import):
         mo.csv.write(file=out, fmt=self.fmt, row_class=self.row_class, rows=rows)
         return out.getvalue()
 
-
-def create_import(user: db.User,
-                  type: ImportType,
-                  fmt: FileFormat,
-                  round: Optional[db.Round] = None,
-                  contest: Optional[db.Contest] = None,
-                  only_region: Optional[db.Place] = None,
-                  task: Optional[db.Task] = None,
-                  allow_add_del: bool = False):
-    imp: Import
-    if type == ImportType.participants:
-        imp = ContestImport()
-    elif type == ImportType.proctors:
-        imp = ProctorImport()
-    elif type == ImportType.judges:
-        imp = JudgeImport()
-    elif type == ImportType.points:
-        imp = PointsImport()
-    else:
-        assert False, "Neznámý typ importu"
-
-    imp.user = user
-    imp.round = round
-    imp.contest = contest
-    imp.only_region = only_region
-    imp.task = task
-    imp.allow_add_del = allow_add_del
-    imp.fmt = fmt
-    imp.gatekeeper = mo.rights.Gatekeeper(user)
-    imp.setup()
-
-    return imp
+    def get_after_import_message(self) -> str:
+        return f'Importováno ({self.cnt_rows} řádků, {self.cnt_set_points} řešení přebodováno, {self.cnt_add_sols} založeno a {self.cnt_del_sols} smazáno)'
diff --git a/mo/rights.py b/mo/rights.py
index b0e8878a0244ec1f2db399956c074892a8e025fc..5bca35d5b5d59b3cfe8ac8935f83d93d6ecb378d 100644
--- a/mo/rights.py
+++ b/mo/rights.py
@@ -358,6 +358,7 @@ class Gatekeeper:
     roles: List[db.UserRole]
     parent_cache: Dict[int, List[db.Place]]
     rights_cache: Dict[Tuple[Optional[int], Optional[int], Optional[str], Optional[int], Optional[db.RoleType]], Rights]
+    root_place: Optional[db.Place]
 
     def __init__(self, user: db.User):
         self.user = user
@@ -365,6 +366,12 @@ class Gatekeeper:
         assert user.is_org or user.is_admin
         self.parent_cache = {}
         self.rights_cache = {}
+        self.root_place = None
+
+    def get_root_place(self) -> db.Place:
+        if not self.root_place:
+            self.root_place = db.get_root_place()
+        return self.root_place
 
     def get_ancestors(self, place: db.Place) -> List[db.Place]:
         pid = place.place_id
@@ -384,7 +391,7 @@ class Gatekeeper:
         """Posbírá role a práva, která se vztahují k danému místu (možno i tranzitivně) a soutěži.
         Pokud place=None, omezení role na místo se nebere v úvahu.
         Pokud year==None, vyhovují role s libovolným ročníkem; pokud year=0, vyhovují jen ty s neuvedeným ročníkem.
-        Podobně cat a seq.
+        Podobně cat a seq (u cat vyhoví jen ty s neuvedenou kategorií, když cat="").
         Pokud min_role!=None, tak se uvažují jen role, které jsou v hierarchii alespoň na úrovni min_role."""
 
         cache_key = (place.place_id if place is not None else None, year, cat, seq, min_role)
@@ -425,7 +432,7 @@ class Gatekeeper:
         elif for_place:
             place = for_place
         else:
-            place = db.get_root_place()
+            place = self.get_root_place()
         rights = RoundRights()
         rights.round = round
         rights._clone_from(self.rights_for(
diff --git a/mo/users.py b/mo/users.py
index 6bbe2d245752129e42b76ad6be6c77ae4328dfe3..851ddf09c345e6b3e4ca7a7b16befb54a5cdb829 100644
--- a/mo/users.py
+++ b/mo/users.py
@@ -52,10 +52,34 @@ def validate_and_find_school(kod: str) -> db.Place:
     return place
 
 
-def find_or_create_user(email: str, krestni: Optional[str], prijmeni: Optional[str], is_org: bool, reason: str) -> Tuple[db.User, bool]:
+class CheckErrorOrgIsUser(mo.CheckError):
+    """Při požadavku na orga nalezen uživatel nebo opačně."""
+    pass
+
+
+def change_user_to_org(user, reason: str):
+    if (db.get_session().query(db.Participation, db.Contest, db.Round)
+            .select_from(db.Participation)
+            .join(db.Contest)
+            .filter(db.Participation.user == user)
+            .filter(db.Round.year == config.CURRENT_YEAR)
+            .count()):
+        raise mo.CheckError("Převedení účastníka na organizátora se nezdařilo, protože se účastní aktuálního ročníku. Kontaktujte prosím správce.")
+    user.is_org = True
+    logger.info(f'{reason.title()}: Změna stavu uživatele user=#{user.user_id} na organizátora')
+    changes = db.get_object_changes(user)
+    mo.util.log(
+        type=db.LogType.user,
+        what=user.user_id,
+        details={'action': 'user-change-is-org', 'reason': reason, 'changes': changes},
+    )
+
+
+def find_or_create_user(email: str, krestni: Optional[str], prijmeni: Optional[str], is_org: bool, reason: str, allow_change_user_to_org=False) -> Tuple[db.User, bool, bool]:
     sess = db.get_session()
     user = sess.query(db.User).filter_by(email=email).one_or_none()
     is_new = user is None
+    is_change_user_to_org = False
     if user is None:  # HACK: Podmínku je nutné zapsat znovu místo užití is_new, jinak si s tím mypy neporadí
         if not krestni or not prijmeni:
             raise mo.CheckError('Osoba s daným emailem zatím neexistuje, je nutné uvést její jméno.')
@@ -73,10 +97,14 @@ def find_or_create_user(email: str, krestni: Optional[str], prijmeni: Optional[s
             raise mo.CheckError(f'Osoba již registrována s odlišným jménem {user.full_name()}')
         if (user.is_admin or user.is_org) != is_org:
             if is_org:
-                raise mo.CheckError('Nelze předefinovat účastníka na organizátora')
+                if allow_change_user_to_org:
+                    change_user_to_org(user, reason)
+                    is_change_user_to_org = True
+                else:
+                    raise CheckErrorOrgIsUser('Nelze předefinovat účastníka na organizátora.')
             else:
-                raise mo.CheckError('Nelze předefinovat organizátora na účastníka')
-    return user, is_new
+                raise mo.CheckError('Nelze předefinovat organizátora na účastníka.')
+    return user, is_new, is_change_user_to_org
 
 
 def find_or_create_participant(user: db.User, year: int, school_id: Optional[int], birth_year: Optional[int], grade: Optional[str], reason: str) -> Tuple[db.Participant, bool]:
diff --git a/mo/web/imports.py b/mo/web/imports.py
new file mode 100644
index 0000000000000000000000000000000000000000..c9a84cb76cf63223e235ddf0fda24840fccdb5e3
--- /dev/null
+++ b/mo/web/imports.py
@@ -0,0 +1,61 @@
+from flask import render_template, g, redirect, url_for, flash, request
+from flask_wtf import FlaskForm
+import flask_wtf.file
+import wtforms
+import wtforms.validators as validators
+from typing import Optional
+
+import mo
+from mo.imports import Import, FileFormat
+from mo.web import app
+
+
+class ImportForm(FlaskForm):
+    file = flask_wtf.file.FileField("Soubor", render_kw={'autofocus': True})
+    fmt = wtforms.SelectField(
+        "Formát souboru",
+        choices=FileFormat.choices(), coerce=FileFormat.coerce,
+        default=FileFormat.cs_csv,
+    )
+    submit = wtforms.SubmitField('Importovat')
+    get_template = wtforms.SubmitField('Stáhnout šablonu')
+
+
+def generic_import_page(form: ImportForm, imp: Optional[Import], redirect_url: str, template: str = 'org_generic_import.html', **kwargs):
+    # Případné další parametry (**kwargs) jsou předávány generování stránky
+    errs = []
+    warnings = []
+    if imp:
+        fmt = form.fmt.data
+        imp.fmt = fmt
+        if form.submit.data:
+            if form.file.data is not None:
+                file = form.file.data.stream
+                import_tmp = mo.util.link_to_dir(file.name, mo.util.data_dir('imports'), suffix='.csv')
+
+                if imp.run(import_tmp):
+                    if imp.cnt_rows == 0:
+                        flash('Soubor neobsahoval žádné řádky s daty', 'danger')
+                    else:
+                        flash(imp.get_after_import_message(), 'success')
+                        return redirect(redirect_url)
+                else:
+                    errs = imp.errors
+                    warnings = imp.warnings
+            else:
+                flash('Vyberte si prosím soubor', 'danger')
+        elif form.get_template.data:
+            out = imp.get_template()
+            resp = app.make_response(out)
+            resp.content_type = fmt.get_content_type()
+            resp.headers.add('Content-Disposition', 'attachment; filename=OSMO-' + imp.template_basename + '.' + fmt.get_extension())
+            return resp
+
+    return render_template(
+        template,
+        errs=errs,
+        warnings=warnings,
+        form=form,
+        imp=imp,
+        **kwargs
+    )
diff --git a/mo/web/org_contest.py b/mo/web/org_contest.py
index a68bd79a44bb5417931c6efb4075cec51cc8f003..823c12bbf2f7782ef0802b515c3b98c8c2234900 100644
--- a/mo/web/org_contest.py
+++ b/mo/web/org_contest.py
@@ -7,7 +7,6 @@ from sqlalchemy import func, and_, select
 from sqlalchemy.orm import joinedload, aliased
 from sqlalchemy.orm.query import Query
 from sqlalchemy.dialects.postgresql import insert as pgsql_insert
-import sqlalchemy.sql.schema
 from typing import Any, List, Tuple, Optional, Dict
 import urllib.parse
 import werkzeug.exceptions
@@ -16,16 +15,16 @@ import wtforms.validators as validators
 from wtforms.widgets.html5 import NumberInput
 
 import mo
-from mo.csv import FileFormat
 import mo.config as config
 import mo.db as db
-from mo.imports import ImportType, create_import
+from mo.imports import PointsImport, ContestImport, OrgsImport
 import mo.jobs.submit
 from mo.rights import Right, RoundRights
 import mo.util
 from mo.util_format import inflect_number, inflect_by_number
 from mo.web import app
 import mo.web.fields as mo_fields
+from mo.web.imports import ImportForm, generic_import_page
 import mo.web.util
 from mo.web.util import PagerForm
 from mo.web.table import CellCheckbox, Table, Row, Column, cell_pion_link, cell_place_link, cell_email_link_flags
@@ -413,70 +412,73 @@ def org_contest(ct_id: int, site_id: Optional[int] = None):
     )
 
 
-class ImportForm(FlaskForm):
-    file = flask_wtf.file.FileField("Soubor", render_kw={'autofocus': True})
-    typ = wtforms.SelectField(
-        "Typ dat",
-        choices=[(x.name, x.friendly_name()) for x in (ImportType.participants, ImportType.proctors, ImportType.judges)],
-        coerce=ImportType.coerce,
-        default=ImportType.participants,
-    )
-    fmt = wtforms.SelectField(
-        "Formát souboru",
-        choices=FileFormat.choices(), coerce=FileFormat.coerce,
-        default=FileFormat.cs_csv,
-    )
-    submit = wtforms.SubmitField('Importovat')
-    get_template = wtforms.SubmitField('Stáhnout šablonu')
+class ContestantImportForm(ImportForm):
+    pass
 
 
-@app.route('/org/contest/c/<int:ct_id>/import', methods=('GET', 'POST'))
-@app.route('/org/contest/r/<int:round_id>/import', methods=('GET', 'POST'))
-@app.route('/org/contest/r/<int:round_id>/h/<int:hier_id>/import', methods=('GET', 'POST'))
-def org_generic_import(round_id: Optional[int] = None, hier_id: Optional[int] = None, ct_id: Optional[int] = None):
+@app.route('/org/contest/c/<int:ct_id>/import-contestant', methods=('GET', 'POST'))
+@app.route('/org/contest/r/<int:round_id>/import-contestant', methods=('GET', 'POST'))
+@app.route('/org/contest/r/<int:round_id>/h/<int:hier_id>/import-contestant', methods=('GET', 'POST'))
+def org_import_user(round_id: Optional[int] = None, hier_id: Optional[int] = None, ct_id: Optional[int] = None):
     ctx = get_context(round_id=round_id, hier_id=hier_id, ct_id=ct_id, right_needed=Right.manage_contest)
     round, contest = ctx.master_round, ctx.master_contest
 
-    form = ImportForm()
-    errs = []
-    warnings = []
-    if form.validate_on_submit():
-        fmt = form.fmt.data
-        imp = create_import(user=g.user, type=form.typ.data, fmt=fmt, round=round, contest=contest, only_region=ctx.hier_place)
-        if form.submit.data:
-            if form.file.data is not None:
-                file = form.file.data.stream
-                import_tmp = mo.util.link_to_dir(file.name, mo.util.data_dir('imports'), suffix='.csv')
-
-                if imp.run(import_tmp):
-                    if imp.cnt_rows == 0:
-                        flash('Soubor neobsahoval žádné řádky s daty', 'danger')
-                    else:
-                        flash(f'Importováno ({imp.cnt_rows} řádků, založeno {imp.cnt_new_users} uživatelů, {imp.cnt_new_participations} účastí, {imp.cnt_new_roles} rolí)', 'success')
-                        return redirect(ctx.url_home())
-                else:
-                    errs = imp.errors
-                    warnings = imp.warnings
-            else:
-                flash('Vyberte si prosím soubor', 'danger')
-        elif form.get_template.data:
-            out = imp.get_template()
-            resp = app.make_response(out)
-            resp.content_type = fmt.get_content_type()
-            resp.headers.add('Content-Disposition', 'attachment; filename=OSMO-' + imp.template_basename + '.' + fmt.get_extension())
-            return resp
+    default_place = contest.place if contest else ctx.hier_place
 
-    return render_template(
-        'org_generic_import.html',
+    form = ContestantImportForm()
+    imp = None
+    if form.validate_on_submit():
+        imp = ContestImport(
+            user=g.user,
+            round=round,
+            contest=contest,
+            only_region=ctx.hier_place,
+            default_place=default_place,
+        )
+    return generic_import_page(
+        form, imp, ctx.url_home(),
+        template='org_contestants_import.html',
         ctx=ctx,
         contest=contest,
         round=round,
-        form=form,
-        errs=errs,
-        warnings=warnings
+        default_place=default_place
     )
 
 
+class OrgImportForm(ImportForm):
+    allow_change_user_to_org = wtforms.BooleanField("Povolit převádění účastníků na organizátory")
+
+
+@app.route('/org/contest/c/<int:ct_id>/import-org', methods=('GET', 'POST'))
+@app.route('/org/contest/r/<int:round_id>/import-org', methods=('GET', 'POST'))
+@app.route('/org/contest/r/<int:round_id>/h/<int:hier_id>/import-org', methods=('GET', 'POST'))
+def org_import_org(round_id: Optional[int] = None, hier_id: Optional[int] = None, ct_id: Optional[int] = None):
+    ctx = get_context(round_id=round_id, hier_id=hier_id, ct_id=ct_id, right_needed=Right.manage_contest)
+    round, contest = ctx.master_round, ctx.master_contest
+
+    default_place = contest.place if contest else ctx.hier_place
+
+    form = OrgImportForm()
+    imp = None
+    if form.validate_on_submit():
+        imp = OrgsImport(
+            user=g.user,
+            round=round,
+            contest=contest,
+            only_region=ctx.hier_place,
+            default_place=default_place,
+            allow_change_user_to_org=form.allow_change_user_to_org.data
+        )
+    return generic_import_page(
+        form, imp, ctx.url_home(),
+        template='org_orgs_import.html',
+        ctx=ctx,
+        contest=contest,
+        round=round,
+        default_place=default_place
+        )
+
+
 # URL je explicitně uvedeno v mo.email.contestant_list_url
 @app.route('/org/contest/c/<int:ct_id>/participants', methods=('GET', 'POST'))
 @app.route('/org/contest/c/<int:ct_id>/site/<int:site_id>/participants', methods=('GET', 'POST'))
@@ -1300,16 +1302,8 @@ def org_generic_batch_upload(task_id: int, round_id: Optional[int] = None, hier_
     )
 
 
-class BatchPointsForm(FlaskForm):
-    file = flask_wtf.file.FileField("Soubor", render_kw={'autofocus': True})
-    fmt = wtforms.SelectField(
-        "Formát souboru",
-        choices=FileFormat.choices(), coerce=FileFormat.coerce,
-        default=FileFormat.cs_csv,
-    )
+class BatchPointsForm(ImportForm):
     add_del_sols = wtforms.BooleanField('Zakládat / mazat řešení', description='Xyzzy')
-    submit = wtforms.SubmitField('Nahrát body')
-    get_template = wtforms.SubmitField('Stáhnout šablonu')
 
 
 @app.route('/org/contest/c/<int:ct_id>/task/<int:task_id>/batch-points', methods=('GET', 'POST'))
@@ -1318,46 +1312,20 @@ class BatchPointsForm(FlaskForm):
 def org_generic_batch_points(task_id: int, round_id: Optional[int] = None, hier_id: Optional[int] = None, ct_id: Optional[int] = None):
     ctx = get_context(round_id=round_id, hier_id=hier_id, ct_id=ct_id, task_id=task_id)
     round, hier_place, contest, task = ctx.round, ctx.hier_place, ctx.contest, ctx.task
+    assert task
 
     if not ctx.rights.can_edit_points():
         raise werkzeug.exceptions.Forbidden()
 
     form = BatchPointsForm()
-    errs = []
-    warnings = []
+    imp = None
     if form.validate_on_submit():
-        fmt = form.fmt.data
-        imp = create_import(user=g.user, type=ImportType.points, fmt=fmt, round=round, only_region=hier_place, contest=contest, task=task, allow_add_del=form.add_del_sols.data)
-        if form.submit.data:
-            if form.file.data is not None:
-                file = form.file.data.stream
-                import_tmp = mo.util.link_to_dir(file.name, mo.util.data_dir('imports'), suffix='.csv')
-
-                if imp.run(import_tmp):
-                    if imp.cnt_rows == 0:
-                        flash('Soubor neobsahoval žádné řádky s daty', 'danger')
-                    else:
-                        flash(f'Importováno ({imp.cnt_rows} řádků, {imp.cnt_set_points} řešení přebodováno, {imp.cnt_add_sols} založeno a {imp.cnt_del_sols} smazáno)', 'success')
-                        return redirect(ctx.url_home())
-                else:
-                    errs = imp.errors
-                    warnings = imp.warnings
-            else:
-                flash('Vyberte si prosím soubor', 'danger')
-        elif form.get_template.data:
-            out = imp.get_template()
-            resp = app.make_response(out)
-            resp.content_type = fmt.get_content_type()
-            resp.headers.add('Content-Disposition', 'attachment; filename=OSMO-' + imp.template_basename + '.' + fmt.get_extension())
-            return resp
-
-    return render_template(
-        'org_generic_batch_points.html',
+        imp = PointsImport(user=g.user, round=round, only_region=hier_place, contest=contest, task=task, allow_add_del=form.add_del_sols.data)
+    return generic_import_page(
+        form, imp, ctx.url_home(),
+        template='org_generic_batch_points.html',
         ctx=ctx,
-        round=round, contest=contest, task=task,
-        form=form,
-        errs=errs,
-        warnings=warnings
+        round=round, contest=contest, task=task
     )
 
 
@@ -1634,7 +1602,7 @@ def org_contest_add_user(ct_id: int, site_id: Optional[int] = None):
 
     if form.validate_on_submit():
         try:
-            user, is_new_user = mo.users.find_or_create_user(form.email.data, form.first_name.data, form.last_name.data, False, reason='web')
+            user, is_new_user, is_change_user_to_org = mo.users.find_or_create_user(form.email.data, form.first_name.data, form.last_name.data, False, reason='web')
             participant, is_new_participant = mo.users.find_or_create_participant(user, contest.round.year, form.school.get_place_id(), form.birth_year.data, form.grade.data, reason='web')
             participation, is_new_participation = mo.users.find_or_create_participation(user, contest, form.participation_place.get_place(), reason='web')
         except mo.CheckError as e:
diff --git a/mo/web/org_users.py b/mo/web/org_users.py
index 0f8b055ab82e8fef7d32c823f06ff306fb1af05e..3ef60af3fb1e5444181611c33489f5f1b213f65d 100644
--- a/mo/web/org_users.py
+++ b/mo/web/org_users.py
@@ -15,11 +15,13 @@ from wtforms.validators import Required
 import mo
 import mo.db as db
 import mo.email
+from mo.imports import GlobalOrgsImport
 from mo.rights import Right
 import mo.util
 import mo.users
 from mo.web import app
 import mo.web.fields as mo_fields
+from mo.web.imports import ImportForm, generic_import_page
 from mo.web.util import PagerForm
 
 
@@ -239,7 +241,7 @@ class FormAddRole(FlaskForm):
     place = mo_fields.Place()
     year = wtforms.IntegerField('Ročník', validators=[validators.Optional()])
     category = wtforms.StringField("Kategorie", validators=[validators.Length(max=2)], filters=[lambda x: x or None])
-    seq = wtforms.IntegerField("Kolo", validators=[validators.Optional()])
+    seq = wtforms.IntegerField("Kolo", render_kw={"placeholder": "Pořadí kola v kategorii"}, validators=[validators.Optional()])
 
     submit = wtforms.SubmitField('Přidat roli')
 
@@ -264,6 +266,10 @@ class ResendInviteForm(FlaskForm):
             flash('Tento uživatel už má účet aktivovaný.', 'danger')
 
 
+class UpgradeToOrgForm(FlaskForm):
+    upgrade = SubmitField()
+
+
 @app.route('/org/org/<int:id>/', methods=('GET', 'POST'))
 def org_org(id: int):
     sess = db.get_session()
@@ -371,6 +377,19 @@ def org_user(id: int):
             resend_invite_form.do(user)
             return redirect(url_for('org_user', id=id))
 
+    upgrade_form: Optional[UpgradeToOrgForm] = None
+    if rr.can_edit_user:
+        upgrade_form = UpgradeToOrgForm()
+        if upgrade_form.upgrade.data and upgrade_form.validate_on_submit():
+            try:
+                mo.users.change_user_to_org(user, reason='web')
+                sess.commit()
+                flash('Účet změněn na organizátorský.', 'success')
+                return redirect(url_for('org_org', id=user.user_id))
+            except mo.CheckError as e:
+                flash(str(e), 'danger')
+                return redirect(url_for('org_user', id=user.user_id))
+
     participants = sess.query(db.Participant).filter_by(user_id=user.user_id)
     participations = (
         sess.query(db.Participation, db.Contest, db.Round)
@@ -388,6 +407,7 @@ def org_user(id: int):
         can_incarnate=g.user.is_admin,
         participants=participants, participations=participations,
         resend_invite_form=resend_invite_form,
+        upgrade_form=upgrade_form,
     )
 
 
@@ -398,6 +418,7 @@ class UserEditForm(FlaskForm):
     note = wtforms.TextAreaField("Poznámka")
     is_test = wtforms.BooleanField("Testovací účet")
     allow_duplicate_name = wtforms.BooleanField("Přidat účet s duplicitním jménem")
+    allow_change_user_to_org = wtforms.BooleanField("Povolit převedení účastníka na organizátora")
     submit = wtforms.SubmitField("Uložit")
 
 
@@ -422,6 +443,7 @@ def org_user_edit(id: int):
 
     form = UserEditForm(obj=user)
     del form.allow_duplicate_name
+    del form.allow_change_user_to_org
     if (user.is_org or user.is_admin) and not g.user.is_admin:
         # emaily u organizátorů může editovat jen správce
         del form.email
@@ -471,12 +493,35 @@ def org_user_new():
     form = UserEditForm()
     form.submit.label.text = 'Vytvořit'
     is_duplicate_name = False
+    allow_change_user_to_org_show_field = False
     if form.validate_on_submit():
         check = True
 
-        if mo.users.user_by_email(form.email.data) is not None:
-            flash('Účet s daným e-mailem již existuje', 'danger')
-            check = False
+        old_user = mo.users.user_by_email(form.email.data)
+        if old_user is not None:
+            if is_org and not old_user.is_org:
+                allow_change_user_to_org_show_field = True
+                if form.allow_change_user_to_org.data:
+                    try:
+                        mo.users.find_or_create_user(
+                            email=form.email.data,
+                            krestni=form.first_name.data,
+                            prijmeni=form.last_name.data,
+                            is_org=True,
+                            reason="web",
+                            allow_change_user_to_org=True)
+                    except mo.CheckError as e:
+                        flash(str(e), 'danger')
+                        check = False
+                    if check:
+                        mo.db.get_session().commit()
+                        return redirect(url_for('org_org', id=old_user.user_id))
+                if check:
+                    flash('Účet s daným e-mailem již existuje. Převedení účastníka na organizátora můžete povolit ve formuláři.', 'danger')
+                    check = False
+            if check:
+                flash('Účet s daným e-mailem již existuje.', 'danger')
+                check = False
 
         if is_org:
             if (mo.db.get_session().query(db.User)
@@ -514,8 +559,11 @@ def org_user_new():
                 return redirect(url_for('org_org', id=new_user.user_id))
             return redirect(url_for('org_user', id=new_user.user_id))
 
-    if not is_duplicate_name:
+    if not is_duplicate_name and not form.allow_duplicate_name.data:
         del form.allow_duplicate_name
+
+    if not (is_org and allow_change_user_to_org_show_field):
+        del form.allow_change_user_to_org
     return render_template('org_user_new.html', form=form, is_org=is_org)
 
 
@@ -561,3 +609,26 @@ def org_user_participant_edit(user_id: int, year: int):
         return redirect(url_for('org_user', id=user_id))
 
     return render_template('org_user_participant_edit.html', user=user, year=year, form=form)
+
+
+class GlobalOrgsImportForm(ImportForm):
+    allow_change_user_to_org = wtforms.BooleanField("Povolit převádění účastníků na organizátory")
+    default_place = mo_fields.Place("Výchozí oblast (není-li v souboru uvedena)")
+    default_cat = wtforms.StringField("Výchozí kategorie (není-li v souboru uvedena)")
+    default_code = wtforms.StringField("Výchozí kód kola (není-li v souboru uveden)")
+    only_this_year = wtforms.BooleanField("Omezit práva na aktuální ročník", default=True)
+
+
+@app.route('/org/org/import', methods=('GET', 'POST'))
+def org_orgs_import():
+    form = GlobalOrgsImportForm()
+    imp = None
+    if form.validate_on_submit():
+        imp = GlobalOrgsImport(
+            g.user,
+            default_place=form.default_place.place,
+            default_cat=form.default_cat.data,
+            default_code=form.default_code.data,
+            year=mo.config.CURRENT_YEAR if form.only_this_year.data else None
+        )
+    return generic_import_page(form, imp, url_for('org_orgs_import'), template='org_global_orgs_import.html')
diff --git a/mo/web/templates/doc_garant.html b/mo/web/templates/doc_garant.html
index 493acc0d47d20bb843d653bfa9c2da08cdb65d44..6bd001cdf30f284174d52d4293c99fa2ed513170 100644
--- a/mo/web/templates/doc_garant.html
+++ b/mo/web/templates/doc_garant.html
@@ -34,6 +34,11 @@ oblasti (kraje, okresy, školy), kategorie či kola soutěže.
 	    i formulář na přidělování rolí.
 </ul>
 
+<p>Může se stát, že budoucí organizátor už má účastnický účet (je to bývalý účastník
+nebo si pomocí registrace účet založil sám). Pak je potřeba účet nejdřív převést
+na organizátorský. To jde udělat vyhledáním uživatele mezi účastníky a zmáčknutím
+tlačítka pro převod účtu.
+
 <h3>Hierarchie míst</h3>
 
 <p>Jednotlivé oblasti a školy, kde se soutěží, jsou zařazeny do hierarchie <b>míst</b>.
@@ -77,6 +82,7 @@ Držte se prosím následujících konvencí:
 	<li><b>ročník MO</b> (nyní 70)
 	<li><b>kategorie</b> (A, P, Z5, &hellip;)
 	<li><b>pořadí</b> v rámci kategorie (1, 2, &hellip;)
+	<li><b>kód</b> v rámci kategorie (zobrazuje se místo pořadí, třeba „S“ pro školní kolo)
 	<li><b>část</b> – u běžných kol 0, jinak viz níže
 	<li><b>úroveň</b> v hierarchii míst, na které se odehrává (to může být celá republika,
 		kraj, okres apod.). Odpovídá tomu, pro jaké oblasti se sestavují samostatné
@@ -93,17 +99,17 @@ Např. krajské kolo má samostatnou soutěž v každém kraji.
 <p>Soutěžní kolo se nachází v jednom z následujících stavů:
 
 <ul>
-	<li>připravuje se – kolo je zatím přístupné jenom organizátorům;
+	<li><b>připravuje se</b> – kolo je zatím přístupné jenom organizátorům;
 		účastníci vidí jen, že kolo existuje (pokud jsou do něj pozvaní),
 		a termín začátku soutěže.
-	<li>běží – účastníkům je dostupné zadání (po zadaném čase) a mohou odevzdávat,
+	<li><b>běží</b> – účastníkům je dostupné zadání (po zadaném čase) a mohou odevzdávat,
 		dozor také může odevzdávat. Opravovatelé si mohou průběžně prohlížet
 		odevzdané úlohy, ale ještě nemohou nic měnit.
-	<li>opravuje se – opravovatelé si mohou stahovat finální verzi řešení, nahrávat opravená
+	<li><b>opravuje se</b> – opravovatelé si mohou stahovat finální verzi řešení, nahrávat opravená
 		řešení a vyplňovat body a poznámky.
-	<li>ukončeno – opravená řešení, body a poznámky jsou dostupné účastníkům (vše pouze
+	<li><b>ukončeno</b> – opravená řešení, body a poznámky jsou dostupné účastníkům (vše pouze
 		ve finální verzi), opravovatelé už nemohou nic měnit.
-	<li>po oblastech – soutěž v každé oblasti si může nastavit svůj stav (viz níže)
+	<li><b>po oblastech</b> – soutěž v každé oblasti si může nastavit svůj stav (viz níže)
 </ul>
 
 <p>Omezení daná stavem soutěže neplatí pro garanty, ti mohou vždy všechno.
@@ -124,7 +130,24 @@ Každá oblast nyní bude ve stavu <em>běží</em> (zdědila předchozí nastav
 oblasti může podle potřeby přepínat do dalších stavů. Až budou všechny oblasti ve stavu <em>ukončeno</em>,
 celostátní garant kolo také přepne do <em>ukončeno.</em>
 
-<h3>Účastníci</h3>
+<h3>Registrace</h3>
+
+<p>Účastníci si mohou sami založit účet a pak se pomocí něj přihlásit do domácího kola.
+Přesněji řečeno každé kolo může mít nastaven jeden ze tří režimů registrace:
+
+<ul>
+	<li><b>účastníci sami</b> &ndash; účastník se registruje sám; používáme v kategorii P
+	<li><b>potvrzení organizátorem</b> &ndash; účastník se registruje sám, ale organizátor
+		musí registraci potvrdit (převést účast ze stavu „přihlášený“ do „soutěží“);
+		používáme v ostatních kategoriích
+	<li><b>jen organizátoři</b> &ndash; účastníky přihlašuje organizátor
+
+</ul>
+
+<p>Do vyšších kol obvykle účastníky nepřihlašujeme přímo, ale používáme
+tlačítko „Postup z minulého kola“ na stránce soutěže.
+
+<h3>Import účastníku</h3>
 
 <p>Garanti mohou přihlašovat účastníky do soutěže <b>importem</b> souboru ve formátu CSV.
 Tento soubor můžete vyrobit v Excelu či jiném tabulkovém kalkulátoru a pak do CSV exportovat.
@@ -158,26 +181,13 @@ místo pak může mít svůj <b>dozor</b> (viz popis rolí). Typické situace js
 <p>Pokud importujete účastníka, který dosud neměl založen účet, účet se automaticky vytvoří
 a účastníkovi se pošle e-mail s odkazem na nastavení hesla.
 
-<h3>Dozor</h3>
+<h3>Import organizátorů</h3>
 
-<p>Osoby vykonávající dozor na soutěžních místech jde také hromadně importovat.
+<p>Účty a role organizátorů je také možné zakládat hromadně pomocí importu.
 Funguje to podobně jako import účastníků, opět je k dispozici <a href='{{ url_for('doc_import') }}'>popis formátu</a>.
 
-<p>Dozírajícím se automaticky založí organizátorské účty (pokud je ještě nemají) a přidělí
-se jim dozorová role k příslušnému kolu a soutěžnímu místu.
-
-<p>Dozor se ke svému soutěžnímu místu dostane přes Soutěž » výběr kola » výběr soutěže »
-výběr soutěžního místa. Pak si může prohlížet seznam účastníků a odevzdané úlohy
-a také za účastníky odevzdávat.
-
-<h3>Opravovatelé</h3>
-
-<p>Opravovatelé si mohou prohlížet účastnická řešení, nahrávat do systému jejich opravené verze
-a udělovat body a poznámky.
-
-<p>Také je možné stáhnout si najednou všechna účastnická řešení jako jeden ZIP, do řešení
-dopsat poznámky a pak je zase jako ZIP nahrát zpět. Přitom je nutné zachovat jména souborů.
-
-<p>Podobně jako dozor, i opravovatele můžete importovat.
+<p>Importovat organizátory jde do konkrétní soutěže (na stránce soutěže, přidělí se role omezené
+na tuto soutěž), do konkrétního kola (na stránce kola) nebo obecně (v záložce „Organizátoři“,
+tak lze zakládat neomezené organizátorské role, což se hodí například pro garanty).
 
 {% endblock %}
diff --git a/mo/web/templates/doc_import.html b/mo/web/templates/doc_import.html
index 163f9badd9770309f86d984b11b012593139bd48..86b95321dfcf0f5168870be0ba353b57c3915710 100644
--- a/mo/web/templates/doc_import.html
+++ b/mo/web/templates/doc_import.html
@@ -41,30 +41,36 @@ když přidáte vlastní sloupce s novými názvy, budou se ignorovat.
 rozporu mezi importovanými údaji a již známými import selže a je nutné provést
 editaci ručně.
 
-<h2>Import dozoru</h2>
+<h2>Import organizátorů</h2>
 
-<p>Definovány jsou tyto sloupce (tučné jsou povinné):
+<p>Definovány jsou tyto sloupce (tučné jsou povinné, kurzívou jsou povinné pro zatím nezaregistrované účty):
 
 <table class=data>
 	<tr><th>Název<th>Obsah
 	<tr><td><b>email</b><td>E-mailová adresa
-	<tr><td><b>krestni</b><td>Křestní jméno
-	<tr><td><b>prijmeni</b><td>Příjmení
-	<tr><td><b>kod_mista</b><td>Kód soutěžního místa (viz katalog škol na tomto webu)
+	<tr><td><i>krestni</i><td>Křestní jméno
+	<tr><td><i>prijmeni</i><td>Příjmení
+	<tr><td>kod_oblasti<td>Pokud neimportujete do konkrétní soutěže,
+		můžete uvést kód oblasti, na kterou bude mít organizátor omezená práva.
+		V opačném případě bude mít práva ke všem oblastem.
+	<tr><td><b>role</b><td>Jedna z následujících rolí:
+					garant,
+					garant_kraj,
+					garant_okres,
+					garant_skola,
+					dozor,
+					opravovatel.
 </table>
 
-<h2>Import opravovatelů</h2>
-
-<p>Definovány jsou tyto sloupce (tučné jsou povinné):
+V obecném importu organizátorů krom výše uvedených existuje ještě:
 
 <table class=data>
 	<tr><th>Název<th>Obsah
-	<tr><td><b>email</b><td>E-mailová adresa
-	<tr><td><b>krestni</b><td>Křestní jméno
-	<tr><td><b>prijmeni</b><td>Příjmení
-	<tr><td>kod_oblasti<td>Pokud neimportujete do konkrétní soutěže, ale do celého kola,
-		můžete uvést kód oblasti, ve které opravovatel pracuje.
-		V opačném případě bude mít práva ke všem oblastem.
+	<tr><td>kategorie<td>Omezí práva na příslušnou kategorii.
+				Kategorie Z funguje pro všechny kategorie pro základní školy a kategorie S pro všechny kategorie A, B, C.
+	<tr><td>kolo<td>Omezí práva organizátora pouze do příslušného kola.
+				Je nutné současně určit i kategorii a nelze kombinovat s importem bez omezení na aktuální ročník.
+				Kolo se zadává pomocí kódu (například 1, S, 2).
 </table>
 
 {% endblock %}
diff --git a/mo/web/templates/org_contest.html b/mo/web/templates/org_contest.html
index cbef413b21e0c2a0d2b81896cdd048a96295f9ed..9fa6826d00312dd9620ba10d6ff2250918ffc37a 100644
--- a/mo/web/templates/org_contest.html
+++ b/mo/web/templates/org_contest.html
@@ -64,7 +64,8 @@
 	<a class="btn btn-primary" href='{{ ctx.url_for('org_contest_advance') }}'>Postup z minulého kola</a>
 	{% endif %}
 	{% if can_manage %}
-	<a class="btn btn-default" href='{{ ctx.url_for('org_generic_import') }}'>Importovat data</a>
+	<a class="btn btn-default" href='{{ ctx.url_for('org_import_user') }}'>Importovat účastníky</a>
+	<a class="btn btn-default" href='{{ ctx.url_for('org_import_org') }}'>Importovat organizátory</a>
 	{% endif %}
 	{% if can_manage and not site %}
 	<a class="btn btn-default" href='{{ ctx.url_for('org_contest_edit') }}'>Nastavení</a>
diff --git a/mo/web/templates/org_contestants_import.html b/mo/web/templates/org_contestants_import.html
new file mode 100644
index 0000000000000000000000000000000000000000..82d27d7e2ca53cd63d8e6955d6f3194cd89863e4
--- /dev/null
+++ b/mo/web/templates/org_contestants_import.html
@@ -0,0 +1,24 @@
+{% extends "org_generic_import.html" %}
+{% import "bootstrap/wtf.html" as wtf %}
+
+{% block title %}
+Import soutěžících {% if contest or round %}do {% if contest %}soutěže {{ contest.place.name_locative() }}{% else %}kola {{ round.round_code() }}{% endif %}{% endif %}
+{% endblock %}
+{% block breadcrumbs %}
+{{ ctx.breadcrumbs(action="Import soutěžících") }}
+{% endblock %}
+
+{% block import_info %}
+{% if not contest %}
+<p><em>Pozor, zde se importuje do více soutěží najednou, takže je nutné uvádět
+kód oblasti. Nechcete raději importovat do konkrétní oblasti?</em></p>
+{% endif %}
+
+{% if default_place %}
+<p>Výchozí oblastí tohoto importu je: <a href='{{ url_for('org_place', id=default_place.place_id) }}'>{{ default_place.name }}</a>.</p>
+{% endif %}
+{% endblock %}
+
+
+{% block import_form %}
+{% endblock %}
diff --git a/mo/web/templates/org_generic_import.html b/mo/web/templates/org_generic_import.html
index 6ed927d607648ab8660340faeb70a481ebc5831b..bd3906686258d0f34cdc270e67134f82b89e4d65 100644
--- a/mo/web/templates/org_generic_import.html
+++ b/mo/web/templates/org_generic_import.html
@@ -2,13 +2,11 @@
 {% import "bootstrap/wtf.html" as wtf %}
 
 {% block title %}
-Import dat do {% if contest %}soutěže {{ contest.place.name_locative() }}{% else %}kola {{ round.round_code() }}{% endif %}
-{% endblock %}
-{% block breadcrumbs %}
-{{ ctx.breadcrumbs(action="Import dat") }}
+Import dat {% if contest or round %}do {% if contest %}soutěže {{ contest.place.name_locative() }}{% else %}kola {{ round.round_code() }}{% endif %}{% endif %}
 {% endblock %}
 
 {% block body %}
+{% block import_errs %}
 {% if warnings %}
 <h3>Varování při importu</h3>
 
@@ -28,15 +26,24 @@ Import dat do {% if contest %}soutěže {{ contest.place.name_locative() }}{% el
 {% endfor %}
 </div>
 {% endif %}
+{% endblock %}
 
-<p>Zde je možné importovat účastníky soutěže, dozor na soutěžních místech a opravovatele.
-Detaily fungování importu najdete v <a href='{{ url_for('doc_import') }}'>dokumentaci</a>.
+{% block import_info %}{% endblock %}
 
-{% if not contest %}
-<p><em>Pozor, zde se importuje do více soutěží najednou, takže je nutné uvádět
-kód oblasti. Nechcete raději importovat do konkrétní oblasti?</em>
-{% endif %}
+{% block import_help %}
+<p>Detaily fungování importu najdete v <a href='{{ url_for('doc_import') }}'>dokumentaci</a>.
+{% endblock %}
+
+<form action="" method="post"  class="form" enctype="multipart/form-data" role="form">
+{{ form.csrf_token }}
+{{ wtf.form_field(form.file) }}
+{{ wtf.form_field(form.fmt) }}
+{% block import_form %}{% endblock %}
+	<div class="btn-group">
+		{{ wtf.form_field(form.submit, class='btn btn-primary') }}
+		{{ wtf.form_field(form.get_template, class='btn btn-default') }}
+	</div>
+</form>
 
-{{ wtf.quick_form(form, form_type='simple', button_map={'submit': 'primary'}) }}
 
 {% endblock %}
diff --git a/mo/web/templates/org_global_orgs_import.html b/mo/web/templates/org_global_orgs_import.html
new file mode 100644
index 0000000000000000000000000000000000000000..0d2abfba84b307c20f5f232a84f7d3752da904c7
--- /dev/null
+++ b/mo/web/templates/org_global_orgs_import.html
@@ -0,0 +1,24 @@
+{% extends "org_generic_import.html" %}
+{% import "bootstrap/wtf.html" as wtf %}
+
+{% block title %}
+Import organizátorů
+{% endblock %}
+
+{% block import_info %}
+<p><em>Toto je obecný import organzátorů. Ten se hodí pro importování „dlouhodobých“ organizátorských rolí,
+které nejsou vázané na konkrétní soutěž. U každého organizátora můžete určit kategorii, kolo
+i oblast jeho působnosti. Pokud chcete raději importovat organizátory konkrétní soutěže, jde to jednodušeji
+přes stránky kola, soutěže, případně soutěžního místa.</em>
+{% endblock %}
+
+
+{% block import_form %}
+	{{ wtf.form_field(form.default_place) }}
+	{{ wtf.form_field(form.default_cat) }}
+	{{ wtf.form_field(form.default_code) }}
+	{{ wtf.form_field(form.only_this_year) }}
+	{% if imp.cnt_change_user_to_org %}
+		{{ wtf.form_field(form.allow_change_user_to_org) }}
+	{% endif %}
+{% endblock %}
diff --git a/mo/web/templates/org_org.html b/mo/web/templates/org_org.html
index 4f9baba97685bad99b6d220ad08df5149daef773..dfeb3a28b22dece4efdcf13e46c6a09a84a4dc35 100644
--- a/mo/web/templates/org_org.html
+++ b/mo/web/templates/org_org.html
@@ -66,7 +66,7 @@ Podobně <code>S</code> znamená všechny středoškolské kategorie <code>A</co
 <table class="data full">
 	<thead>
 		<tr>
-			<th>Role<th>Oblast<th>Ročník<th>Kategorie<th>Kolo<th>Přidělil<th>Akce
+			<th>Role<th>Oblast<th>Ročník<th>Kategorie<th class='has-tip' title='Pořadí kola v kategorii'>Kolo<th>Přidělil<th>Akce
 		</tr>
 	</thead>
 {% for role in user.roles %}
diff --git a/mo/web/templates/org_orgs.html b/mo/web/templates/org_orgs.html
index 4795f4159d575f524d53907fd9f21ff6ed05509e..7d556a2ac02c668edfe4a8007c44c9db451934ae 100644
--- a/mo/web/templates/org_orgs.html
+++ b/mo/web/templates/org_orgs.html
@@ -3,7 +3,10 @@
 {% block title %}Organizátoři{% endblock %}
 {% block body %}
 {% if can_add %}
-<a class="pull-right btn btn-primary" style="margin-top: -40px;" href="{{ url_for('org_org_new') }}">Nový organizátor</a>
+<div class="btn-group pull-right" style="margin-top: -40px;">
+<a class="btn btn-primary" href="{{ url_for('org_org_new') }}">Nový organizátor</a>
+<a class="btn btn-default" href="{{ url_for('org_orgs_import') }}">Importovat organizátory</a>
+</div>
 {% endif %}
 
 <div class="form-frame">
diff --git a/mo/web/templates/org_orgs_import.html b/mo/web/templates/org_orgs_import.html
new file mode 100644
index 0000000000000000000000000000000000000000..2fde575d9b3177b235f0969d291bd442205feeec
--- /dev/null
+++ b/mo/web/templates/org_orgs_import.html
@@ -0,0 +1,30 @@
+{% extends "org_generic_import.html" %}
+{% import "bootstrap/wtf.html" as wtf %}
+
+{% block title %}
+Import organizátorů {% if contest or round %}{% if contest %}soutěže {{ contest.place.name_locative() }}{% else %}kola {{ round.round_code() }}{% endif %}{% endif %}
+{% endblock %}
+{% block breadcrumbs %}
+{{ ctx.breadcrumbs(action="Import organizátorů") }}
+{% endblock %}
+
+{% block import_info %}
+{% if not contest %}
+<p><em>Zde můžete importovat organizátory do více soutěží najednou, takže je nutné uvádět kód oblasti.
+Nechcete raději importovat do konkrétní oblasti na stránce soutěže?</em></p>
+{% else %}
+<p><em>Zde můžete importovat organizátory soutěže. Dostanou organizátorskou roli
+omezenou na tuto konkrétní soutěž.</em></p>
+{% endif %}
+
+{% if default_place %}
+<p>Výchozí oblastí tohoto importu je: <a href='{{ url_for('org_place', id=default_place.place_id) }}'>{{ default_place.name }}</a>.</p>
+{% endif %}
+{% endblock %}
+
+
+{% block import_form %}
+	{% if imp.cnt_change_user_to_org %}
+		{{ wtf.form_field(form.allow_change_user_to_org) }}
+	{% endif %}
+{% endblock %}
diff --git a/mo/web/templates/org_place_rights.html b/mo/web/templates/org_place_rights.html
index 7664ec0d55a84f9186b84ca2eaced16cbf8ee293..22505de047e20e161449d80c7fefc3007b757205 100644
--- a/mo/web/templates/org_place_rights.html
+++ b/mo/web/templates/org_place_rights.html
@@ -11,7 +11,7 @@
 		<th>Jméno
 		<th>Roč.
 		<th>Kat.
-		<th>Kolo
+		<th class='has-tip' title='Pořadí kola v kategorii'>Kolo
 		<th>Zdroj
 	</thead>
 	{% for role in roles %}
diff --git a/mo/web/templates/org_round.html b/mo/web/templates/org_round.html
index ebeb92d499f8a7aea7bef00b949fb9590d0258fb..0a8ab128d1246d46ba686a142c3ebf2e6ed13cd3 100644
--- a/mo/web/templates/org_round.html
+++ b/mo/web/templates/org_round.html
@@ -92,7 +92,8 @@
 	<a class="btn btn-primary" href='{{ ctx.url_for('org_score') }}'>Výsledky</a>
 	{% endif %}
 	{% if can_manage_contest %}
-	<a class="btn btn-default" href='{{ ctx.url_for('org_generic_import') }}'>Importovat data</a>
+	<a class="btn btn-default" href='{{ ctx.url_for('org_import_user') }}'>Importovat účastníky</a>
+	<a class="btn btn-default" href='{{ ctx.url_for('org_import_org') }}'>Importovat organizátory</a>
 	{% endif %}
 	{% if can_manage_round %}
 	<a class="btn btn-default" href='{{ ctx.url_for('org_round_edit') }}'>Nastavení a termíny</a>
diff --git a/mo/web/templates/org_user.html b/mo/web/templates/org_user.html
index 9595d8566dcee426aad7afa1425c7ab4ee103145..9155eb29c953a2f8fda028848f6c0a1ca34811cc 100644
--- a/mo/web/templates/org_user.html
+++ b/mo/web/templates/org_user.html
@@ -28,6 +28,14 @@
 	</button>
 </form>
 {% endif %}
+{% if upgrade_form %}
+<form method=POST class='btn-group' onsubmit='return confirm("Změnit účastnický účet na organizátorský?");'>
+	{{ upgrade_form.csrf_token }}
+	<button class="btn btn-default" type='submit' name='upgrade' value='yes'>
+		Změnit na organizátora
+	</button>
+</form>
+{% endif %}
 {% if g.user.is_admin %}
 	<a class="btn btn-default" href="{{ log_url('user', user.user_id) }}">Historie</a>
 {% endif %}
diff --git a/static/mo.css b/static/mo.css
index 7dcf41627fcf49589e0437c14bdfd8833da9d03f..6796adad0767a50499d93370f949bafc347ac262 100644
--- a/static/mo.css
+++ b/static/mo.css
@@ -70,6 +70,12 @@ span.unknown {
 	color: red;
 }
 
+.has-tip {
+	text-decoration: underline dashed;
+}
+
+/* Tables */
+
 table.data {
 	border-collapse: collapse;
 	margin-top: 2ex;
@@ -388,6 +394,8 @@ div.alert + div.alert {
 	max-height: 100vh;
 }
 
+/* User messages */
+
 div.message {
 	padding: 5px 10px;
 	margin-bottom: 5px;