diff --git a/bin/register-year b/bin/register-year
index 2799b375381c193a9a335c609521baad5c972276..1b1da151e99ff972259b4914a4aea65b7e56aa9f 100755
--- a/bin/register-year
+++ b/bin/register-year
@@ -13,7 +13,7 @@ parser.add_argument('--uid', type=int, help='ID uživatele')
 parser.add_argument('--year', type=int, required=True, help='ročník MO')
 parser.add_argument('--school', type=str, required=True, help='kód školy')
 parser.add_argument('--grade', type=str, required=True, help='třída')
-parser.add_argument('--birth-year', type=int, required=True, help='rok narození')
+parser.add_argument('--birth-year', type=int, help='rok narození')
 
 args = parser.parse_args()
 init_standalone()
diff --git a/db/db.ddl b/db/db.ddl
index 7727df20ff54f487e12cde0dfc1d019fb1baf35e..a7c33b3947513858dc913605bdf69aee215bfc6f 100644
--- a/db/db.ddl
+++ b/db/db.ddl
@@ -179,7 +179,7 @@ CREATE TABLE participants (
 	user_id		int		NOT NULL REFERENCES users(user_id),
 	year		int		NOT NULL,			-- ročník MO
 	school		int		NOT NULL REFERENCES places(place_id),
-	birth_year	int		NOT NULL,
+	birth_year	int,
 	grade		varchar(20)	NOT NULL,			-- třída ve tvaru "X/Y"
 	registered_on	timestamp with time zone	DEFAULT NULL,	-- kdy se účastník přihlásil (NULL, pokud ho přihlásil organizátor)
 	PRIMARY KEY (user_id, year)
diff --git a/db/upgrade-20230929.sql b/db/upgrade-20230929.sql
new file mode 100644
index 0000000000000000000000000000000000000000..576c8e28e870ceff20aa26490566cebd2e827bb6
--- /dev/null
+++ b/db/upgrade-20230929.sql
@@ -0,0 +1,4 @@
+SET ROLE 'mo_osmo';
+
+ALTER TABLE participants
+	ALTER COLUMN birth_year DROP NOT NULL;
diff --git a/mo/db.py b/mo/db.py
index 33f4156b179be7e3779a3e791255df41609060ad..434f35594c8d908405d170365eb41eff3641edb9 100644
--- a/mo/db.py
+++ b/mo/db.py
@@ -541,7 +541,7 @@ class Participant(Base):
     user_id = Column(Integer, ForeignKey('users.user_id'), primary_key=True, nullable=False)
     year = Column(Integer, primary_key=True, nullable=False)
     school = Column(Integer, ForeignKey('places.place_id'), nullable=False)
-    birth_year = Column(Integer, nullable=False)
+    birth_year = Column(Integer, nullable=True)
     grade = Column(String(20), nullable=False)
     registered_on = Column(DateTime(True))
 
diff --git a/mo/imports.py b/mo/imports.py
index 18f86059814d695f8353df687010395a108bcb25..41c183801fdb9f22c1caa9b849e79ec64e736519 100644
--- a/mo/imports.py
+++ b/mo/imports.py
@@ -76,16 +76,25 @@ class Import:
         self.to_log = []
         self.user = user
 
-    def error(self, msg: str) -> Any:
+    def _format_message(self, msg: str) -> str:
         if self.line_number > 0:
             if self.row_name:
-                msg = f"Řádek {self.line_number} ({self.row_name}): {msg}"
+                return f"Řádek {self.line_number} ({self.row_name}): {msg}"
             else:
-                msg = f"Řádek {self.line_number}: {msg}"
+                return f"Řádek {self.line_number}: {msg}"
+        return msg
+
+    def error(self, msg: str) -> Any:
+        msg = self._format_message(msg)
         self.errors.append(msg)
-        logger.info('Import: >> %s', msg)
+        logger.info('Import: E> %s', msg)
         return None     # Kdyby bylo otypováno správně jako -> None, při volání by si mypy stěžoval
 
+    def warning(self, msg: str) -> None:
+        msg = self._format_message(msg)
+        self.warnings.append(msg)
+        logger.info('Import: W> %s', msg)
+
     def parse_user_id(self, user_id_str: str) -> Optional[int]:
         if user_id_str == "":
             return self.error('Chybí ID uživatele')
@@ -95,9 +104,12 @@ class Import:
         except ValueError:
             return self.error('ID uživatele není číslo')
 
-    def parse_email(self, email: str) -> Optional[str]:
+    def parse_email(self, email: str, optional: bool) -> Optional[str]:
         if email == "":
-            return self.error('Chybí e-mailová adresa')
+            if optional:
+                return ""
+            else:
+                return self.error('Chybí e-mailová adresa')
 
         try:
             # XXX: Zde si nemůžeme dovolit kontrolovat existenci domén,
@@ -204,12 +216,11 @@ class Import:
 
     def find_or_create_user(self, email: str, krestni: Optional[str], prijmeni: Optional[str], is_org: bool) -> Optional[db.User]:
         try:
-            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.")
+            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:
@@ -378,12 +389,14 @@ class Import:
             return False
 
     def get_row_name(self, row: mo.csv.Row) -> Optional[str]:
-        if hasattr(row, 'email'):
-            # čtení prvku potomka
+        # čteme prvky potomka třídy Row
+        if hasattr(row, 'email') and row.email:
             return row.email  # type: ignore
+        if hasattr(row, 'krestni') and hasattr(row, 'prijmeni') and row.krestni and row.prijmeni:
+            return row.krestni + ' ' + row.prijmeni  # type: ignore
         return None
 
-    def generic_import(self, path: str) -> bool:
+    def generic_import(self, path: str, despite_warnings: bool) -> bool:
         charset = self.fmt.get_charset()
         if charset != 'utf-8' and self.check_utf8(path):
             logger.info('Import: Uhodnuto kódování utf-8')
@@ -412,7 +425,7 @@ class Import:
             self.line_number += 1
         self.row_name = None
 
-        return len(self.errors) == 0
+        return len(self.errors) == 0 and (len(self.warnings) == 0 or despite_warnings)
 
     def notify_users(self) -> None:
         # Projde všechny uživatele a těm, kteří ještě nemají nastavené heslo,
@@ -436,10 +449,10 @@ class Import:
         mo.csv.write(file=out, fmt=self.fmt, row_class=self.row_class, rows=[self.row_example])
         return out.getvalue()
 
-    def run(self, path: str) -> bool:
+    def run(self, path: str, despite_warnings: bool = False) -> bool:
         self.log_start(path)
 
-        if not self.generic_import(path):
+        if not self.generic_import(path, despite_warnings):
             logger.info('Import: Rollback')
             db.get_session().rollback()
             return False
@@ -478,6 +491,7 @@ class ContestImport(Import):
     log_msg_prefix = 'Účastníci'
     log_details = {'action': 'import'}
     template_basename = 'sablona-ucast'
+    email_last_seen: Dict[str, int]
 
     def __init__(
          self,
@@ -499,13 +513,14 @@ class ContestImport(Import):
             self.row_example.kod_skoly = default_place.get_code()
         else:
             self.row_example.kod_skoly = '#3333'
+        self.email_last_seen = {}
         assert not self.round.is_subround()
 
     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)
+        email = self.parse_email(r.email, optional=True)
         krestni = self.parse_name(r.krestni) if r.krestni else None
         prijmeni = self.parse_name(r.prijmeni) if r.prijmeni else None
         school_place = self.parse_school(r.kod_skoly) if r.kod_skoly else None
@@ -513,6 +528,7 @@ class ContestImport(Import):
             school_place = self.default_place
         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()}')
