Browse Source

Merge branch 'data_migrations' of gimli.ms.mff.cuni.cz:/akce/mam/git/mamweb into data_migrations

middleware_test
Anet 5 years ago
parent
commit
6ce6ce9a46
  1. 54
      mamweb/static/css/mamweb.css
  2. 19
      seminar/admin.py
  3. 121
      seminar/migrations/0080_zruseni_claneknode_a_konferanode.py
  4. 2
      seminar/migrations/0081_auto_20200408_2221.py
  5. 100
      seminar/models.py
  6. 2
      seminar/templates/seminar/archiv/cisla.html
  7. 47
      seminar/templates/seminar/archiv/rocnik.html
  8. 7
      seminar/testutils.py
  9. 132
      seminar/views/views_all.py

54
mamweb/static/css/mamweb.css

@ -51,7 +51,10 @@ a:focus, a:hover, a:active {
color: #e84e10;
text-decoration: none;
}
a:focus a:hover
img {
filter: drop-shadow(0px 3px 3px rgba(0, 0, 0, 0.4));
}
h1 { /*todo: odlišit 1 a 2 */
font-size: 200%;
@ -594,6 +597,14 @@ div.org_pole, div.rocnik_pole {
text-align: center;
}
div.cislo_pole {
display: inline-block;
width: 15%;
min-width: 165px;
text-align: center;
padding: 10px;
}
div.seznam_orgu h3 {
text-align: center;
margin-top: 10px;
@ -623,6 +634,11 @@ div.org_email {
height: 298px;
}
#archiv-rocnik.flip-card {
width: 144px;
height: 205px;
}
/* This container is needed to position the front and back side */
.flip-card-inner {
position: relative;
@ -654,16 +670,13 @@ div.org_email {
div.flip-card-foto img {
width: 100%;
height: 100%;
filter: drop-shadow(0px 3px 3px rgba(0, 0, 0, 0.4)); /* FIXME: obecně k obrázkům */
}
/* Style the back side */
.flip-card-back {
/*background-color: #e84e10;
color: #fffbf6;
background-color: #fdedd5;*/
background-color: #f9d59e;
color: black;/**/
color: black;
transform: rotateY(180deg);
padding: 10px;
padding-top: 20px;
@ -678,26 +691,35 @@ div.popis_rocniku {
}
div.popis_rocniku a{
div.popis_rocniku a, div.cislo_odkazy a {
font-weight: bold;
color: black;
}
div.popis_rocniku a:hover{
div.popis_rocniku a:hover,
div.cislo_odkazy a:hover {
color: #6f2509;
}
/* graf na úvodní stránce */
a span.popup {
position: absolute;
visibility: hidden;
div.cislo_odkazy ul {
margin: 0px;
padding: 0px;
}
a span.popup:hover {
visibility:visible;
top:37px; left:37px;
/* archiv ročník
div.cisla-v-rocniku {
font-weight: bold;
color: #6f2509;
}
div.cislo-v-rocniku-blok {
display: inline-block;
width: 150px;
height: 220px;
text-align: center;
}*/
/* galerie */
/* velká fotka */

19
seminar/admin.py

@ -35,6 +35,7 @@ class ProblemAdmin(PolymorphicParentModelAdmin):
m.Tema,
m.Clanek,
m.Uloha,
m.Konfera,
]
@admin.register(m.Tema)
@ -52,6 +53,11 @@ class UlohaAdmin(PolymorphicChildModelAdmin):
base_model = m.Uloha
show_in_index = True
@admin.register(m.Konfera)
class KonferaAdmin(PolymorphicChildModelAdmin):
base_model = m.Konfera
show_in_index = True
class TextAdminInline(admin.TabularInline):
model = m.Text
exclude = ['text_zkraceny_set','text_zkraceny']
@ -82,7 +88,6 @@ class ReseniAdmin(ReverseModelAdmin):
admin.site.register(m.Hodnoceni)
admin.site.register(m.Pohadka)
admin.site.register(m.Konfera)
admin.site.register(m.Obrazek)
@ -97,8 +102,6 @@ class TreeNodeAdmin(PolymorphicParentModelAdmin):
m.CisloNode,
m.MezicisloNode,
m.TemaVCisleNode,
m.KonferaNode,
m.ClanekNode,
m.UlohaZadaniNode,
m.PohadkaNode,
m.UlohaVzorakNode,
@ -136,16 +139,6 @@ class TemaVCisleNodeAdmin(PolymorphicChildModelAdmin):
base_model = m.TemaVCisleNode
show_in_index = True
@admin.register(m.KonferaNode)
class KonferaNodeAdmin(PolymorphicChildModelAdmin):
base_model = m.KonferaNode
show_in_index = True
@admin.register(m.ClanekNode)
class ClanekNodeAdmin(PolymorphicChildModelAdmin):
base_model = m.ClanekNode
show_in_index = True
@admin.register(m.UlohaZadaniNode)
class UlohaZadaniNodeAdmin(PolymorphicChildModelAdmin):
base_model = m.UlohaZadaniNode

121
seminar/migrations/0080_zruseni_claneknode_a_konferanode.py

@ -0,0 +1,121 @@
# Generated by Django 2.2.12 on 2020-04-01 20:54
# Fixed by Pavel, 2020-01-04 20:56 UTC
# This is quite possibly a bug in Django.
from django.db import migrations, models
import django.db.models.deletion
def vyrob_dummy_problemy(apps, schema_editor):
Problem = apps.get_model('seminar', 'Problem')
Konfera = apps.get_model('seminar', 'Konfera')
for k in Konfera.objects.all():
pr = Problem.objects.create(nazev=k.nazev,
garant=k.organizator)
pr.save()
k.problem_ptr = pr
k.save()
class Migration(migrations.Migration):
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
('seminar', '0079_clanek_resitelsky'),
]
operations = [
migrations.DeleteModel(
name='Konfery_Ucastnici',
),
migrations.RemoveField(
model_name='konfera',
name='ucastnici',
),
migrations.CreateModel(
name='OrgTextNode',
fields=[
('treenode_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='seminar.TreeNode')),
('org_verejny', models.BooleanField(default=True, help_text='Pokud ano, bude org pod článkem podepsaný', verbose_name='Org je veřejný?')),
('organizator', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to='seminar.Organizator', verbose_name='Organizátor')),
],
options={
'verbose_name': 'Organizátorský článek (Node)',
'verbose_name_plural': 'Organizátorské články (Node)',
'db_table': 'seminar_nodes_orgtextnode',
},
bases=('seminar.treenode',),
),
migrations.RemoveField(
model_name='konfera',
name='id',
),
migrations.RenameModel(
old_name='OtisteneReseniNode',
new_name='ReseniNode',
),
migrations.RemoveField(
model_name='clanek',
name='cislo',
),
migrations.RemoveField(
model_name='clanek',
name='resitelsky',
),
migrations.RemoveField(
model_name='reseni',
name='text_zkraceny',
),
migrations.AddField(
model_name='konfera',
name='problem_ptr',
field=models.OneToOneField(auto_created=True, null=False, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='seminar.Problem'),
preserve_default=False,
),
migrations.RemoveField(
model_name='konfera',
name='nazev',
),
migrations.RemoveField(
model_name='konfera',
name='organizator',
),
migrations.RemoveField(
model_name='konfera',
name='poznamka',
),
migrations.RemoveField(
model_name='konfera',
name='reseni',
),
migrations.AlterField(
model_name='reseni',
name='text_cely',
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='reseni_cely_set', to='seminar.ReseniNode', verbose_name='Plná verze textu řešení'),
),
migrations.DeleteModel(
name='ClanekNode',
),
migrations.DeleteModel(
name='KonferaNode',
),
migrations.CreateModel(
name='Konfery_Ucastnici',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('poznamka', models.TextField(blank=True, help_text='Neveřejná poznámka k účasti (plain text)', verbose_name='neveřejná poznámka')),
('konfera', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='seminar.Konfera', verbose_name='konfera')),
('resitel', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='seminar.Resitel', verbose_name='řešitel')),
],
options={
'verbose_name': 'Účast na konfeře',
'verbose_name_plural': 'Účasti na konfeře',
'db_table': 'seminar_konfery_ucastnici',
'ordering': ['konfera', 'resitel'],
},
),
migrations.AddField(
model_name='konfera',
name='ucastnici',
field=models.ManyToManyField(help_text='Seznam účastníků konfery', through='seminar.Konfery_Ucastnici', to='seminar.Resitel', verbose_name='účastníci konfery'),
),
]

2
seminar/migrations/0080_auto_20200408_2221.py → seminar/migrations/0081_auto_20200408_2221.py

@ -7,7 +7,7 @@ import seminar.models
class Migration(migrations.Migration):
dependencies = [
('seminar', '0079_clanek_resitelsky'),
('seminar', '0080_zruseni_claneknode_a_konferanode'),
]
operations = [

100
seminar/models.py

@ -792,14 +792,6 @@ class Clanek(Problem):
verbose_name = 'Článek'
verbose_name_plural = 'Články'
cislo = models.ForeignKey(Cislo, verbose_name='číslo', blank=True, null=True,
on_delete=models.PROTECT)
resitelsky = models.BooleanField('Jde o řešitelský článek?', default=True)
# má OneToOneField s:
# ClanekNode
def kod_v_rocniku(self):
if self.stav == 'zadany':
# Nemělo by být potřeba
@ -808,15 +800,6 @@ class Clanek(Problem):
return "c{}".format(self.kod)
return '<Není zadaný>'
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
# *Node.save() aktualizuje název *Nodu.
try:
self.claneknode.save()
except ObjectDoesNotExist:
# Neexistující *Node nemá smysl aktualizovat.
pass
class Text(SeminarModelBase):
class Meta:
db_table = 'seminar_texty'
@ -926,13 +909,10 @@ class Reseni(SeminarModelBase):
forma = models.CharField('forma řešení', max_length=16, choices=FORMA_CHOICES, blank=False,
default=FORMA_EMAIL)
text_cely = models.OneToOneField(Text, verbose_name='Plná verze textu řešení',
text_cely = models.OneToOneField('ReseniNode', verbose_name='Plná verze textu řešení',
blank=True, null=True, related_name="reseni_cely_set",
on_delete=models.SET_NULL)
on_delete=models.PROTECT)
text_zkraceny = models.ManyToManyField(Text, verbose_name='zkrácené verze řešení',
help_text='Seznam úryvků z řešení',related_name="reseni_zkraceny_set")
poznamka = models.TextField('neveřejná poznámka', blank=True,
help_text='Neveřejná poznámka k řešení (plain text)')
@ -1141,25 +1121,18 @@ class Soustredeni_Organizatori(SeminarModelBase):
@reversion.register(ignore_duplicates=True)
class Konfera(models.Model):
class Konfera(Problem):
class Meta:
db_table = 'seminar_konfera'
verbose_name = 'Konfera'
verbose_name_plural = 'Konfery'
# Interní ID
id = models.AutoField(primary_key = True)
nazev = models.CharField('název konfery', max_length=100, help_text = 'Název konfery')
anotace = models.TextField('anotace', blank=True,
help_text='Popis, o čem bude konfera.')
abstrakt = models.TextField('abstrakt', blank=True,
help_text='Abstrakt konfery tak, jak byl uveden ve sborníku')
organizator = models.ForeignKey(Organizator, verbose_name='organizátor', related_name='konfery',
on_delete = models.SET_NULL, null=True)
# FIXME: Umíme omezit jen na účastníky daného soustřeďka?
ucastnici = models.ManyToManyField(Resitel, verbose_name='účastníci konfery',
help_text='Seznam účastníků konfery', through='Konfery_Ucastnici')
@ -1167,13 +1140,6 @@ class Konfera(models.Model):
soustredeni = models.ForeignKey(Soustredeni, verbose_name='soustředění',
related_name='konfery', on_delete = models.SET_NULL, null=True)
poznamka = models.TextField('neveřejná poznámka', blank=True,
help_text='Neveřejná poznámka ke konfeře(plain text)')
# Jedno reseni se vztahuje nejvyse k jedne konfere
reseni = models.OneToOneField(Reseni, verbose_name='článek ke konfeře', related_name='konfery',
help_text='Účastnický přípěvek o konfeře', on_delete = models.SET_NULL,
null=True, blank=True)
TYP_VELETRH = 'veletrh'
TYP_PREZENTACE = 'prezentace'
TYP_CHOICES = [
@ -1190,21 +1156,9 @@ class Konfera(models.Model):
help_text = 'Další materiály ke konfeře zabalené do jednoho souboru',
upload_to = generate_filename_konfera, blank=True)
# má OneToOneField s:
# KonferaNode
def __str__(self):
return "{}: ({})".format(self.nazev, self.soustredeni)
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
# *Node.save() aktualizuje název *Nodu.
try:
self.konferanode.save()
except ObjectDoesNotExist:
# Neexistující *Node nemá smysl aktualizovat.
pass
# Vazebna tabulka. Mozna se generuje automaticky.
@reversion.register(ignore_duplicates=True)
@ -1417,36 +1371,30 @@ class TemaVCisleNode(TreeNode):
def getOdkazStr(self):
return str(self.tema)
class KonferaNode(TreeNode):
class OrgTextNode(TreeNode):
class Meta:
db_table = 'seminar_nodes_konfera'
verbose_name = 'Konfera (Node)'
verbose_name_plural = 'Konfery (Node)'
konfera = models.OneToOneField(Konfera,
on_delete=models.PROTECT, # Pokud chci mazat téma, musím si Node pořešit ručně
verbose_name = "konfera",
null=True,
blank=False)
def aktualizuj_nazev(self):
self.nazev = "KonferaNode: "+str(self.konfera)
class ClanekNode(TreeNode):
class Meta:
db_table = 'seminar_nodes_clanek'
verbose_name = 'Článek (Node)'
verbose_name_plural = 'Články (Node)'
clanek = models.OneToOneField(Clanek,
on_delete=models.PROTECT, # Pokud chci mazat téma, musím si Node pořešit ručně
verbose_name = "článek",
null=True,
blank=False)
db_table = 'seminar_nodes_orgtextnode'
verbose_name = 'Organizátorský článek (Node)'
verbose_name_plural = 'Organizátorské články (Node)'
organizator = models.ForeignKey(Organizator,
null=False,
blank=False,
on_delete=models.DO_NOTHING,
verbose_name="Organizátor",
)
org_verejny = models.BooleanField(default = True,
verbose_name = "Org je veřejný?",
help_text = "Pokud ano, bude org pod článkem podepsaný",
null=False,
)
def aktualizuj_nazev(self):
self.nazev = "ClanekNode: "+str(self.clanek)
return f"OrgTextNode začínající následujícim: {self.first_child.nazev}"
def getOdkazStr(self):
return str(self.clanek)
# FIXME!!!
#def getOdkazStr(self):
# return str(self.clanek)
class UlohaZadaniNode(TreeNode):
@ -1527,7 +1475,7 @@ class CastNode(TreeNode):
def getOdkazStr(self):
return str(self.nadpis)
class OtisteneReseniNode(TreeNode):
class ReseniNode(TreeNode):
class Meta:
db_table = 'seminar_nodes_otistene_reseni'
verbose_name = 'Otištěné řešení (Node)'

2
seminar/templates/seminar/archiv/cisla.html

@ -40,7 +40,7 @@
{% for cislo in rocnik.cisla.all reversed %}
<li><a href='{{ cislo.verejne_url }}'>{{ cislo.poradi }}. číslo</a> {% if cislo.pdf %}(<a href='{{ cislo.pdf.url }}'>pdf</a>) {% endif %}
{% empty %}
---
Žádná čísla k zobrazení
{% endfor %}
</ul>
<a href='{{ rocnik.verejne_url }}'>Výsledková listina</a> <!-- FIXME: url výsledkovky-->

47
seminar/templates/seminar/archiv/rocnik.html

@ -17,14 +17,49 @@
</ul>
{% endif %}
<ul>
<div class="cisla-v-rocniku">
{% for c in rocnik.verejna_cisla %}
<li><a href="{{ c.verejne_url }}">Číslo {{ c.kod }}</a>
{% if c.pdf %}
(<a href='{{ c.pdf.url }}'>pdf</a>)
{% endif %}
<div class="cislo_pole">
<h6> Číslo {{ c.kod }}</h6>
<div class="flip-card" id="archiv-rocnik">
<div class="flip-card-inner">
<div class="flip-card-front">
<div class="flip-card-foto">
{% if c.titulka_nahled %}
<img src="{{ c.titulka_nahled.url }}" alt="{{ c.kod }}" height=180px>
{% else %}
<img src="" alt="no image" height=180px>
{% endif %}
</div>
</div>
<div class="flip-card-back">
<div class="cislo_odkazy">
<ul>
<li>
<a href="{{ c.verejne_url }}">archiv čísla</a>
</li>
{% if c.pdf %}
<li>
<a href='{{ c.pdf.url }}'>pdf</a>
</li>
{% endif %}
</ul>
</div>
</div>
</div>
</div>
</div>
{% endfor %}
</ul>
</div>
{% if vysledkovka %}
{% if user.is_staff %}

7
seminar/testutils.py

@ -9,7 +9,7 @@ from django.db import transaction
import unidecode
import logging
from seminar.models import Skola, Resitel, Rocnik, Cislo, Problem, Reseni, PrilohaReseni, Nastaveni, Soustredeni, Soustredeni_Ucastnici, Soustredeni_Organizatori, Osoba, Organizator, Prijemce, Tema, Uloha, Konfera, KonferaNode, TextNode, UlohaVzorakNode, RocnikNode, CisloNode, TemaVCisleNode, Text, Hodnoceni, UlohaZadaniNode, Novinky, TreeNode
from seminar.models import Skola, Resitel, Rocnik, Cislo, Problem, Reseni, PrilohaReseni, Nastaveni, Soustredeni, Soustredeni_Ucastnici, Soustredeni_Organizatori, Osoba, Organizator, Prijemce, Tema, Uloha, Konfera, TextNode, UlohaVzorakNode, RocnikNode, CisloNode, TemaVCisleNode, Text, Hodnoceni, UlohaZadaniNode, Novinky, TreeNode
from django.contrib.flatpages.models import FlatPage
from django.contrib.sites.models import Site
@ -339,7 +339,7 @@ def gen_konfery(size, rnd, organizatori, resitele, soustredeni):
nazev=rnd.choice(['Pozorování', 'Zkoumání', 'Modelování', 'Počítání', 'Zkoušení']) + rnd.choice([' vlastností', ' jevů', ' charakteristik']) + rnd.choice([' vektorových prostorů', ' kinetické terorie látek', ' molekulární biologie', ' syntentických stromů']),
anotace=lorem.paragraph(),
abstrakt=lorem.paragraph(),
organizator=rnd.choice(organizatori),
garant=rnd.choice(organizatori),
soustredeni=rnd.choice(soustredeni),
typ_prezentace=rnd.choice(['veletrh', 'prezentace']))
ucastnici_sous = list(konfera.soustredeni.ucastnici.all())
@ -349,9 +349,6 @@ def gen_konfery(size, rnd, organizatori, resitele, soustredeni):
# Konfery_Ucastnici.objects.create(resitel=res, konfera=konfera)
konfera.save()
konfery.append(konfera)
konferanode = KonferaNode.objects.create(konfera=konfera)
konferanode.save()
return konfery
def gen_cisla(rnd, rocniky):

132
seminar/views/views_all.py

@ -309,136 +309,16 @@ class ArchivView(generic.ListView):
def get_context_data(self, **kwargs):
context = super(ArchivView, self).get_context_data(**kwargs)
vyska = 594 # px
sirka = 420 # px
# první číslo z každého ročníku
cisla = Cislo.objects.filter(poradi=1)
# op == os.path, udělá z argumentů cestu
png_dir = op.join(settings.MEDIA_ROOT, "cislo", "png")
# slovník {(ročník, url obrázku)}
cisla = Cislo.objects.filter(poradi=1)
urls ={}
# for j, rocnik in enumerate(Rocnik.objects.all()):
# urls_rocnik = {}
# for i,c in enumerate(rocnik.cisla.all()):
# if not c.pdf:
# urls_rocnik[c.poradi] = op.join(settings.MEDIA_URL, "cislo", "png", "default.png")
# else:
# filename = os.path.split(c.pdf.file.name)[1].split(".")[0]
# png_filename = "{}.png".format(filename)
# # Pokud obrázek neexistuje nebo není aktuální, vytvoř jej
# png_path = op.join(png_dir, png_filename)
# if not op.exists(png_path) or \
# op.getmtime(png_path) < op.getmtime(c.pdf.path):
# subprocess.call([
# "convert",
# "-density", "300x300",
# "-geometry", "{}x{}".format(vyska, sirka),
# "-background", "white",
# "-flatten",
# "{}[0]".format(c.pdf.path), # titulní strana
# png_path
# ])
# urls_rocnik[c.poradi] = op.join(settings.MEDIA_URL, "cislo", "png", png_filename)
# urls[rocnik] = urls_rocnik
for i,c in enumerate(cisla):
if not c.pdf:
urls[c.rocnik] = op.join(settings.MEDIA_URL, "cislo", "png", "default.png")
else:
filename = os.path.split(c.pdf.file.name)[1].split(".")[0]
png_filename = "{}.png".format(filename)
# Pokud obrázek neexistuje nebo není aktuální, vytvoř jej
png_path = op.join(png_dir, png_filename)
if not op.exists(png_path) or \
op.getmtime(png_path) < op.getmtime(c.pdf.path):
subprocess.call([
"convert",
"-density", "300x300",
"-geometry", "{}x{}".format(vyska, sirka),
"-background", "white",
"-flatten",
"{}[0]".format(c.pdf.path), # titulní strana
png_path
])
urls[c.rocnik] = op.join(settings.MEDIA_URL, "cislo", "png", png_filename)
for i, c in enumerate(cisla):
if c.titulka_nahled:
urls[c.rocnik] = c.titulka_nahled.url
else:
urls[c.rocnik] = op.join(settings.MEDIA_URL, "cislo", "png", "default.png")
context["object_list"] = urls
print(context)
# for i, c in enumerate(cisla):
# if not c.pdf:
# continue
# filename = os.path.split(c.pdf.file.name)[1].split(".")[0]
# png_filename = "{}-{}px.png".format(filename, vyska)
# # Pokud obrázek neexistuje nebo není aktuální, vytvoř jej
# png_path = op.join(png_dir, png_filename)
# if not op.exists(png_path) or \
# op.getmtime(png_path) < op.getmtime(c.pdf.path):
# subprocess.call([
# "convert",
# "-density", "300x300",
# "-geometry", "{}x{}".format(vyska, sirka),
# "-background", "white",
# "-flatten",
# "-rotate", str(90 * i),
# "{}[0]".format(c.pdf.path), # titulní strana
# png_path
# ])
# urls.append(
# (op.join(settings.MEDIA_URL, "cislo", "png", png_filename), c)
# )
# vyska, sirka = sirka, vyska / 2
# tags = []
# def spirala(urls, tags, idx):
# """Rekurzivně prochází urls a generuje strom elementů do tags"""
# if idx >= len(urls):
# return
# img_url, cislo = urls[idx]
# tags.append(
# "<div style='top:{}%;left:{}%;width:{}%;height:{}%;'>"
# .format(
# 50 if idx % 4 == 2 else 0,
# 50 if idx % 4 == 1 else 0,
# 50 if idx % 2 == 1 else 100,
# 50 if idx > 0 and idx % 2 == 0 else 100
# )
# )
# tags.append("<a href='{}' title='{}'>".format(
# cislo.verejne_url(), cislo.kod()
# ))
# tags.append(
# "<img src='{}' style='top:{}%;left:{}%;width:{}%;height:{}%;'>"
# .format(
# img_url,
# 50 if idx % 4 == 3 else 0,
# 50 if idx % 4 == 2 else 0,
# 50 if idx % 2 == 0 else 100,
# 50 if idx % 2 == 1 else 100
# )
# )
# tags.append("</a>")
# spirala(urls, tags, idx + 1)
# tags.append("</div>")
# spirala(urls, tags, 0)
# context["nahledy"] = "\n".join(tags)
return context

Loading…
Cancel
Save