diff --git a/db/db.ddl b/db/db.ddl
index b5e22cfc96b62abf2fe8c74c33b42e9389beb2fe..803d275d87724c5db2362f5948248bb399277ad8 100644
--- a/db/db.ddl
+++ b/db/db.ddl
@@ -90,9 +90,11 @@ CREATE TYPE score_mode AS ENUM (
 
 CREATE TABLE rounds (
 	round_id	serial		PRIMARY KEY,
+	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.
+	part		int		NOT NULL DEFAULT 0,		-- část kola (u dělených kol)
 	level		int		NOT NULL,			-- úroveň hierarchie míst
 	name		varchar(255)	NOT NULL,			-- zobrazované jméno ("Krajské kolo" apod.)
 	state		round_state	NOT NULL DEFAULT 'preparing',	-- stav kola
@@ -104,17 +106,22 @@ CREATE TABLE rounds (
 	score_mode	score_mode	NOT NULL DEFAULT 'basic',	-- mód výsledkovky
 	score_winner_limit	int	DEFAULT NULL,			-- bodový limit na označení za vítěze
 	score_successful_limit	int	DEFAULT NULL,			-- bodový limit na označení za úspěšného řešitele
-	UNIQUE (year, category, seq)
+	UNIQUE (year, category, seq, part)
 );
 
+CREATE INDEX rounds_master_round_id_index ON rounds (master_round_id);
+
 -- Soutěže (instance kola v konkrétním místě)
 CREATE TABLE contests (
-	contest_id	serial		PRIMARY KEY,
-	round_id	int		NOT NULL REFERENCES rounds(round_id),
-	place_id	int		NOT NULL REFERENCES places(place_id),
+	contest_id		serial		PRIMARY KEY,
+	master_contest_id	int		DEFAULT NULL REFERENCES contests(contest_id),
+	round_id		int		NOT NULL REFERENCES rounds(round_id),
+	place_id		int		NOT NULL REFERENCES places(place_id),
 	UNIQUE (round_id, place_id)
 );
 
+CREATE INDEX contests_master_contest_id_index ON contests (master_contest_id);
+
 -- Detaily účastníka
 CREATE TABLE participants (
 	user_id		int		NOT NULL REFERENCES users(user_id),
diff --git a/db/upgrade-20210307b.sql b/db/upgrade-20210307b.sql
new file mode 100644
index 0000000000000000000000000000000000000000..4c1fc444e11bac39a054bfd2b15ee3f0b4ffae81
--- /dev/null
+++ b/db/upgrade-20210307b.sql
@@ -0,0 +1,18 @@
+SET ROLE 'mo_osmo';
+
+ALTER TABLE rounds
+	ADD COLUMN	master_round_id		int	DEFAULT NULL REFERENCES rounds(round_id),
+	ADD COLUMN	part			int	NOT NULL DEFAULT 0;				-- část kola (u dělených kol)
+
+ALTER TABLE contests
+	ADD COLUMN	master_contest_id	int	DEFAULT NULL REFERENCES contests(contest_id);
+
+ALTER TABLE rounds
+	DROP CONSTRAINT "rounds_year_category_seq_key",
+	ADD CONSTRAINT "rounds_year_category_seq_part_key" UNIQUE (year, category, seq, part);
+
+CREATE INDEX rounds_master_round_id_index ON rounds (master_round_id);
+CREATE INDEX contests_master_contest_id_index ON contests (master_contest_id);
+
+UPDATE rounds SET master_round_id=round_id;
+UPDATE contests SET master_contest_id=contest_id;
diff --git a/mo/db.py b/mo/db.py
index 7ddf905cb220d7ec712e78c8f4c25310fd030920..769e839d8e6e251ea413d37bc984d35964387890 100644
--- a/mo/db.py
+++ b/mo/db.py
@@ -187,13 +187,15 @@ round_score_mode_names = {
 class Round(Base):
     __tablename__ = 'rounds'
     __table_args__ = (
-        UniqueConstraint('year', 'category', 'seq'),
+        UniqueConstraint('year', 'category', 'seq', 'part'),
     )
 
     round_id = Column(Integer, primary_key=True, server_default=text("nextval('rounds_round_id_seq'::regclass)"))
+    master_round_id = Column(Integer, ForeignKey('rounds.round_id'))
     year = Column(Integer, nullable=False)
     category = Column(String(2), nullable=False)
     seq = Column(Integer, nullable=False)
+    part = Column(Integer, nullable=False)
     level = Column(Integer, nullable=False)
     name = Column(String(255), nullable=False)
     state = Column(Enum(RoundState, name='round_state'), nullable=False, server_default=text("'preparing'::round_state"))
@@ -206,12 +208,34 @@ class Round(Base):
     score_winner_limit = Column(Integer)
     score_successful_limit = Column(Integer)
 
-    def round_code(self):
+    master = relationship('Round', primaryjoin='Round.master_round_id == Round.round_id', remote_side='Round.round_id', post_update=True)
+
+    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.seq}"
 
+    def part_code(self):
+        return chr(ord('a') + self.part - 1) if self.part > 0 else ""
+
+    def round_code(self):
+        """ Kód kola včetně označení části, pokud je ve skupině kol. """
+        code = self.round_code_short()
+        part = self.part_code()
+        return f"{code}{part}"
+
     def has_tasks(self):
         return self.tasks_file
 
+    def is_subround(self) -> bool:
+        return self.master_round_id != self.round_id
+
+    def get_group_rounds(self, add_self=False) -> List['Round']:
+        # podle PEP484 se má použít string při forward typové referenci
+        return get_session().query(Round).filter(
+            Round.master_round_id == self.master_round_id,
+            add_self or Round.round_id != self.round_id,
+        ).all()
+
     def long_state(self) -> str:
         details = ""
         if self.state == RoundState.preparing:
@@ -275,12 +299,23 @@ class Contest(Base):
     )
 
     contest_id = Column(Integer, primary_key=True, server_default=text("nextval('contests_contest_id_seq'::regclass)"))
+    master_contest_id = Column(Integer, ForeignKey('contests.contest_id'))
     round_id = Column(Integer, ForeignKey('rounds.round_id'), nullable=False)
     place_id = Column(Integer, ForeignKey('places.place_id'), nullable=False)
 
+    master = relationship('Contest', primaryjoin='Contest.master_contest_id == Contest.contest_id', remote_side='Contest.contest_id', post_update=True)
     place = relationship('Place')
     round = relationship('Round')
 
+    def is_subcontest(self) -> bool:
+        return self.master_contest_id != self.contest_id
+
+    def get_group_contests(self, add_self=False) -> List['Contest']:
+        # podle PEP484 se má použít string při forward typové referenci
+        return get_session().query(Contest).filter(
+            Contest.master_contest_id == self.master_contest_id,
+            add_self or Contest.contest_id != self.contest_id,
+        ).options(joinedload(Contest.round)).all()
 
 class LogType(MOEnum):
     general = auto()
diff --git a/mo/jobs/submit.py b/mo/jobs/submit.py
index 019b907402a1c5ebe48b257dd9603eaed06a68ae..3f40f738d52f5a2c1a6a711ae33f4b6595959ab1 100644
--- a/mo/jobs/submit.py
+++ b/mo/jobs/submit.py
@@ -51,7 +51,7 @@ def handle_download_submits(the_job: TheJob):
               .join(db.User, db.User.user_id == db.Paper.for_user)
               .join(db.Task, db.Task.task_id == db.Paper.for_task)
               .join(db.Participation, db.Participation.user_id == db.Paper.for_user)
-              .join(db.Contest, and_(db.Contest.contest_id == db.Participation.contest_id, db.Contest.round_id == db.Task.round_id))
+              .join(db.Contest, and_(db.Contest.master_contest_id == db.Participation.contest_id, db.Contest.round_id == db.Task.round_id))
               .join(db.Place, db.Place.place_id == db.Contest.place_id)
               .all())
     papers.sort(key=lambda p: (p[1].sort_key(), p[2]))
@@ -216,7 +216,7 @@ def handle_upload_feedback(the_job: TheJob):
         rows = (sess.query(db.User, db.Participation, db.Contest)
                 .select_from(db.Participation)
                 .join(db.User, db.User.user_id == db.Participation.user_id)
-                .join(db.Contest, db.Contest.contest_id == db.Participation.contest_id)
+                .join(db.Contest, db.Contest.master_contest_id == db.Participation.contest_id)
                 .filter(db.Contest.round == round)
                 .filter(db.Participation.user_id.in_(user_dict.keys()))
                 .all())
diff --git a/mo/score.py b/mo/score.py
index b7f4ed5d36fbf561fc29d520d02dd50f3a4e6448..aba2ffe7517ffe8c92f4e21e9bbe1ff82896ebda 100644
--- a/mo/score.py
+++ b/mo/score.py
@@ -105,12 +105,12 @@ class Score:
         self.contest = contest
         self.part_states = part_states
 
-        # Příprava subquery na účastníky
+        # Příprava subquery na účastníky (contest_subq obsahuje master_contest_id)
         sess = db.get_session()
         if contest:
-            contest_subq = [contest.contest_id]
+            contest_subq = [contest.master_contest_id]
         else:
-            contest_subq = sess.query(db.Contest.contest_id).filter_by(round=round)
+            contest_subq = sess.query(db.Contest.master_contest_id).filter_by(round=round)
 
         # Načtení účastníků
         data: List[Tuple[db.User, db.Participation, db.Participant]] = (
@@ -164,7 +164,9 @@ class Score:
         num_participants = db.get_count(user_id_subq)
 
         # Načtení úloh
-        tasks: List[db.Task] = sess.query(db.Task).filter_by(round=round).all()
+        tasks: List[db.Task] = sess.query(db.Task).filter(db.Task.round_id.in_(
+            sess.query(db.Round.round_id).filter_by(master_round_id=round.master_round_id)
+        )).all()
         for task in tasks:
             self._tasks[step][task.task_id] = ScoreTask(task)
             self._tasks[step][task.task_id].num_solutions = num_participants
@@ -202,7 +204,7 @@ class Score:
         # Zkusíme nalézt kolo o `step` kroků zpět
         prev_round = sess.query(db.Round).filter_by(
             year=self.round.year, category=self.round.category, seq=self.round.seq - step
-        ).one_or_none()
+        ).filter(db.Round.master_round_id == db.Round.round_id).one_or_none()
         if prev_round is None:
             return False
         self._prev_rounds[step] = prev_round
@@ -211,13 +213,13 @@ class Score:
             # Pokud tvoříme výsledkovku pro contest, tak nás zajímají jen řešení
             # z podoblastí contestu spadajícího pod hlavní
             desc_cte = db.place_descendant_cte(self.contest.place, max_level=prev_round.level)
-            contest_subq = sess.query(db.Contest.contest_id).filter(
+            contest_subq = sess.query(db.Contest.master_contest_id).filter(
                 db.Contest.round == prev_round,
                 db.Contest.place_id.in_(select([desc_cte]))
             )
         else:
             # Pokud vytváříme výsledkovku pro celé kolo, bereme vše
-            contest_subq = sess.query(db.Contest.contest_id).filter_by(round=prev_round)
+            contest_subq = sess.query(db.Contest.master_contest_id).filter_by(round=prev_round)
 
         self._load_tasks_and_sols(step, prev_round, contest_subq)
         return True
diff --git a/mo/web/org_contest.py b/mo/web/org_contest.py
index 933d9d2ae77dfadc2b096c04f965988bd67279fb..d788d9a0cc9a192325b18362f7a6c87ff769ddce 100644
--- a/mo/web/org_contest.py
+++ b/mo/web/org_contest.py
@@ -15,7 +15,7 @@ import wtforms
 import mo
 from mo.csv import FileFormat
 import mo.db as db
-from mo.imports import ImportType, Import, create_import
+from mo.imports import ImportType, create_import
 import mo.jobs.submit
 from mo.rights import Right, Rights
 import mo.util
@@ -129,13 +129,14 @@ class ParticipantsActionForm(FlaskForm):
             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()
+            # Contest hledáme vždy v master kole, abychom náhodou nepřesunuli účastníky do soutěže v podkole
+            contest = sess.query(db.Contest).filter_by(round_id=round.master_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')
+                flash(f"Nepovedlo se najít soutěž v kole {round.round_code_short()} v oblasti {contest_place.name}", 'danger')
                 return False
             rr = g.gatekeeper.rights_for_contest(contest)
             if not rr.have_right(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 účastníky", 'danger')
+                flash(f"Nemáte právo ke správě soutěže v kole {round.round_code_short()} v oblasti {contest_place.name}, nelze do ní přesunout účastníky", 'danger')
                 return False
         elif self.remove_participation.data:
             pass
@@ -158,7 +159,7 @@ class ParticipantsActionForm(FlaskForm):
             rr = g.gatekeeper.rights_for_contest(pion.contest)
             if not rr.have_right(Right.manage_contest):
                 flash(
-                    f"Nemáte právo ke správě soutěže v kole {round.round_code()} v oblasti {pion.contest.place.name} "
+                    f"Nemáte právo ke správě soutěže v kole {round.round_code_short()} v oblasti {pion.contest.place.name} "
                     + f"(účastník {u.full_name()}). Žádná akce nebyla provedena.", 'danger'
                 )
                 return False
@@ -233,33 +234,43 @@ class ParticipantsActionForm(FlaskForm):
         return True
 
 
-def get_contest(id: int) -> db.Contest:
+def get_contest(id: int) -> Tuple[db.Contest, db.Contest]:
+    """ Vrací contest a master_contest pro zadané contest_id.
+    Pro nedělená kola platí contest == master_contest.
+    Operace s účastníky by měly probíhat vždy přes master_contest."""
     contest = (db.get_session().query(db.Contest)
                .options(joinedload(db.Contest.place),
-                        joinedload(db.Contest.round))
+                        joinedload(db.Contest.round),
+                        joinedload(db.Contest.master).joinedload(db.Contest.round))
                .get(id))
     if not contest:
         raise werkzeug.exceptions.NotFound()
-    return contest
+    return contest, contest.master
 
 
-def get_contest_rr(id: int, right_needed: Optional[Right] = None) -> Tuple[db.Contest, Rights]:
-    contest = get_contest(id)
+def get_contest_rr(id: int, right_needed: Optional[Right] = None) -> Tuple[db.Contest, db.Contest, Rights]:
+    """ Vrací contest, master_contest a Rights objekt pro zadané contest_id.
+    Pro nedělená kola platí contest == master_contest.
+    Operace s účastníky by měly probíhat vždy přes master_contest."""
+    contest, master_contest = get_contest(id)
 
     rr = g.gatekeeper.rights_for_contest(contest)
 
     if not (right_needed is None or rr.have_right(right_needed)):
         raise werkzeug.exceptions.Forbidden()
 
-    return contest, rr
+    return contest, master_contest, rr
 
 
-def get_contest_site_rr(id: int, site_id: Optional[int], right_needed: Optional[Right] = None) -> Tuple[db.Contest, Optional[db.Place], Rights]:
+def get_contest_site_rr(id: int, site_id: Optional[int], right_needed: Optional[Right] = None) -> Tuple[db.Contest, db.Contest, Optional[db.Place], Rights]:
+    """ Vrací contest, master_contest, optional site a Rights objekt pro zadané contest_id a site_id.
+    Pro nedělená kola platí contest == master_contest.
+    Operace s účastníky by měly probíhat vždy přes master_contest."""
     if site_id is None:
-        contest, rr = get_contest_rr(id, right_needed)
-        return contest, None, rr
+        contest, master_contest, rr = get_contest_rr(id, right_needed)
+        return contest, master_contest, None, rr
 
-    contest = get_contest(id)
+    contest, master_contest = get_contest(id)
     site = db.get_session().query(db.Place).get(site_id)
     if not site:
         raise werkzeug.exceptions.NotFound()
@@ -269,7 +280,7 @@ def get_contest_site_rr(id: int, site_id: Optional[int], right_needed: Optional[
     if not (right_needed is None or rr.have_right(right_needed)):
         raise werkzeug.exceptions.Forbidden()
 
-    return contest, site, rr
+    return contest, master_contest, site, rr
 
 
 def contest_breadcrumbs(
@@ -313,11 +324,11 @@ def contest_breadcrumbs(
 @app.route('/org/contest/c/<int:id>/site/<int:site_id>/')
 def org_contest(id: int, site_id: Optional[int] = None):
     sess = db.get_session()
-    contest, site, rr = get_contest_site_rr(id, site_id, None)
+    contest, master_contest, site, rr = get_contest_site_rr(id, site_id, None)
     round = contest.round
 
     tasks_subq = sess.query(db.Task.task_id).filter_by(round=contest.round)
-    pions_subq = sess.query(db.Participation.user_id).filter_by(contest=contest)
+    pions_subq = sess.query(db.Participation.user_id).filter_by(contest=master_contest)
     if site:
         pions_subq = pions_subq.filter_by(place=site)
     sol_counts_q = (
@@ -343,12 +354,15 @@ def org_contest(id: int, site_id: Optional[int] = None):
             sess.query(db.Place, func.count('*'))
             .select_from(db.Participation).join(db.Place)
             .group_by(db.Place)
-            .filter(db.Participation.contest_id == id).all()
+            .filter(db.Participation.contest == master_contest).all()
         )
 
+    group_contests = contest.get_group_contests(True)
+    group_contests.sort(key=lambda c: c.round.round_code())
+
     return render_template(
         'org_contest.html',
-        contest=contest, site=site,
+        contest=contest, group_contests=group_contests, site=site,
         rights=sorted(rr.rights, key=lambda r: r.name),
         roles=[r.friendly_name() for r in rr.get_roles()],
         can_manage=rr.have_right(Right.manage_contest),
@@ -360,7 +374,7 @@ def org_contest(id: int, site_id: Optional[int] = None):
     )
 
 
-def generic_import(round: db.Round, contest: Optional[db.Contest]):
+def generic_import(round: db.Round, master_round: db.Round, contest: Optional[db.Contest], master_contest: Optional[db.Contest]):
     """Společná funkce pro importování do soutěží a kol"""
 
     form = ImportForm()
@@ -368,7 +382,7 @@ def generic_import(round: db.Round, contest: Optional[db.Contest]):
     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)
+        imp = create_import(user=g.user, type=form.typ.data, fmt=fmt, round=master_round, contest=master_contest)
         if form.submit.data:
             if form.file.data is not None:
                 file = form.file.data.stream
@@ -412,21 +426,24 @@ def doc_import():
 
 @app.route('/org/contest/c/<int:id>/import', methods=('GET', 'POST'))
 def org_contest_import(id: int):
-    contest, rr = get_contest_rr(id, Right.manage_contest)
-    return generic_import(contest.round, contest)
+    contest, master_contest, rr = get_contest_rr(id, Right.manage_contest)
+    return generic_import(
+        round=contest.round, master_round=master_contest.round,
+        contest=contest, master_contest=master_contest
+    )
 
 
 @app.route('/org/contest/c/<int:id>/ucastnici', methods=('GET', 'POST'))
 @app.route('/org/contest/c/<int:id>/site/<int:site_id>/ucastnici', methods=('GET', 'POST'))
 def org_contest_list(id: int, site_id: Optional[int] = None):
-    contest, site, rr = get_contest_site_rr(id, site_id)
+    contest, master_contest, site, rr = get_contest_site_rr(id, site_id)
     can_edit = rr.have_right(Right.manage_contest)
     format = request.args.get('format', "")
 
     filter = ParticipantsFilterForm(request.args)
     filter.validate()
     query = get_contestants_query(
-        round=contest.round, contest=contest, site=site,
+        round=master_contest.round, contest=master_contest, site=site,
         school=filter.f_school,
         # contest_place=filter.f_contest_place,
         participation_place=filter.f_participation_place,
@@ -556,7 +573,9 @@ def make_contestant_table(query: Query, add_checkbox: bool = False, add_contest_
 @dataclass
 class SolutionContext:
     contest: db.Contest
+    master_contest: db.Contest
     round: db.Round
+    master_round: db.Round
     pion: Optional[db.Participation]
     user: Optional[db.User]
     task: Optional[db.Task]
@@ -572,8 +591,9 @@ def get_solution_context(contest_id: int, user_id: Optional[int], task_id: Optio
     sess = db.get_session()
 
     # Nejprve zjistíme, zda existuje soutěž
-    contest = get_contest(contest_id)
+    contest, master_contest = get_contest(contest_id)
     round = contest.round
+    master_round = master_contest.round
 
     # Najdeme úlohu a ověříme, že je součástí soutěže
     if task_id is not None:
@@ -588,7 +608,7 @@ def get_solution_context(contest_id: int, user_id: Optional[int], task_id: Optio
     if user_id is not None:
         # Zkontrolujeme, zda se účastník opravdu účastní soutěže
         pion = (sess.query(db.Participation)
-                .filter_by(user_id=user_id, contest_id=contest_id)
+                .filter_by(user_id=user_id, contest_id=master_contest.contest_id)
                 .options(joinedload(db.Participation.place),
                          joinedload(db.Participation.user))
                 .one_or_none())
@@ -620,8 +640,8 @@ def get_solution_context(contest_id: int, user_id: Optional[int], task_id: Optio
     allow_upload_solutions = rr.can_upload_solutions(round)
     allow_upload_feedback = rr.can_upload_feedback(round)
     return SolutionContext(
-        contest=contest,
-        round=round,
+        contest=contest, master_contest=master_contest,
+        round=round, master_round=master_round,
         pion=pion,
         user=user,
         task=task,
@@ -934,7 +954,7 @@ def org_contest_task(contest_id: int, task_id: int, site_id: Optional[int] = Non
 
     sess = db.get_session()
 
-    q = get_solutions_query(sc.task, for_contest=sc.contest, for_site=sc.site)
+    q = get_solutions_query(sc.task, for_contest=sc.master_contest, for_site=sc.site)
     rows: List[Tuple[db.Participation, db.Solution]] = q.all()
     rows.sort(key=lambda r: r[0].user.sort_key())
 
@@ -1044,13 +1064,13 @@ def org_contest_solutions(id: int, site_id: Optional[int] = None):
     if edit_action and not sc.allow_create_solutions:
         raise werkzeug.exceptions.Forbidden()
 
-    pions_subq = sess.query(db.Participation.user_id).filter_by(contest=sc.contest)
+    pions_subq = sess.query(db.Participation.user_id).filter_by(contest=sc.master_contest)
     if sc.site:
         pions_subq = pions_subq.filter_by(place=sc.site)
     pions_subq = pions_subq.subquery()
     pions = (sess.query(db.Participation)
              .filter(
-                 db.Participation.contest == sc.contest,
+                 db.Participation.contest == sc.master_contest,
                  db.Participation.user_id.in_(pions_subq),
              ).options(joinedload(db.Participation.user))
              .all())
@@ -1182,11 +1202,11 @@ def generic_batch_download(round: db.Round, contest: Optional[db.Contest], site:
 
     pion_query = sess.query(db.Participation.user_id).select_from(db.Participation)
     if contest is not None:
-        pion_query = pion_query.filter_by(contest=contest)
+        pion_query = pion_query.filter_by(contest_id=contest.master_contest_id)
         if site is not None:
             pion_query = pion_query.filter_by(place=site)
     else:
-        pion_query = pion_query.join(db.Contest).filter(db.Contest.round == round)
+        pion_query = pion_query.join(db.Contest).filter(db.Contest.round_id == round.master_round_id)
 
     sol_query = (sess.query(db.Solution)
                  .select_from(db.Solution)
@@ -1394,7 +1414,7 @@ class AdvanceForm(FlaskForm):
 def org_contest_advance(contest_id: int):
     sess = db.get_session()
     conn = sess.connection()
-    contest, rr = get_contest_rr(contest_id, Right.manage_contest)
+    contest, master_contest, rr = get_contest_rr(contest_id, Right.manage_contest)
 
     def redirect_back():
         return redirect(url_for('org_contest', id=contest_id))
@@ -1404,11 +1424,14 @@ def org_contest_advance(contest_id: int):
         flash('Aktuální kolo není ve stavu přípravy', 'danger')
         return redirect_back()
 
-    prev_round = sess.query(db.Round).filter_by(year=round.year, category=round.category, seq=round.seq - 1).one_or_none()
+    prev_round = sess.query(db.Round).filter_by(
+        year=round.year, category=round.category, seq=round.seq - 1
+    ).filter(db.Round.master_round_id == db.Round.round_id).one_or_none()
     if prev_round is None:
         flash('Předchozí kolo nenalezeno', 'danger')
         return redirect_back()
     elif prev_round.state != db.RoundState.closed:
+        # FIXME: Možná kontrolovat stav uzavření všech kol ve skupině kol?
         flash('Předchozí kolo dosud nebylo ukončeno', 'danger')
         return redirect_back()
     elif prev_round.level < round.level:
@@ -1440,7 +1463,8 @@ def org_contest_advance(contest_id: int):
         if form.boundary.data > 0:
             accept_uids = (sess.query(db.Solution.user_id)
                            .select_from(db.Solution)
-                           .join(db.Task, and_(db.Task.task_id == db.Solution.task_id, db.Task.round == prev_round))
+                           # Vybíráme úlohy, jejich round patří do stejné skupiny kol jako prev_round
+                           .join(db.Task, and_(db.Task.task_id == db.Solution.task_id, db.Task.round.master_round_id == prev_round.round_id))
                            .filter(db.Solution.user_id.in_(prev_pion_query.with_entities(db.Participation.user_id).subquery()))
                            .group_by(db.Solution.user_id)
                            .having(func.sum(db.Solution.points) >= form.boundary.data)
@@ -1451,10 +1475,10 @@ def org_contest_advance(contest_id: int):
 
         want_execute = form.execute.data
         if want_execute:
-            app.logger.info(f'Postup: Z kola #{prev_round.round_id} do #{round.round_id}, soutěž #{contest_id}')
+            app.logger.info(f'Postup: Z kola #{prev_round.round_id} do #{round.master_round_id}, soutěž #{master_contest.contest_id}')
             mo.util.log(
                 type=db.LogType.contest,
-                what=contest_id,
+                what=master_contest.contest_id,
                 details={'action': 'advance'},
             )
 
diff --git a/mo/web/org_round.py b/mo/web/org_round.py
index 9235d248ed68d91fa94dc4be8b668b58899486b6..f14705d3b4aa9d138d7079279e0592147457743a 100644
--- a/mo/web/org_round.py
+++ b/mo/web/org_round.py
@@ -20,22 +20,20 @@ from mo.web.org_contest import ParticipantsActionForm, ParticipantsFilterForm, g
     generic_import, generic_batch_download, generic_batch_upload, generic_batch_points
 
 
-def get_round(id: int) -> db.Round:
-    round = db.get_session().query(db.Round).get(id)
+def get_round_rr(id: int, right_needed: Optional[Right], any_place: bool) -> Tuple[db.Round, db.Round, Rights]:
+    """Vrací round, master_round a Rights objekt pro zadané round_id.
+    Pro nedělená kola platí round == master_round.
+    Operace s účastníky by měly probíhat vždy přes master_round."""
+    round = db.get_session().query(db.Round).options(joinedload(db.Round.master)).get(id)
     if not round:
         raise werkzeug.exceptions.NotFound()
-    return round
-
-
-def get_round_rr(id: int, right_needed: Optional[Right], any_place: bool) -> Tuple[db.Round, Rights]:
-    round = get_round(id)
 
     rr = g.gatekeeper.rights_for_round(round, any_place)
 
     if not (right_needed is None or rr.have_right(right_needed)):
         raise werkzeug.exceptions.Forbidden()
 
-    return round, rr
+    return round, round.master, rr
 
 
 def get_task(round: db.Round, task_id: int) -> db.Task:
@@ -49,7 +47,7 @@ def get_task(round: db.Round, task_id: int) -> db.Task:
 def org_rounds():
     sess = db.get_session()
 
-    rounds = sess.query(db.Round).filter_by(year=mo.current_year).order_by(db.Round.year, db.Round.category, db.Round.seq)
+    rounds = sess.query(db.Round).filter_by(year=mo.current_year).order_by(db.Round.year, db.Round.category, db.Round.seq, db.Round.part)
     return render_template('org_rounds.html', rounds=rounds, level_names=mo.db.place_level_names)
 
 
@@ -104,16 +102,17 @@ def add_contest(round: db.Round, form: AddContestForm) -> bool:
         flash(f'Místo s kódem {form.place_code.data} neexistuje', 'danger')
         return False
 
-    if place.level != round.level:
-        flash(f'{place.type_name().title()} {place.name} není {db.place_level_names[round.level]}', 'danger')
-        return False
+    # if place.level != round.level:
+    #     flash(f'{place.type_name().title()} {place.name} není {db.place_level_names[round.level]}', 'danger')
+    #     return False
 
     sess = db.get_session()
     if sess.query(db.Contest).filter_by(round=round, place=place).one_or_none():
         flash(f'Pro {place.type_name()} {place.name} už toto kolo existuje', 'danger')
         return False
 
-    contest = db.Contest(round=round, place=place)
+    # Soutěž vytvoříme vždy v hlavním kole
+    contest = db.Contest(round=round.master, place=place)
     rr = g.gatekeeper.rights_for_contest(contest)
     if not rr.have_right(Right.add_contest):
         flash('Vaše role nedovoluje vytvořit soutěž v oblasti {place.type_name()} {place.name}', 'danger')
@@ -121,6 +120,9 @@ def add_contest(round: db.Round, form: AddContestForm) -> bool:
 
     sess.add(contest)
     sess.flush()
+    contest.master_contest_id = contest.contest_id
+    sess.add(contest)
+    sess.flush()
 
     mo.util.log(
         type=db.LogType.contest,
@@ -128,8 +130,26 @@ def add_contest(round: db.Round, form: AddContestForm) -> bool:
         details={'action': 'add', 'contest': db.row2dict(contest)},
     )
     app.logger.info(f"Soutěž #{contest.contest_id} založena: {db.row2dict(contest)}")
-    sess.commit()
 
+    # Přidání soutěže do podkol ve skupině
+    subrounds = round.master.get_group_rounds()
+    for subround in subrounds:
+        subcontest = db.Contest(
+            round_id=subround.round_id,
+            master_contest_id=contest.contest_id,
+            place_id=contest.place_id,
+        )
+        sess.add(subcontest)
+        sess.flush()
+
+        mo.util.log(
+            type=db.LogType.contest,
+            what=subcontest.contest_id,
+            details={'action': 'add', 'contest': db.row2dict(subcontest)},
+        )
+        app.logger.info(f"Soutěž #{subcontest.contest_id} založena: {db.row2dict(subcontest)}")
+
+    sess.commit()
     flash(f'Soutěž v oblasti {place.type_name()} {place.name} založena', 'success')
     return True
 
@@ -137,7 +157,7 @@ def add_contest(round: db.Round, form: AddContestForm) -> bool:
 @app.route('/org/contest/r/<int:id>/', methods=('GET', 'POST'))
 def org_round(id: int):
     sess = db.get_session()
-    round, rr = get_round_rr(id, None, True)
+    round, _, rr = get_round_rr(id, None, True)
 
     can_manage_round = rr.have_right(Right.manage_round)
     can_manage_contestants = rr.have_right(Right.manage_contest)
@@ -147,10 +167,11 @@ def org_round(id: int):
         func.count(db.Participation.user_id).label('count')
     ).group_by(db.Participation.contest_id).subquery()
 
+    # účastníci jsou jen pod master contesty
     contests_counts = (sess.query(
                     db.Contest,
                     coalesce(participants_count.c.count, 0)
-                ).outerjoin(participants_count)
+                ).outerjoin(participants_count, db.Contest.master_contest_id == participants_count.c.contest_id)
                 .filter(db.Contest.round == round)
                 .options(joinedload(db.Contest.place))
                 .all())
@@ -181,9 +202,12 @@ def org_round(id: int):
     if add_contest(round, form_add_contest):
         return redirect(url_for('org_round', id=id))
 
+    group_rounds = round.get_group_rounds(True)
+    group_rounds.sort(key=lambda r: r.round_code())
+
     return render_template(
         'org_round.html',
-        round=round,
+        round=round, group_rounds=group_rounds,
         roles=[r.friendly_name() for r in rr.get_roles()],
         contests_counts=contests_counts,
         tasks=tasks, form_delete_task=form_delete_task,
@@ -215,7 +239,7 @@ class TaskEditForm(FlaskForm):
 @app.route('/org/contest/r/<int:id>/task/new', methods=('GET', 'POST'))
 def org_round_task_new(id: int):
     sess = db.get_session()
-    round, rr = get_round_rr(id, Right.manage_round, True)
+    round, _, _ = get_round_rr(id, Right.manage_round, True)
 
     form = TaskEditForm()
     if form.validate_on_submit():
@@ -247,7 +271,7 @@ def org_round_task_new(id: int):
 @app.route('/org/contest/r/<int:id>/task/<int:task_id>/edit', methods=('GET', 'POST'))
 def org_round_task_edit(id: int, task_id: int):
     sess = db.get_session()
-    round, rr = get_round_rr(id, Right.manage_round, True)
+    round, _, _ = get_round_rr(id, Right.manage_round, True)
 
     task = sess.query(db.Task).get(task_id)
     # FIXME: Check contest!
@@ -286,14 +310,14 @@ def org_round_task_edit(id: int, task_id: int):
 
 @app.route('/org/contest/r/<int:round_id>/task/<int:task_id>/download', methods=('GET', 'POST'))
 def org_round_task_download(round_id: int, task_id: int):
-    round, rr = get_round_rr(round_id, Right.view_submits, False)
+    round, _, _ = get_round_rr(round_id, Right.view_submits, False)
     task = get_task(round, task_id)
     return generic_batch_download(round=round, contest=None, site=None, task=task)
 
 
 @app.route('/org/contest/r/<int:round_id>/task/<int:task_id>/upload', methods=('GET', 'POST'))
 def org_round_task_upload(round_id: int, task_id: int):
-    round, rr = get_round_rr(round_id, Right.view_submits, False)
+    round, _, rr = get_round_rr(round_id, Right.view_submits, False)
     task = get_task(round, task_id)
     return generic_batch_upload(round=round, contest=None, site=None, task=task,
                                 can_upload_solutions=rr.can_upload_solutions(round),
@@ -302,20 +326,20 @@ def org_round_task_upload(round_id: int, task_id: int):
 
 @app.route('/org/contest/r/<int:round_id>/task/<int:task_id>/batch-points', methods=('GET', 'POST'))
 def org_round_task_batch_points(round_id: int, task_id: int):
-    round, rr = get_round_rr(round_id, Right.edit_points, True)
+    round, _, _ = get_round_rr(round_id, Right.edit_points, True)
     task = get_task(round, task_id)
     return generic_batch_points(round=round, contest=None, task=task)
 
 
 @app.route('/org/contest/r/<int:id>/list', methods=('GET', 'POST'))
 def org_round_list(id: int):
-    round, rr = get_round_rr(id, Right.manage_contest, True)
+    round, master_round, r = get_round_rr(id, Right.manage_contest, True)
     format = request.args.get('format', "")
 
     filter = ParticipantsFilterForm(request.args)
     filter.validate()
     query = get_contestants_query(
-        round=round,
+        round=master_round,
         school=filter.f_school,
         contest_place=filter.f_contest_place,
         participation_place=filter.f_participation_place,
@@ -323,7 +347,7 @@ def org_round_list(id: int):
     )
 
     action_form = ParticipantsActionForm()
-    if action_form.do_action(round=round, query=query):
+    if action_form.do_action(round=master_round, query=query):
         # Action happened, redirect
         return redirect(request.url)
 
@@ -345,8 +369,8 @@ def org_round_list(id: int):
 
 @app.route('/org/contest/r/<int:id>/import', methods=('GET', 'POST'))
 def org_round_import(id: int):
-    round, rr = get_round_rr(id, Right.manage_contest, True)
-    return generic_import(round, None)
+    round, master_round, rr = get_round_rr(id, Right.manage_contest, True)
+    return generic_import(round, master_round, None, None)
 
 
 class MODateTimeField(wtforms.DateTimeField):
@@ -355,7 +379,7 @@ class MODateTimeField(wtforms.DateTimeField):
         super().__init__(label, format=format, description=description, **kwargs)
 
     def process_data(self, valuelist):
-        super().process_formdata(valuelist)
+        super().process_data(valuelist)
         if self.data is not None:
             self.data = self.data.astimezone()
 
@@ -366,6 +390,7 @@ class MODateTimeField(wtforms.DateTimeField):
 
 
 class RoundEditForm(FlaskForm):
+    name = wtforms.StringField("Název")
     state = wtforms.SelectField("Stav kola", choices=db.RoundState.choices(), coerce=db.RoundState.coerce)
     # Only the desktop Firefox does not support datetime-local field nowadays,
     # other browsers does provide date and time picker UI :(
@@ -389,7 +414,7 @@ class RoundEditForm(FlaskForm):
 @app.route('/org/contest/r/<int:id>/edit', methods=('GET', 'POST'))
 def org_round_edit(id: int):
     sess = db.get_session()
-    round, rr = get_round_rr(id, Right.manage_round, True)
+    round, _, rr = get_round_rr(id, Right.manage_round, True)
 
     form = RoundEditForm(obj=round)
     if form.validate_on_submit():
@@ -420,7 +445,7 @@ def org_round_edit(id: int):
 
 @app.route('/org/contest/r/<int:id>/task-statement/zadani.pdf')
 def org_task_statement(id: int):
-    round, rr = get_round_rr(id, None, True)
+    round, _, rr = get_round_rr(id, None, True)
 
     if not rr.can_view_statement(round):
         app.logger.warn(f'Organizátor #{g.user.user_id} chce zadání, na které nemá právo')
diff --git a/mo/web/org_score.py b/mo/web/org_score.py
index 4925de52983b4623069b642da1d8207bc0d6bc4a..190e5e44911f393116c8f34d6597b0c15a440b87 100644
--- a/mo/web/org_score.py
+++ b/mo/web/org_score.py
@@ -1,6 +1,6 @@
 from flask import render_template, request, g
 from flask.helpers import url_for
-from typing import Optional
+from typing import List, Optional
 import werkzeug.exceptions
 
 import mo
@@ -102,6 +102,16 @@ def org_score(round_id: Optional[int] = None, contest_id: Optional[int] = None):
     results = score.get_sorted_results()
     messages = score.get_messages()
 
+    # Pro tvorbu odkazů na správné contesty ve výsledkovkách dělených kol
+    all_subcontests: List[db.Contest] = sess.query(db.Contest).filter(
+        db.Contest.round_id.in_(
+            sess.query(db.Round.round_id).filter_by(master_round_id=round.master_round_id)
+        )
+    ).all()
+    subcontest_id_map = {}
+    for subcontest in all_subcontests:
+        subcontest_id_map[(subcontest.round_id, subcontest.master_contest_id)] = subcontest.contest_id
+
     # Construct columns
     is_export = (format != "")
     columns = []
@@ -123,13 +133,14 @@ def org_score(round_id: Optional[int] = None, contest_id: Optional[int] = None):
     for task in tasks:
         title = task.code
         if contest_id:
+            local_ct_id = subcontest_id_map[(task.round_id, contest.master_contest_id)]
             title = '<a href="{}">{}</a>'.format(
-                url_for('org_contest_task', contest_id=contest_id, task_id=task.task_id),
+                url_for('org_contest_task', contest_id=local_ct_id, task_id=task.task_id),
                 task.code
             )
             if rr.can_edit_points(round):
                 title += ' <a href="{}" title="Editovat body" class="icon">✎</a>'.format(
-                    url_for('org_contest_task_points', contest_id=contest_id, task_id=task.task_id),
+                    url_for('org_contest_task_points', contest_id=local_ct_id, task_id=task.task_id),
                 )
         columns.append(Column(key=f'task_{task.task_id}', name=task.code, title=title))
     columns.append(Column(key='total_points', name='celkove_body', title='Celkové body'))
@@ -140,13 +151,14 @@ def org_score(round_id: Optional[int] = None, contest_id: Optional[int] = None):
     for result in results:
         user, pant, pion = result.user, result.pant, result.pion
         school = pant.school_place
+        local_pion_ct_id = subcontest_id_map[(round.round_id, pion.contest_id)]
         row = Row(keys={
             'order':        OrderCell(result.order.place, result.order.span, result.order.continuation),
             'winner':       'ano' if result.winner else '',
             'successful':   'ano' if result.successful else '',
             'user':         user,
             'email':        user.email,
-            'participant':  cell_pion_link(user, pion.contest_id, user.full_name()),
+            'participant':  cell_pion_link(user, local_pion_ct_id, user.full_name()),
             'contest':      CellLink(pion.contest.place.name, url_for('org_contest', id=pion.contest_id)),
             'pion_place':   pion.place.name,
             'school':       CellLink(school.name, url_for('org_place', id=school.place_id)),
@@ -157,8 +169,9 @@ def org_score(round_id: Optional[int] = None, contest_id: Optional[int] = None):
         })
         sols = result.get_sols_map()
         for task in tasks:
+            local_sol_ct_id = subcontest_id_map[(task.round_id, pion.contest_id)]
             row.keys[f'task_{task.task_id}'] = SolPointsCell(
-                contest_id=pion.contest_id, user=user, sol=sols.get(task.task_id)
+                contest_id=local_sol_ct_id, user=user, sol=sols.get(task.task_id)
             )
         if result.winner:
             row.html_attr = {"class": "winner", "title": "Vítěz"}
@@ -176,11 +189,15 @@ def org_score(round_id: Optional[int] = None, contest_id: Optional[int] = None):
         filename=filename,
     )
 
+    group_rounds = round.get_group_rounds(True)
+    group_rounds.sort(key=lambda r: r.round_code())
+
     if format == "":
         return render_template(
             'org_score.html',
             contest=contest, round=round, tasks=tasks,
             table=table, messages=messages,
+            group_rounds=group_rounds,
         )
     else:
         return table.send_as(format)
diff --git a/mo/web/org_users.py b/mo/web/org_users.py
index 1a9fffa2e9838cae834c2a7bf241a1c85314ccfc..5937abc2cd8c1c17e5b9e2ca9d89d8e582db3642 100644
--- a/mo/web/org_users.py
+++ b/mo/web/org_users.py
@@ -301,12 +301,21 @@ def org_user(id: int):
     rr = g.gatekeeper.rights_generic()
 
     participants = sess.query(db.Participant).filter_by(user_id=user.user_id)
-    rounds = sess.query(db.Participation).filter_by(user_id=user.user_id)
+    participations = (
+        sess.query(db.Participation, db.Contest, db.Round)
+        .select_from(db.Participation)
+        .join(db.Contest, db.Contest.master_contest_id == db.Participation.contest_id)
+        .join(db.Round)
+        .filter(db.Participation.user == user)
+        .options(joinedload(db.Contest.place))
+        .order_by(db.Round.year.desc(), db.Round.category, db.Round.seq, db.Round.part)
+        .all()
+    )
 
     return render_template(
         'org_user.html', user=user, can_edit=rr.can_edit_user(user),
         can_incarnate=g.user.is_admin,
-        participants=participants, rounds=rounds
+        participants=participants, participations=participations,
     )
 
 
diff --git a/mo/web/templates/org_contest.html b/mo/web/templates/org_contest.html
index afed4ec18db6334194cd06b8f18d07e7de922a64..5a1cc62b44445c7e83117bfd84c20deb6f579faa 100644
--- a/mo/web/templates/org_contest.html
+++ b/mo/web/templates/org_contest.html
@@ -18,6 +18,14 @@
 	{% endif %}
 	<tr><td>Stav<td class='rstate-{{round.state.name}}'>{{ round.state.friendly_name() }}
 	<tr><td>Vaše role<td>{% if roles %}{{ roles|join(", ") }}{% else %}–{% endif %}
+	{% if group_contests|length > 1 %}
+	<tr><td>Soutěže ve skupině kol:<td>
+		{% for c in group_contests %}
+			{% if c == contest %}<i>{% else %}<a href="{{ url_for('org_contest', id=c.contest_id) }}">{% endif %}
+				{{ c.round.round_code() }}: {% if site %}soutěžní místo {{ site.name }}{% else %}{{ contest.place.name }}{% endif %}
+			{% if c == contest %}</i>{% else %}</a>{% endif %}<br>
+		{% endfor %}
+	{% endif %}
 	<tr><td>Zadání<td>
 {% if round.tasks_file %}
 	{% if can_view_statement %}
diff --git a/mo/web/templates/org_contest_solutions.html b/mo/web/templates/org_contest_solutions.html
index dcb73a6f713c7b2386521b024525a0d7ee0ba0d6..623a82bc69c9b95e6d4ea27a5996c4bed16cf1da 100644
--- a/mo/web/templates/org_contest_solutions.html
+++ b/mo/web/templates/org_contest_solutions.html
@@ -61,7 +61,7 @@ konkrétní úlohu. Symbol <span class="icon">🗐</span> značí, že existuje
 	{% for pion in pions %}
 	{% set u = pion.user %}
 	<tr class="state-{{ pion.state.name }}{% if u.is_test %} testuser{% endif %}" {% if u.is_test %}title="Testovací uživatel"{% endif %}>
-		<th>{{ u|pion_link(pion.contest_id) }}
+		<th>{{ u|pion_link(contest.contest_id) }}
 		<td>{{ pion.state.friendly_name() }}
 		{% set sum_points = [] %}
 		{% for task in tasks %}
diff --git a/mo/web/templates/org_round.html b/mo/web/templates/org_round.html
index f8109b80a85e7ed910240225a6f5ee5de95d8820..9f4a4055be4ea6e2f02a5fad8e7ee77aab0e8b00 100644
--- a/mo/web/templates/org_round.html
+++ b/mo/web/templates/org_round.html
@@ -12,10 +12,20 @@
 	<tr><td>Ročník<td>{{ round.year }}
 	<tr><td>Kategorie<td>{{ round.category }}
 	<tr><td>Pořadí<td>{{ round.seq }}
-	<tr><td>Název<td>{{ round.name }}
+	{% if round.part > 0 %}<tr><td>Část:<td>{{ round.part_code() }}{% endif %}
 	<tr><td>Oblast<td>{{ level_names[round.level] }}
-	<tr><td>Stav<td class='rstate-{{round.state.name}}'>{{ round.state.friendly_name() }}
 	<tr><td>Vaše role<td>{% if roles %}{{ roles|join(", ") }}{% else %}–{% endif %}
+	{% if group_rounds|length > 1 %}
+	<tr><td>Skupina kol:<td>
+		{% for r in group_rounds %}
+			{% if r == round %}<i>{{ r.name }} {{ r.round_code() }} (toto kolo)</i><br>
+			{% else %}<a href="{{ url_for('org_round', id=r.round_id) }}">{{ r.name }} {{ r.round_code() }}</a><br>
+			{% endif %}
+		{% endfor %}
+	{% endif %}
+	<tr><th colspan=2><h4>Nastavení kola:</h4>
+	<tr><td>Název<td>{{ round.name }}
+	<tr><td>Stav<td class='rstate-{{round.state.name}}'>{{ round.state.friendly_name() }}
 	<tr><td>Účastníci vidí zadání od<td>{{ round.ct_tasks_start|timeformat }}
 	<tr><td>Účastníci odevzdávají do<td>{{ round.ct_submit_end|timeformat }}
 	<tr><td>Dozor vidí zadání od<td>{{ round.pr_tasks_start|timeformat }}
@@ -119,7 +129,7 @@
 			</div>
 			{% endif %}
 			{% if can_handle_submits or can_upload %}
-			<td><dic class="btn-group">
+			<td><div class="btn-group">
 				{% if can_handle_submits %}
 					<a class="btn btn-xs btn-primary" href="{{ url_for('org_round_task_download', round_id=round.round_id, task_id=task.task_id) }}">Stáhnout ZIP</a>
 				{% endif %}
diff --git a/mo/web/templates/org_rounds.html b/mo/web/templates/org_rounds.html
index 9f7a261898001e57397aa8df10544452f09d66fa..27e362aff57d83492a95a73d16923796199229b6 100644
--- a/mo/web/templates/org_rounds.html
+++ b/mo/web/templates/org_rounds.html
@@ -17,7 +17,7 @@
 			<td><a href='{{ url_for('org_round', id=r.round_id) }}'>{{ r.round_code() }}</a>
 			<td>{{ r.year }}
 			<td>{{ r.category }}
-			<td>{{ r.seq }}
+			<td>{{ r.seq }}{{ r.part_code() }}
 			<td>{{ level_names[r.level] }}
 			<td>{{ r.name }}
 			<td class='rstate-{{r.state.name}}'>{{ r.state.friendly_name() }}
diff --git a/mo/web/templates/org_score.html b/mo/web/templates/org_score.html
index 15ae4bebd5e42a2953787808516afefd50b9be27..8c1872548411d8d3387e6c4f88546f91da0a4372 100644
--- a/mo/web/templates/org_score.html
+++ b/mo/web/templates/org_score.html
@@ -38,6 +38,12 @@
 </div>
 {% endif %}
 
+{% if group_rounds|length > 1 %}
+<p>Toto je <b>sdílená výsledková listina</b> pro několik kol:
+{% for r in group_rounds %}{% if loop.index > 1 %}, {% endif %}<a href="{{ url_for('org_round', id=r.round_id) }}">{{ r.round_code() }} {{ r.name }}</a>{% endfor %}.
+Jsou v ní započítány body ze všech úloh těchto kol.</p>
+{% endif %}
+
 <p>Mód této výsledkové listiny je <b>{{ round.score_mode.friendly_name() }}</b>.
 Diskvalifikovaní, odmítnuvší a nepřítomní účastníci jsou skryti, stejně tak testovací uživatelé.
 Export pod tabulkou obsahuje sloupce navíc.
diff --git a/mo/web/templates/org_user.html b/mo/web/templates/org_user.html
index b6160543a667411b87759eed5977dea5a94f5204..83423526c2ea907f22466e2945117910aabb51e6 100644
--- a/mo/web/templates/org_user.html
+++ b/mo/web/templates/org_user.html
@@ -52,27 +52,27 @@
 {% endif %}
 
 <h3>Účast v kolech</h3>
-{% if rounds.count() %}
+{% if participations %}
 <table class="data full">
 	<thead>
 		<tr>
 			<th>Ročník<th>Kategorie<th>Kolo<th>Místo<th>Stav účasti<th>Odkazy
 		</tr>
 	</thead>
-{% for round in rounds %}
+{% for (pion, contest, round) in participations %}
 	<tr>
-		<td>{{ round.contest.round.year }}
-		<td>{{ round.contest.round.category }}
-		<td>{{ round.contest.round.seq }}
-		<td><a href="{{ url_for('org_place', id=round.contest.place_id) }}">{{ round.contest.place.name }}</a>
-			{% if round.place_id != round.contest.place_id %}
-				<br>(ale soutěží v <a href="{{ url_for('org_place', id=round.place_id) }}">{{ round.place.name }}</a>)
+		<td>{{ round.year }}
+		<td>{{ round.category }}
+		<td>{{ round.seq }}{{ round.part_code() }}
+		<td><a href="{{ url_for('org_place', id=contest.place_id) }}">{{ contest.place.name }}</a>
+			{% if pion.place_id != contest.place_id %}
+				<br>(ale soutěží v <a href="{{ url_for('org_place', id=pion.place_id) }}">{{ pion.place.name }}</a>)
 			{% endif %}
 		<td>{{ round.state.friendly_name() }}
 		<td><div class="btn-group">
-			<a class="btn btn-xs btn-primary" href="{{ url_for('org_contest_user', contest_id=round.contest.contest_id, user_id=user.user_id) }}">Odevzdané úlohy</a>
-			<a class="btn btn-xs btn-default" href="{{ url_for('org_contest', id=round.contest.contest_id) }}">Stránka soutěže</a>
-			<a class="btn btn-xs btn-default" href="{{ url_for('org_round', id=round.contest.round.round_id) }}">Stránka kola</a>
+			<a class="btn btn-xs btn-primary" href="{{ url_for('org_contest_user', contest_id=contest.contest_id, user_id=user.user_id) }}">Odevzdané úlohy</a>
+			<a class="btn btn-xs btn-default" href="{{ url_for('org_contest', id=contest.contest_id) }}">Stránka soutěže</a>
+			<a class="btn btn-xs btn-default" href="{{ url_for('org_round', id=round.round_id) }}">Stránka kola</a>
 		{% if g.user.is_admin %}
 			<a class="btn btn-xs btn-default" href="{{ log_url('participant', user.user_id) }}">Historie</a>
 		{% endif %}
diff --git a/mo/web/templates/parts/org_solution_table.html b/mo/web/templates/parts/org_solution_table.html
index a9340f07d23895a0acdda8be4bfe5ef4d86cf693..3b9c625c98bce7448602b47de839301eebf8a19a 100644
--- a/mo/web/templates/parts/org_solution_table.html
+++ b/mo/web/templates/parts/org_solution_table.html
@@ -41,7 +41,7 @@ finální (ve výchozím stavu poslední nahrané).{% elif sc.allow_upload_solut
 		<td>{% if for_user %}
 			<a href='{{ url_for('org_contest_task', contest_id=ct_id, task_id=task.task_id) }}'>{{ task.code }} {{ task.name }}</a>
 		    {% else %}
-			{{ u|pion_link(obj.contest_id) }}</a>
+			{{ u|pion_link(contest.contest_id) }}</a>
 		    {% endif %}
 		{% if for_task %}<td>{{ obj.state.friendly_name() }}{% endif %}
 		{% if sol %}
diff --git a/mo/web/user.py b/mo/web/user.py
index f4ac0297b3e72e73939b74ce80e9676319ac14bf..5ebc54f2f02a4d563747a16c48f07014112da557 100644
--- a/mo/web/user.py
+++ b/mo/web/user.py
@@ -22,11 +22,11 @@ def user_index():
 
     pions = (sess.query(db.Participation, db.Contest, db.Round)
              .select_from(db.Participation)
-             .join(db.Contest)
+             .join(db.Contest, db.Contest.master_contest_id == db.Participation.contest_id)
              .join(db.Round)
              .filter(db.Participation.user == g.user)
              .options(joinedload(db.Contest.place))
-             .order_by(db.Round.year.desc(), db.Round.category, db.Round.seq)
+             .order_by(db.Round.year.desc(), db.Round.category, db.Round.seq, db.Round.part)
              .all())
 
     return render_template(
@@ -46,7 +46,7 @@ def get_contest(id: int) -> db.Contest:
 
     # FIXME: Kontrolovat nějak pion.state?
     pion = (db.get_session().query(db.Participation)
-            .filter_by(user=g.user, contest=contest)
+            .filter_by(user=g.user, contest_id=contest.master_contest_id)
             .one_or_none())
     if not pion:
         raise werkzeug.exceptions.Forbidden()