@@ -521,10 +537,18 @@ class ContestImport(Import):
         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):
+        if len(self.errors) > num_prev_errs:
             return
 
+        try:
+            email = mo.users.email_or_synthesize(email, krestni, prijmeni, rocnik, school_place)
+        except mo.CheckError as e:
+            return self.error(str(e))
+
+        if email in self.email_last_seen:
+            return self.error(f'Účastník není rozlišitelný od toho z řádku {self.email_last_seen[email]}. Případné jmenovce vyřešte ručním přidáním účastníka.')
+        self.email_last_seen[email] = self.line_number
+
         user = self.find_or_create_user(email, krestni, prijmeni, is_org=False)
         if user is None:
             return
@@ -539,6 +563,18 @@ class ContestImport(Import):
 
         self.find_or_create_participation(user, contest, misto)
 
+        # Kontrola na jmenovce
+        sess = db.get_session()
+        similar_users = (sess.query(db.User)
+                             .join(db.Participation)
+                             .filter(db.User.first_name == krestni)
+                             .filter(db.User.last_name == prijmeni)
+                             .filter(db.Participation.contest == contest)
+                             .filter(db.User != user)
+                             .all())
+        if similar_users:
+            self.warning('Účastník stejného jména a příjmení už na tomto místě soutěží, ověřte prosím, zda to není duplicita')
+
     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_contests} soutěží)'
 
@@ -595,7 +631,7 @@ class OrgsImport(Import):
     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)
+        email = self.parse_email(r.email, optional=False)
         krestni = self.parse_name(r.krestni)
         prijmeni = self.parse_name(r.prijmeni)
         role = self.parse_role(r.role)
diff --git a/mo/jobs/submit.py b/mo/jobs/submit.py
index 1a5c7560fecd0ee07e1575a26bf9e119847e908d..545afd87b5880918e214ad4117d2b399b20ed21e 100644
--- a/mo/jobs/submit.py
+++ b/mo/jobs/submit.py
@@ -10,13 +10,13 @@ from sqlalchemy.orm import joinedload
 import subprocess
 from tempfile import NamedTemporaryFile
 from typing import List, Optional
-import unicodedata
 import werkzeug.utils
 import zipfile
 
 import mo.db as db
 from mo.jobs import TheJob, job_handler
 from mo.submit import Submitter, SubmitException
+import mo.util
 from mo.util import logger, data_dir
 from mo.util_format import inflect_number, inflect_by_number, data_size
 
@@ -75,9 +75,7 @@ def handle_download_submits(the_job: TheJob):
         for p, u, task_code, place in papers:
             cnt += 1
             full_name = u.full_name()
-            ascii_name = (unicodedata.normalize('NFD', full_name)
-                          .encode('ascii', 'ignore')
-                          .decode('utf-8'))
+            ascii_name = mo.util.asciify(full_name)
             fn = f'{task_code}_{cnt:04d}_{u.user_id}_{p.paper_id}_{ascii_name}.pdf'
             fn = werkzeug.utils.secure_filename(fn)
             if want_subdirs:
diff --git a/mo/users.py b/mo/users.py
index bbfedf7f79469253d0576da0c69b93f68e1b4efe..be100b53cdcf9799b6f2ef1d874cd0c20de1cd18 100644
--- a/mo/users.py
+++ b/mo/users.py
@@ -158,11 +158,8 @@ def find_or_create_participant(user: db.User, year: int, school_id: Optional[int
                 school_id = prev_part.school
             else:
                 raise mo.CheckError('Osoba s daným e-mailem zatím není zaregistrovaná do ročníku, je nutné uvést školu.')
-        if not birth_year:
-            if prev_part:
-                birth_year = prev_part.birth_year
-            else:
-                raise mo.CheckError('Osoba s daným e-mailem zatím není zaregistrovaná do ročníku, je nutné uvést rok narození.')
+        if not birth_year and prev_part:
+            birth_year = prev_part.birth_year
         if not grade:
             raise mo.CheckError('Osoba s daným e-mailem zatím není zaregistrovaná do ročníku, je nutné uvést ročník.')
 
@@ -193,7 +190,7 @@ def find_or_create_participant(user: db.User, year: int, school_id: Optional[int
 
     if ((school_id and part.school != school_id)
             or (grade and part.grade != grade)
-            or (birth_year and part.birth_year != birth_year)):
+            or (birth_year is not None and part.birth_year is not None and part.birth_year != birth_year)):
         raise mo.CheckError('Účastník již zaregistrován s odlišnou školou/ročníkem/rokem narození')
 
     return part, is_new
@@ -293,8 +290,34 @@ def email_check_domain(domain: str):
     raise mo.CheckError(f'Doména {domain} nepřijímá poštu')
 
 
+def email_or_synthesize(addr: Optional[str], first_name: Optional[str], last_name: Optional[str], grade: Optional[str], school: Optional[db.Place]) -> str:
+    """
+    Pokud e-mailová adresa není zadaná, nějakou si vymyslíme podle jména, školy a ročníku.
+    Předpokládá, že parametry, které nejsou None, mají syntaxi zkontrolovanou.
+    """
+
+    if addr:
+        return addr
+
+    if not first_name or not last_name or not grade or school is None:
+        raise mo.CheckError('Pokud není zadaná e-mailová adresa, je nutné uvést jméno, příjmení, školu a ročník')
+
+    # Spočítáme ročník MO, kdy daný účastník chodil do 1. třídy
+    norm_grade = mo.util.normalize_grade(grade)
+    if norm_grade < 0:
+        ref_year = 0
+    else:
+        ref_year = config.CURRENT_YEAR - norm_grade + 1
+
+    x = mo.util.asciify(f'{school.place_id}-{ref_year}.{first_name}.{last_name}')
+    x = re.sub(r'\s+', '-', x)
+    x = re.sub(r'[^0-9a-zA-Z.-]', "", x)
+
+    return normalize_email(x + '@nomail')
+
+
 def normalize_email(addr: str, check_existence: bool = False, make_unique_nomail: bool = False) -> str:
-    if make_unique_nomail and addr.endswith('@nomail'):
+    if make_unique_nomail and addr == '@nomail':
         addr = base64.b32encode(secrets.token_bytes(10)).decode('US-ASCII').lower() + '@nomail'
 
     if '@' not in addr:
diff --git a/mo/util.py b/mo/util.py
index 0874e3a7768c18d4d9351f52b896f4c856317ab3..6f9335ded2e45b97c623ac0e80851bc04d3167c9 100644
--- a/mo/util.py
+++ b/mo/util.py
@@ -12,6 +12,7 @@ import secrets
 import sys
 from types import TracebackType
 from typing import Any, Optional, NoReturn, Tuple, TypeVar, List, Union
+import unicodedata
 
 import mo
 import mo.db as db
@@ -265,3 +266,9 @@ def magic_increment(t: str) -> Tuple[str, bool]:
         return m[1] + str(int(m[2]) + 1), True
     else:
         return t, False
+
+
+def asciify(s: str) -> str:
+    return (unicodedata.normalize('NFD', s)
+                       .encode('ascii', errors='ignore')
+                       .decode('utf-8'))
diff --git a/mo/web/imports.py b/mo/web/imports.py
index a9c19e040d2e819310ae20361ae0480b7cb4d324..efe5cb19c52c3f1684d32e5a797f62ff32a89a23 100644
--- a/mo/web/imports.py
+++ b/mo/web/imports.py
@@ -19,6 +19,7 @@ class ImportForm(FlaskForm):
         choices=FileFormat.choices(), coerce=FileFormat.coerce,
         default=FileFormat.cs_csv,
     )
+    despite_warnings = wtforms.BooleanField('Importovat navzdory varováním')
     submit = wtforms.SubmitField('Importovat')
     get_template = wtforms.SubmitField('Stáhnout šablonu')
 
@@ -35,7 +36,7 @@ def generic_import_page(form: ImportForm, imp: Optional[Import], redirect_url: s
                 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.run(import_tmp, despite_warnings=form.despite_warnings.data):
                     if imp.cnt_rows == 0:
                         flash('Soubor neobsahoval žádné řádky s daty', 'danger')
                     else:
@@ -48,7 +49,7 @@ def generic_import_page(form: ImportForm, imp: Optional[Import], redirect_url: s
                 flash('Vyberte si prosím soubor', 'danger')
         elif form.get_template.data:
             out = imp.get_template()
-            resp = app.make_response(out.encode(fmt.get_charset()))
+            resp = app.make_response(out.encode(fmt.get_charset(), errors='replace'))
             resp.content_type = fmt.get_content_type()
             resp.headers.add('Content-Disposition', 'attachment; filename=OSMO-' + imp.template_basename + '.' + fmt.get_extension())
             return resp
diff --git a/mo/web/org_contest.py b/mo/web/org_contest.py
index c9a043927c14356eea013320530ec2f24892eb67..caf515d4225572d06886af481989531fb627a3e5 100644
--- a/mo/web/org_contest.py
+++ b/mo/web/org_contest.py
@@ -1842,18 +1842,19 @@ def check_contest_state(round: db.Round, contest: Optional[db.Contest], state: d
 
 
 class ParticipantAddForm(FlaskForm):
-    email = mo_fields.Email(validators=[validators.DataRequired()], check_existence=True, make_unique_nomail=True)
+    email = mo_fields.Email(check_existence=True, make_unique_nomail=True)
     first_name = mo_fields.FirstName(validators=[validators.Optional()])
     last_name = mo_fields.LastName(validators=[validators.Optional()])
     school = mo_fields.School(validators=[validators.Optional()])
     grade = mo_fields.Grade(validators=[validators.Optional()])
-    birth_year = mo_fields.BirthYear(validators=[validators.Optional()])
+    birth_year = mo_fields.BirthYear(description='Není povinné uvádět.')
     participation_place = mo_fields.Place("Kód soutěžního místa")
     save = wtforms.SubmitField("Přidat")
 
     def set_descriptions(self, contest: db.Contest, place_desc: bool):
         self.email.description = ("Nepoužívejte prosím e-mailové adresy ve školních doménách, na které nejde posílat pošta z veřejné sítě. "
-                                  + "Pokud zatím neznáte e-mail, zadejte @nomail, ale pak adresu doplňte.")
+                                  + "Pokud zatím neznáte e-mail, zadejte @nomail, ale pak adresu doplňte. "
+                                  + "U účastníků ze základních škol můžete v nouzi adresu úplně vynechat.")
         self.school.description = f'Kód školy najdete v <a href="{url_for("org_place", id=contest.place.place_id)}">katalogu míst</a>.'
         if place_desc:
             self.participation_place.description = f'Pokud účastník soutěží někde jinde než {contest.place.name_locative()}, vyplňte <a href="{url_for("org_place", id=contest.place.place_id)}">kód místa</a>. Dozor na tomto místě pak může za účastníka odevzdávat řešení.'
@@ -1877,7 +1878,8 @@ def org_contest_add_user(ct_id: int, site_id: Optional[int] = None):
 
     if form.validate_on_submit():
         try:
-            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')
+            email = mo.users.email_or_synthesize(form.email.data, form.first_name.data, form.last_name.data, form.grade.data, form.school.get_place())
+            user, is_new_user, is_change_user_to_org = mo.users.find_or_create_user(email, 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 7dc43cd04e5a34168824b8c7bf83639a1615e054..8acae46f87cf4e72fa16e8642c768db988bfe041 100644
--- a/mo/web/org_users.py
+++ b/mo/web/org_users.py
@@ -572,7 +572,7 @@ def org_user_new():
 class ParticipantEditForm(FlaskForm):
     school = mo_fields.School("Škola", validators=[DataRequired()], render_kw={'autofocus': True})
     grade = mo_fields.Grade("Třída", validators=[DataRequired()])
-    birth_year = mo_fields.BirthYear("Rok narození", validators=[DataRequired()])
+    birth_year = mo_fields.BirthYear("Rok narození")
     submit = wtforms.SubmitField("Uložit")
 
 
diff --git a/mo/web/table.py b/mo/web/table.py
index 2d69af23a02d253e62f5d349cb50073dd4c702ba..0ceaf2757d4f9fa5a8b5fb00963ca42a3071e41d 100644
--- a/mo/web/table.py
+++ b/mo/web/table.py
@@ -187,6 +187,8 @@ class Table:
                 val = r.get(c.key)
                 if isinstance(val, Cell):
                     tab.append(val.to_html())
+                elif val is None:
+                    tab.append(f'\t<td>')
                 else:
                     tab.append(f'\t<td>{escape(str(val))}')
 
@@ -239,7 +241,7 @@ class Table:
             r = [row.get(c.key) for c in export_columns]
             writer.writerow(r)
 
-        return out.getvalue().encode(fmt.get_charset())
+        return out.getvalue().encode(fmt.get_charset(), errors='replace')
 
     def to_csv_stream(self, fmt: FileFormat, export_columns: List[Column]) -> Iterable[bytes]:
         out = io.StringIO()
@@ -252,12 +254,12 @@ class Table:
 
             nrows += 1
             if nrows >= 100:
-                yield out.getvalue().encode(fmt.get_charset())
+                yield out.getvalue().encode(fmt.get_charset(), errors='replace')
                 out.seek(0)
                 out.truncate()
                 nrows = 0
 
-        yield out.getvalue().encode(fmt.get_charset())
+        yield out.getvalue().encode(fmt.get_charset(), errors='replace')
 
     def send_as(self, format: Union[FileFormat, str], streaming: bool = False, args: Union[None, MultiDict, ImmutableMultiDict] = None) -> Response:
         try:
diff --git a/mo/web/templates/doc_import.html b/mo/web/templates/doc_import.html
index 8a03cac53cbb40c3c1b338e52176c79eda70089c..ee35f3728082aa6b8f102869a90d1c91759d45f2 100644
--- a/mo/web/templates/doc_import.html
+++ b/mo/web/templates/doc_import.html
@@ -24,14 +24,16 @@ když přidáte vlastní sloupce s novými názvy, budou se ignorovat.
 
 <table class=data>
 	<tr><th>Název<th>Obsah
-	<tr><td><b>email</b><td>E-mailová adresa
+	<tr><td><b>email</b><td>E-mailová adresa.
+		V kategoriích pro základní školy připouštíme účastníky bez e-mailové adresy,
+		kteří jsou identifikovaní jménem, příjmením, školou a rokem zahájení školní docházky.
 	<tr><td><i>krestni</i><td>Křestní jméno
 	<tr><td><i>prijmeni</i><td>Příjmení
 	<tr><td><i>kod_skoly</i><td>Kód školy (viz katalog škol na tomto webu).
-		Pokud importujete do soutěže ve školním kole, můžete kód školy vynechat
+		Pokud importujete do soutěže v domácím nebo školním kole, můžete kód školy vynechat
 		a doplní se vaše škola.
 	<tr><td><i>rocnik</i><td>Navštěvovaný ročník (třída). Pro základní školy je to číslo od 1 do 9, pro <i>k</i>-tý ročník <i>r</i>-leté střední školy má formát <i>k</i>/<i>r</i>.
-	<tr><td><i>rok_naroz</i><td>Rok narození
+	<tr><td>rok_naroz<td>Rok narození (nepovinný, ale doporučujeme uvádět)
 	<tr><td>kod_mista<td>Pokud účastník soutěží někde jinde, je zde uveden kód oblasti, školy,
 		nebo speciálního soutěžního místa, kde se soutěž koná. Dozor na soutěžním místě
 		má pak právo odevzdávat za účastníka řešení.
diff --git a/mo/web/templates/org_contest_add_user.html b/mo/web/templates/org_contest_add_user.html
index fe181f2379b20d2247f56a900b49dcae74bbf22c..c4dfd50da2bac3bee97e818724c9aeb86599d5fb 100644
--- a/mo/web/templates/org_contest_add_user.html
+++ b/mo/web/templates/org_contest_add_user.html
@@ -10,7 +10,7 @@
 
 {% block body %}
 
-<p>Jméno, škola, ročník a rok narození nejsou povinné pro již registrované účty.</p>
+<p>Jméno, škola, ročník nejsou povinné pro již registrované účty.</p>
 
 {{ wtf.quick_form(form, form_type='simple', button_map={'save': 'primary'}) }}
 
diff --git a/mo/web/templates/org_contest_user.html b/mo/web/templates/org_contest_user.html
index 91919e04160490e1e5c87012284f2bdd70658596..b70d3de90714f95f7d8cfa9cbd29dfe224239e24 100644
--- a/mo/web/templates/org_contest_user.html
+++ b/mo/web/templates/org_contest_user.html
@@ -35,7 +35,7 @@
 	<tr><td>E-mail:<td>{{ user.email|mailto }}{{ user|user_flags }}
 	<tr><td>Škola:<td><a href='{{ url_for('org_place', id=pant.school) }}'>{{ pant.school_place.name }}</a>
 	<tr><td>Třída:<td>{{ pant.grade }}
-	<tr><td>Rok narození:<td>{{ pant.birth_year }}
+	<tr><td>Rok narození:<td>{{ pant.birth_year|none_value('–') }}
 	<tr><td>Poznámka:<td style="white-space: pre-line;">{{ user.note }}
 	<thead>
 		<tr><th colspan='2'>Účast v kole
diff --git a/mo/web/templates/org_generic_import.html b/mo/web/templates/org_generic_import.html
index bd3906686258d0f34cdc270e67134f82b89e4d65..467c086efd647c287a246123afd2f06b9cef028b 100644
--- a/mo/web/templates/org_generic_import.html
+++ b/mo/web/templates/org_generic_import.html
@@ -14,7 +14,12 @@ Import dat {% if contest or round %}do {% if contest %}soutěže {{ contest.plac
 {% for e in warnings %}
 {{ e }}
 {% endfor %}
+{% if not errs %}
+
+<em>Aby se import provedl, zopakujte ho znovu se zaškrtnutým „Importovat navzdory varováním“.</em>
+{% endif %}
 </div>
+
 {% endif %}
 
 {% if errs %}
@@ -38,6 +43,7 @@ Import dat {% if contest or round %}do {% if contest %}soutěže {{ contest.plac
 {{ form.csrf_token }}
 {{ wtf.form_field(form.file) }}
 {{ wtf.form_field(form.fmt) }}
+{{ wtf.form_field(form.despite_warnings) }}
 {% block import_form %}{% endblock %}
 	<div class="btn-group">
 		{{ wtf.form_field(form.submit, class='btn btn-primary') }}
diff --git a/mo/web/templates/org_user.html b/mo/web/templates/org_user.html
index c8ff380bde1072979144cfeaec93af046effa2d0..313a25696dc925a0cf221c45cb3fd8fd9f022fcf 100644
--- a/mo/web/templates/org_user.html
+++ b/mo/web/templates/org_user.html
@@ -61,7 +61,7 @@
 		<td>{{ participant.year }}
 		<td><a href="{{ url_for('org_place', id=participant.school) }}">{{ participant.school_place.name }}</a>
 		<td>{{ participant.grade }}
-		<td>{{ participant.birth_year }}
+		<td>{{ participant.birth_year|none_value('–') }}
 		<td><div class="btn-group">
 			<a class="btn btn-xs btn-primary" href="{{ url_for('org_user_participant_edit', user_id=user.user_id, year=participant.year) }}">Editovat</a>
 			<form class="btn-group" method="POST" onsubmit="return confirm('Opravdu nenávratně smazat?')" action='{{ url_for('org_user_participant_delete', user_id=user.user_id, year=participant.year) }}'>
diff --git a/mo/web/templates/settings.html b/mo/web/templates/settings.html
index 29470c2e99b5fd2f6736897f51d2036b8c22c58a..50335f12b36d2aaae770f8dedb7ac131278de930 100644
--- a/mo/web/templates/settings.html
+++ b/mo/web/templates/settings.html
@@ -12,7 +12,7 @@
 {% if pant %}
 	<tr><th>Škola<td>{{ pant.school_place.name }}
 	<tr><th>Ročník<td>{{ pant.grade }}
-	<tr><th>Rok narození<td>{{ pant.birth_year }}
+	<tr><th>Rok narození<td>{{ pant.birth_year|none_value('–') }}
 {% endif %}
 	</table>
 
diff --git a/mo/web/user.py b/mo/web/user.py
index b0673e40a2a64945c06b9270969a8d9fb4dc4126..c429c0bb2fecc9775ba60c0c14850edbda87b9d7 100644
--- a/mo/web/user.py
+++ b/mo/web/user.py
@@ -89,7 +89,7 @@ class JoinRoundForm(FlaskForm):
     town_list = wtforms.HiddenField()
 
     grade = mo_fields.Grade("Třída", validators=[DataRequired()])
-    birth_year = mo_fields.BirthYear("Rok narození", validators=[DataRequired()])
+    birth_year = mo_fields.BirthYear("Rok narození")
     submit = wtforms.SubmitField('Přihlásit se')
 
 
diff --git a/mo/web/util.py b/mo/web/util.py
index 5e3d56eb009d68bff641ffd57a416f350eb18c1b..1a1008e7d5bd94643fc0d6ddd89ccdc681c38a6c 100644
--- a/mo/web/util.py
+++ b/mo/web/util.py
@@ -5,7 +5,6 @@ from flask_wtf import FlaskForm
 import os
 from sqlalchemy.orm.query import Query
 from typing import Tuple, Optional, Union
-import unicodedata
 import werkzeug.exceptions
 import werkzeug.utils
 import wtforms
@@ -75,9 +74,7 @@ def _task_paper_filename(user: db.User, paper: db.Paper) -> str:
     # Tato jména parsuje dávkový upload
 
     full_name = user.full_name()
-    ascii_name = (unicodedata.normalize('NFD', full_name)
-                  .encode('ascii', 'ignore')
-                  .decode('utf-8'))
+    ascii_name = mo.util.asciify(full_name)
 
     if paper.type == db.PaperType.solution:
         typ = 'reseni'