diff --git a/judge/admin/problem.py b/judge/admin/problem.py
index c78c0981f6..cb995f5ee0 100644
--- a/judge/admin/problem.py
+++ b/judge/admin/problem.py
@@ -125,7 +125,7 @@ class ProblemAdmin(NoBatchDeleteMixin, VersionAdmin):
'fields': (
'code', 'name', 'is_public', 'is_manually_managed', 'date', 'authors', 'curators', 'testers',
'organizations', 'submission_source_visibility_mode', 'is_full_markup',
- 'description', 'license',
+ 'description', 'include_test_cases', 'license',
),
}),
(_('Social Media'), {'classes': ('collapse',), 'fields': ('og_image', 'summary')}),
diff --git a/judge/migrations/0147_infer_test_cases_from_zip.py b/judge/migrations/0147_infer_test_cases_from_zip.py
new file mode 100644
index 0000000000..1297d33452
--- /dev/null
+++ b/judge/migrations/0147_infer_test_cases_from_zip.py
@@ -0,0 +1,85 @@
+# Generated by Django 3.2.25 on 2024-08-16 11:00
+
+from django.db import migrations, models
+
+import judge.models.problem
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('judge', '0146_comment_revision_count_v2'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='problem',
+ name='include_test_cases',
+ field=models.BooleanField(default=False, help_text='If true, the inputs and otuputs of every test case will be automatically added after the body.', verbose_name='include test cases'),
+ ),
+ migrations.AddField(
+ model_name='problemdata',
+ name='infer_from_zip',
+ field=models.BooleanField(blank=True, null=True, verbose_name='infer test cases from zip'),
+ ),
+ migrations.AddField(
+ model_name='problemdata',
+ name='test_cases_content',
+ field=models.TextField(blank=True, verbose_name='test cases content'),
+ ),
+ migrations.AddField(
+ model_name='problemtestcase',
+ name='is_private',
+ field=models.BooleanField(default=False, verbose_name='case is private?'),
+ ),
+ migrations.AlterField(
+ model_name='problem',
+ name='name',
+ field=models.CharField(db_index=True, help_text='The full name of the problem, as shown in the problem list.', max_length=100, validators=[judge.models.problem.disallowed_characters_validator], verbose_name='problem name'),
+ ),
+ migrations.AlterField(
+ model_name='problemdata',
+ name='checker',
+ field=models.CharField(blank=True, choices=[('standard', 'Standard'), ('floats', 'Floats'), ('floatsabs', 'Floats (absolute)'), ('floatsrel', 'Floats (relative)'), ('rstripped', 'Non-trailing spaces'), ('sorted', 'Sorted'), ('identical', 'Byte identical'), ('linecount', 'Line-by-line')], max_length=10, verbose_name='checker'),
+ ),
+ migrations.AlterField(
+ model_name='problemdata',
+ name='nobigmath',
+ field=models.BooleanField(blank=True, null=True, verbose_name='disable bigInteger / bigDecimal'),
+ ),
+ migrations.AlterField(
+ model_name='problemtestcase',
+ name='checker',
+ field=models.CharField(blank=True, choices=[('standard', 'Standard'), ('floats', 'Floats'), ('floatsabs', 'Floats (absolute)'), ('floatsrel', 'Floats (relative)'), ('rstripped', 'Non-trailing spaces'), ('sorted', 'Sorted'), ('identical', 'Byte identical'), ('linecount', 'Line-by-line')], max_length=10, verbose_name='checker'),
+ ),
+ migrations.AlterField(
+ model_name='problemtestcase',
+ name='checker_args',
+ field=models.TextField(blank=True, help_text='checker arguments as a JSON object', verbose_name='checker arguments'),
+ ),
+ migrations.AlterField(
+ model_name='problemtestcase',
+ name='is_pretest',
+ field=models.BooleanField(default=False, verbose_name='case is pretest?'),
+ ),
+ migrations.AlterField(
+ model_name='problemtranslation',
+ name='language',
+ field=models.CharField(choices=[('ca', 'Catalan'), ('de', 'German'), ('el', 'Greek'), ('en', 'English'), ('es', 'Spanish'), ('fr', 'French'), ('hr', 'Croatian'), ('hu', 'Hungarian'), ('ja', 'Japanese'), ('kk', 'Kazakh'), ('ko', 'Korean'), ('pt', 'Brazilian Portuguese'), ('ro', 'Romanian'), ('ru', 'Russian'), ('sr-latn', 'Serbian (Latin)'), ('tr', 'Turkish'), ('vi', 'Vietnamese'), ('zh-hans', 'Simplified Chinese'), ('zh-hant', 'Traditional Chinese')], max_length=7, verbose_name='language'),
+ ),
+ migrations.AlterField(
+ model_name='profile',
+ name='ace_theme',
+ field=models.CharField(choices=[('auto', 'Follow site theme'), ('ambiance', 'Ambiance'), ('chaos', 'Chaos'), ('chrome', 'Chrome'), ('clouds', 'Clouds'), ('clouds_midnight', 'Clouds Midnight'), ('cobalt', 'Cobalt'), ('crimson_editor', 'Crimson Editor'), ('dawn', 'Dawn'), ('dreamweaver', 'Dreamweaver'), ('eclipse', 'Eclipse'), ('github', 'Github'), ('idle_fingers', 'Idle Fingers'), ('katzenmilch', 'Katzenmilch'), ('kr_theme', 'KR Theme'), ('kuroir', 'Kuroir'), ('merbivore', 'Merbivore'), ('merbivore_soft', 'Merbivore Soft'), ('mono_industrial', 'Mono Industrial'), ('monokai', 'Monokai'), ('pastel_on_dark', 'Pastel on Dark'), ('solarized_dark', 'Solarized Dark'), ('solarized_light', 'Solarized Light'), ('terminal', 'Terminal'), ('textmate', 'Textmate'), ('tomorrow', 'Tomorrow'), ('tomorrow_night', 'Tomorrow Night'), ('tomorrow_night_blue', 'Tomorrow Night Blue'), ('tomorrow_night_bright', 'Tomorrow Night Bright'), ('tomorrow_night_eighties', 'Tomorrow Night Eighties'), ('twilight', 'Twilight'), ('vibrant_ink', 'Vibrant Ink'), ('xcode', 'XCode')], default='auto', max_length=30, verbose_name='Ace theme'),
+ ),
+ migrations.AlterField(
+ model_name='profile',
+ name='math_engine',
+ field=models.CharField(choices=[('tex', 'Leave as LaTeX'), ('svg', 'SVG only'), ('mml', 'MathML only'), ('jax', 'MathJax with SVG fallback'), ('auto', 'Detect best quality')], default='auto', help_text='The rendering engine used to render math.', max_length=4, verbose_name='math engine'),
+ ),
+ migrations.AlterField(
+ model_name='profile',
+ name='timezone',
+ field=models.CharField(choices=[('Africa', [('Africa/Abidjan', 'Abidjan'), ('Africa/Accra', 'Accra'), ('Africa/Addis_Ababa', 'Addis_Ababa'), ('Africa/Algiers', 'Algiers'), ('Africa/Asmara', 'Asmara'), ('Africa/Asmera', 'Asmera'), ('Africa/Bamako', 'Bamako'), ('Africa/Bangui', 'Bangui'), ('Africa/Banjul', 'Banjul'), ('Africa/Bissau', 'Bissau'), ('Africa/Blantyre', 'Blantyre'), ('Africa/Brazzaville', 'Brazzaville'), ('Africa/Bujumbura', 'Bujumbura'), ('Africa/Cairo', 'Cairo'), ('Africa/Casablanca', 'Casablanca'), ('Africa/Ceuta', 'Ceuta'), ('Africa/Conakry', 'Conakry'), ('Africa/Dakar', 'Dakar'), ('Africa/Dar_es_Salaam', 'Dar_es_Salaam'), ('Africa/Djibouti', 'Djibouti'), ('Africa/Douala', 'Douala'), ('Africa/El_Aaiun', 'El_Aaiun'), ('Africa/Freetown', 'Freetown'), ('Africa/Gaborone', 'Gaborone'), ('Africa/Harare', 'Harare'), ('Africa/Johannesburg', 'Johannesburg'), ('Africa/Juba', 'Juba'), ('Africa/Kampala', 'Kampala'), ('Africa/Khartoum', 'Khartoum'), ('Africa/Kigali', 'Kigali'), ('Africa/Kinshasa', 'Kinshasa'), ('Africa/Lagos', 'Lagos'), ('Africa/Libreville', 'Libreville'), ('Africa/Lome', 'Lome'), ('Africa/Luanda', 'Luanda'), ('Africa/Lubumbashi', 'Lubumbashi'), ('Africa/Lusaka', 'Lusaka'), ('Africa/Malabo', 'Malabo'), ('Africa/Maputo', 'Maputo'), ('Africa/Maseru', 'Maseru'), ('Africa/Mbabane', 'Mbabane'), ('Africa/Mogadishu', 'Mogadishu'), ('Africa/Monrovia', 'Monrovia'), ('Africa/Nairobi', 'Nairobi'), ('Africa/Ndjamena', 'Ndjamena'), ('Africa/Niamey', 'Niamey'), ('Africa/Nouakchott', 'Nouakchott'), ('Africa/Ouagadougou', 'Ouagadougou'), ('Africa/Porto-Novo', 'Porto-Novo'), ('Africa/Sao_Tome', 'Sao_Tome'), ('Africa/Timbuktu', 'Timbuktu'), ('Africa/Tripoli', 'Tripoli'), ('Africa/Tunis', 'Tunis'), ('Africa/Windhoek', 'Windhoek')]), ('America', [('America/Adak', 'Adak'), ('America/Anchorage', 'Anchorage'), ('America/Anguilla', 'Anguilla'), ('America/Antigua', 'Antigua'), ('America/Araguaina', 'Araguaina'), ('America/Argentina/Buenos_Aires', 'Argentina/Buenos_Aires'), ('America/Argentina/Catamarca', 'Argentina/Catamarca'), ('America/Argentina/ComodRivadavia', 'Argentina/ComodRivadavia'), ('America/Argentina/Cordoba', 'Argentina/Cordoba'), ('America/Argentina/Jujuy', 'Argentina/Jujuy'), ('America/Argentina/La_Rioja', 'Argentina/La_Rioja'), ('America/Argentina/Mendoza', 'Argentina/Mendoza'), ('America/Argentina/Rio_Gallegos', 'Argentina/Rio_Gallegos'), ('America/Argentina/Salta', 'Argentina/Salta'), ('America/Argentina/San_Juan', 'Argentina/San_Juan'), ('America/Argentina/San_Luis', 'Argentina/San_Luis'), ('America/Argentina/Tucuman', 'Argentina/Tucuman'), ('America/Argentina/Ushuaia', 'Argentina/Ushuaia'), ('America/Aruba', 'Aruba'), ('America/Asuncion', 'Asuncion'), ('America/Atikokan', 'Atikokan'), ('America/Atka', 'Atka'), ('America/Bahia', 'Bahia'), ('America/Bahia_Banderas', 'Bahia_Banderas'), ('America/Barbados', 'Barbados'), ('America/Belem', 'Belem'), ('America/Belize', 'Belize'), ('America/Blanc-Sablon', 'Blanc-Sablon'), ('America/Boa_Vista', 'Boa_Vista'), ('America/Bogota', 'Bogota'), ('America/Boise', 'Boise'), ('America/Buenos_Aires', 'Buenos_Aires'), ('America/Cambridge_Bay', 'Cambridge_Bay'), ('America/Campo_Grande', 'Campo_Grande'), ('America/Cancun', 'Cancun'), ('America/Caracas', 'Caracas'), ('America/Catamarca', 'Catamarca'), ('America/Cayenne', 'Cayenne'), ('America/Cayman', 'Cayman'), ('America/Chicago', 'Chicago'), ('America/Chihuahua', 'Chihuahua'), ('America/Ciudad_Juarez', 'Ciudad_Juarez'), ('America/Coral_Harbour', 'Coral_Harbour'), ('America/Cordoba', 'Cordoba'), ('America/Costa_Rica', 'Costa_Rica'), ('America/Creston', 'Creston'), ('America/Cuiaba', 'Cuiaba'), ('America/Curacao', 'Curacao'), ('America/Danmarkshavn', 'Danmarkshavn'), ('America/Dawson', 'Dawson'), ('America/Dawson_Creek', 'Dawson_Creek'), ('America/Denver', 'Denver'), ('America/Detroit', 'Detroit'), ('America/Dominica', 'Dominica'), ('America/Edmonton', 'Edmonton'), ('America/Eirunepe', 'Eirunepe'), ('America/El_Salvador', 'El_Salvador'), ('America/Ensenada', 'Ensenada'), ('America/Fort_Nelson', 'Fort_Nelson'), ('America/Fort_Wayne', 'Fort_Wayne'), ('America/Fortaleza', 'Fortaleza'), ('America/Glace_Bay', 'Glace_Bay'), ('America/Godthab', 'Godthab'), ('America/Goose_Bay', 'Goose_Bay'), ('America/Grand_Turk', 'Grand_Turk'), ('America/Grenada', 'Grenada'), ('America/Guadeloupe', 'Guadeloupe'), ('America/Guatemala', 'Guatemala'), ('America/Guayaquil', 'Guayaquil'), ('America/Guyana', 'Guyana'), ('America/Halifax', 'Halifax'), ('America/Havana', 'Havana'), ('America/Hermosillo', 'Hermosillo'), ('America/Indiana/Indianapolis', 'Indiana/Indianapolis'), ('America/Indiana/Knox', 'Indiana/Knox'), ('America/Indiana/Marengo', 'Indiana/Marengo'), ('America/Indiana/Petersburg', 'Indiana/Petersburg'), ('America/Indiana/Tell_City', 'Indiana/Tell_City'), ('America/Indiana/Vevay', 'Indiana/Vevay'), ('America/Indiana/Vincennes', 'Indiana/Vincennes'), ('America/Indiana/Winamac', 'Indiana/Winamac'), ('America/Indianapolis', 'Indianapolis'), ('America/Inuvik', 'Inuvik'), ('America/Iqaluit', 'Iqaluit'), ('America/Jamaica', 'Jamaica'), ('America/Jujuy', 'Jujuy'), ('America/Juneau', 'Juneau'), ('America/Kentucky/Louisville', 'Kentucky/Louisville'), ('America/Kentucky/Monticello', 'Kentucky/Monticello'), ('America/Knox_IN', 'Knox_IN'), ('America/Kralendijk', 'Kralendijk'), ('America/La_Paz', 'La_Paz'), ('America/Lima', 'Lima'), ('America/Los_Angeles', 'Los_Angeles'), ('America/Louisville', 'Louisville'), ('America/Lower_Princes', 'Lower_Princes'), ('America/Maceio', 'Maceio'), ('America/Managua', 'Managua'), ('America/Manaus', 'Manaus'), ('America/Marigot', 'Marigot'), ('America/Martinique', 'Martinique'), ('America/Matamoros', 'Matamoros'), ('America/Mazatlan', 'Mazatlan'), ('America/Mendoza', 'Mendoza'), ('America/Menominee', 'Menominee'), ('America/Merida', 'Merida'), ('America/Metlakatla', 'Metlakatla'), ('America/Mexico_City', 'Mexico_City'), ('America/Miquelon', 'Miquelon'), ('America/Moncton', 'Moncton'), ('America/Monterrey', 'Monterrey'), ('America/Montevideo', 'Montevideo'), ('America/Montreal', 'Montreal'), ('America/Montserrat', 'Montserrat'), ('America/Nassau', 'Nassau'), ('America/New_York', 'New_York'), ('America/Nipigon', 'Nipigon'), ('America/Nome', 'Nome'), ('America/Noronha', 'Noronha'), ('America/North_Dakota/Beulah', 'North_Dakota/Beulah'), ('America/North_Dakota/Center', 'North_Dakota/Center'), ('America/North_Dakota/New_Salem', 'North_Dakota/New_Salem'), ('America/Nuuk', 'Nuuk'), ('America/Ojinaga', 'Ojinaga'), ('America/Panama', 'Panama'), ('America/Pangnirtung', 'Pangnirtung'), ('America/Paramaribo', 'Paramaribo'), ('America/Phoenix', 'Phoenix'), ('America/Port-au-Prince', 'Port-au-Prince'), ('America/Port_of_Spain', 'Port_of_Spain'), ('America/Porto_Acre', 'Porto_Acre'), ('America/Porto_Velho', 'Porto_Velho'), ('America/Puerto_Rico', 'Puerto_Rico'), ('America/Punta_Arenas', 'Punta_Arenas'), ('America/Rainy_River', 'Rainy_River'), ('America/Rankin_Inlet', 'Rankin_Inlet'), ('America/Recife', 'Recife'), ('America/Regina', 'Regina'), ('America/Resolute', 'Resolute'), ('America/Rio_Branco', 'Rio_Branco'), ('America/Rosario', 'Rosario'), ('America/Santa_Isabel', 'Santa_Isabel'), ('America/Santarem', 'Santarem'), ('America/Santiago', 'Santiago'), ('America/Santo_Domingo', 'Santo_Domingo'), ('America/Sao_Paulo', 'Sao_Paulo'), ('America/Scoresbysund', 'Scoresbysund'), ('America/Shiprock', 'Shiprock'), ('America/Sitka', 'Sitka'), ('America/St_Barthelemy', 'St_Barthelemy'), ('America/St_Johns', 'St_Johns'), ('America/St_Kitts', 'St_Kitts'), ('America/St_Lucia', 'St_Lucia'), ('America/St_Thomas', 'St_Thomas'), ('America/St_Vincent', 'St_Vincent'), ('America/Swift_Current', 'Swift_Current'), ('America/Tegucigalpa', 'Tegucigalpa'), ('America/Thule', 'Thule'), ('America/Thunder_Bay', 'Thunder_Bay'), ('America/Tijuana', 'Tijuana'), ('America/Toronto', 'Toronto'), ('America/Tortola', 'Tortola'), ('America/Vancouver', 'Vancouver'), ('America/Virgin', 'Virgin'), ('America/Whitehorse', 'Whitehorse'), ('America/Winnipeg', 'Winnipeg'), ('America/Yakutat', 'Yakutat'), ('America/Yellowknife', 'Yellowknife')]), ('Antarctica', [('Antarctica/Casey', 'Casey'), ('Antarctica/Davis', 'Davis'), ('Antarctica/DumontDUrville', 'DumontDUrville'), ('Antarctica/Macquarie', 'Macquarie'), ('Antarctica/Mawson', 'Mawson'), ('Antarctica/McMurdo', 'McMurdo'), ('Antarctica/Palmer', 'Palmer'), ('Antarctica/Rothera', 'Rothera'), ('Antarctica/South_Pole', 'South_Pole'), ('Antarctica/Syowa', 'Syowa'), ('Antarctica/Troll', 'Troll'), ('Antarctica/Vostok', 'Vostok')]), ('Arctic', [('Arctic/Longyearbyen', 'Longyearbyen')]), ('Asia', [('Asia/Aden', 'Aden'), ('Asia/Almaty', 'Almaty'), ('Asia/Amman', 'Amman'), ('Asia/Anadyr', 'Anadyr'), ('Asia/Aqtau', 'Aqtau'), ('Asia/Aqtobe', 'Aqtobe'), ('Asia/Ashgabat', 'Ashgabat'), ('Asia/Ashkhabad', 'Ashkhabad'), ('Asia/Atyrau', 'Atyrau'), ('Asia/Baghdad', 'Baghdad'), ('Asia/Bahrain', 'Bahrain'), ('Asia/Baku', 'Baku'), ('Asia/Bangkok', 'Bangkok'), ('Asia/Barnaul', 'Barnaul'), ('Asia/Beirut', 'Beirut'), ('Asia/Bishkek', 'Bishkek'), ('Asia/Brunei', 'Brunei'), ('Asia/Calcutta', 'Calcutta'), ('Asia/Chita', 'Chita'), ('Asia/Choibalsan', 'Choibalsan'), ('Asia/Chongqing', 'Chongqing'), ('Asia/Chungking', 'Chungking'), ('Asia/Colombo', 'Colombo'), ('Asia/Dacca', 'Dacca'), ('Asia/Damascus', 'Damascus'), ('Asia/Dhaka', 'Dhaka'), ('Asia/Dili', 'Dili'), ('Asia/Dubai', 'Dubai'), ('Asia/Dushanbe', 'Dushanbe'), ('Asia/Famagusta', 'Famagusta'), ('Asia/Gaza', 'Gaza'), ('Asia/Harbin', 'Harbin'), ('Asia/Hebron', 'Hebron'), ('Asia/Ho_Chi_Minh', 'Ho_Chi_Minh'), ('Asia/Hong_Kong', 'Hong_Kong'), ('Asia/Hovd', 'Hovd'), ('Asia/Irkutsk', 'Irkutsk'), ('Asia/Istanbul', 'Istanbul'), ('Asia/Jakarta', 'Jakarta'), ('Asia/Jayapura', 'Jayapura'), ('Asia/Jerusalem', 'Jerusalem'), ('Asia/Kabul', 'Kabul'), ('Asia/Kamchatka', 'Kamchatka'), ('Asia/Karachi', 'Karachi'), ('Asia/Kashgar', 'Kashgar'), ('Asia/Kathmandu', 'Kathmandu'), ('Asia/Katmandu', 'Katmandu'), ('Asia/Khandyga', 'Khandyga'), ('Asia/Kolkata', 'Kolkata'), ('Asia/Krasnoyarsk', 'Krasnoyarsk'), ('Asia/Kuala_Lumpur', 'Kuala_Lumpur'), ('Asia/Kuching', 'Kuching'), ('Asia/Kuwait', 'Kuwait'), ('Asia/Macao', 'Macao'), ('Asia/Macau', 'Macau'), ('Asia/Magadan', 'Magadan'), ('Asia/Makassar', 'Makassar'), ('Asia/Manila', 'Manila'), ('Asia/Muscat', 'Muscat'), ('Asia/Nicosia', 'Nicosia'), ('Asia/Novokuznetsk', 'Novokuznetsk'), ('Asia/Novosibirsk', 'Novosibirsk'), ('Asia/Omsk', 'Omsk'), ('Asia/Oral', 'Oral'), ('Asia/Phnom_Penh', 'Phnom_Penh'), ('Asia/Pontianak', 'Pontianak'), ('Asia/Pyongyang', 'Pyongyang'), ('Asia/Qatar', 'Qatar'), ('Asia/Qostanay', 'Qostanay'), ('Asia/Qyzylorda', 'Qyzylorda'), ('Asia/Rangoon', 'Rangoon'), ('Asia/Riyadh', 'Riyadh'), ('Asia/Saigon', 'Saigon'), ('Asia/Sakhalin', 'Sakhalin'), ('Asia/Samarkand', 'Samarkand'), ('Asia/Seoul', 'Seoul'), ('Asia/Shanghai', 'Shanghai'), ('Asia/Singapore', 'Singapore'), ('Asia/Srednekolymsk', 'Srednekolymsk'), ('Asia/Taipei', 'Taipei'), ('Asia/Tashkent', 'Tashkent'), ('Asia/Tbilisi', 'Tbilisi'), ('Asia/Tehran', 'Tehran'), ('Asia/Tel_Aviv', 'Tel_Aviv'), ('Asia/Thimbu', 'Thimbu'), ('Asia/Thimphu', 'Thimphu'), ('Asia/Tokyo', 'Tokyo'), ('Asia/Tomsk', 'Tomsk'), ('Asia/Ujung_Pandang', 'Ujung_Pandang'), ('Asia/Ulaanbaatar', 'Ulaanbaatar'), ('Asia/Ulan_Bator', 'Ulan_Bator'), ('Asia/Urumqi', 'Urumqi'), ('Asia/Ust-Nera', 'Ust-Nera'), ('Asia/Vientiane', 'Vientiane'), ('Asia/Vladivostok', 'Vladivostok'), ('Asia/Yakutsk', 'Yakutsk'), ('Asia/Yangon', 'Yangon'), ('Asia/Yekaterinburg', 'Yekaterinburg'), ('Asia/Yerevan', 'Yerevan')]), ('Atlantic', [('Atlantic/Azores', 'Azores'), ('Atlantic/Bermuda', 'Bermuda'), ('Atlantic/Canary', 'Canary'), ('Atlantic/Cape_Verde', 'Cape_Verde'), ('Atlantic/Faeroe', 'Faeroe'), ('Atlantic/Faroe', 'Faroe'), ('Atlantic/Jan_Mayen', 'Jan_Mayen'), ('Atlantic/Madeira', 'Madeira'), ('Atlantic/Reykjavik', 'Reykjavik'), ('Atlantic/South_Georgia', 'South_Georgia'), ('Atlantic/St_Helena', 'St_Helena'), ('Atlantic/Stanley', 'Stanley')]), ('Australia', [('Australia/ACT', 'ACT'), ('Australia/Adelaide', 'Adelaide'), ('Australia/Brisbane', 'Brisbane'), ('Australia/Broken_Hill', 'Broken_Hill'), ('Australia/Canberra', 'Canberra'), ('Australia/Currie', 'Currie'), ('Australia/Darwin', 'Darwin'), ('Australia/Eucla', 'Eucla'), ('Australia/Hobart', 'Hobart'), ('Australia/LHI', 'LHI'), ('Australia/Lindeman', 'Lindeman'), ('Australia/Lord_Howe', 'Lord_Howe'), ('Australia/Melbourne', 'Melbourne'), ('Australia/NSW', 'NSW'), ('Australia/North', 'North'), ('Australia/Perth', 'Perth'), ('Australia/Queensland', 'Queensland'), ('Australia/South', 'South'), ('Australia/Sydney', 'Sydney'), ('Australia/Tasmania', 'Tasmania'), ('Australia/Victoria', 'Victoria'), ('Australia/West', 'West'), ('Australia/Yancowinna', 'Yancowinna')]), ('Brazil', [('Brazil/Acre', 'Acre'), ('Brazil/DeNoronha', 'DeNoronha'), ('Brazil/East', 'East'), ('Brazil/West', 'West')]), ('Canada', [('Canada/Atlantic', 'Atlantic'), ('Canada/Central', 'Central'), ('Canada/Eastern', 'Eastern'), ('Canada/Mountain', 'Mountain'), ('Canada/Newfoundland', 'Newfoundland'), ('Canada/Pacific', 'Pacific'), ('Canada/Saskatchewan', 'Saskatchewan'), ('Canada/Yukon', 'Yukon')]), ('Chile', [('Chile/Continental', 'Continental'), ('Chile/EasterIsland', 'EasterIsland')]), ('Etc', [('Etc/Greenwich', 'Greenwich'), ('Etc/UCT', 'UCT'), ('Etc/UTC', 'UTC'), ('Etc/Universal', 'Universal'), ('Etc/Zulu', 'Zulu')]), ('Europe', [('Europe/Amsterdam', 'Amsterdam'), ('Europe/Andorra', 'Andorra'), ('Europe/Astrakhan', 'Astrakhan'), ('Europe/Athens', 'Athens'), ('Europe/Belfast', 'Belfast'), ('Europe/Belgrade', 'Belgrade'), ('Europe/Berlin', 'Berlin'), ('Europe/Bratislava', 'Bratislava'), ('Europe/Brussels', 'Brussels'), ('Europe/Bucharest', 'Bucharest'), ('Europe/Budapest', 'Budapest'), ('Europe/Busingen', 'Busingen'), ('Europe/Chisinau', 'Chisinau'), ('Europe/Copenhagen', 'Copenhagen'), ('Europe/Dublin', 'Dublin'), ('Europe/Gibraltar', 'Gibraltar'), ('Europe/Guernsey', 'Guernsey'), ('Europe/Helsinki', 'Helsinki'), ('Europe/Isle_of_Man', 'Isle_of_Man'), ('Europe/Istanbul', 'Istanbul'), ('Europe/Jersey', 'Jersey'), ('Europe/Kaliningrad', 'Kaliningrad'), ('Europe/Kiev', 'Kiev'), ('Europe/Kirov', 'Kirov'), ('Europe/Kyiv', 'Kyiv'), ('Europe/Lisbon', 'Lisbon'), ('Europe/Ljubljana', 'Ljubljana'), ('Europe/London', 'London'), ('Europe/Luxembourg', 'Luxembourg'), ('Europe/Madrid', 'Madrid'), ('Europe/Malta', 'Malta'), ('Europe/Mariehamn', 'Mariehamn'), ('Europe/Minsk', 'Minsk'), ('Europe/Monaco', 'Monaco'), ('Europe/Moscow', 'Moscow'), ('Europe/Nicosia', 'Nicosia'), ('Europe/Oslo', 'Oslo'), ('Europe/Paris', 'Paris'), ('Europe/Podgorica', 'Podgorica'), ('Europe/Prague', 'Prague'), ('Europe/Riga', 'Riga'), ('Europe/Rome', 'Rome'), ('Europe/Samara', 'Samara'), ('Europe/San_Marino', 'San_Marino'), ('Europe/Sarajevo', 'Sarajevo'), ('Europe/Saratov', 'Saratov'), ('Europe/Simferopol', 'Simferopol'), ('Europe/Skopje', 'Skopje'), ('Europe/Sofia', 'Sofia'), ('Europe/Stockholm', 'Stockholm'), ('Europe/Tallinn', 'Tallinn'), ('Europe/Tirane', 'Tirane'), ('Europe/Tiraspol', 'Tiraspol'), ('Europe/Ulyanovsk', 'Ulyanovsk'), ('Europe/Uzhgorod', 'Uzhgorod'), ('Europe/Vaduz', 'Vaduz'), ('Europe/Vatican', 'Vatican'), ('Europe/Vienna', 'Vienna'), ('Europe/Vilnius', 'Vilnius'), ('Europe/Volgograd', 'Volgograd'), ('Europe/Warsaw', 'Warsaw'), ('Europe/Zagreb', 'Zagreb'), ('Europe/Zaporozhye', 'Zaporozhye'), ('Europe/Zurich', 'Zurich')]), ('Indian', [('Indian/Antananarivo', 'Antananarivo'), ('Indian/Chagos', 'Chagos'), ('Indian/Christmas', 'Christmas'), ('Indian/Cocos', 'Cocos'), ('Indian/Comoro', 'Comoro'), ('Indian/Kerguelen', 'Kerguelen'), ('Indian/Mahe', 'Mahe'), ('Indian/Maldives', 'Maldives'), ('Indian/Mauritius', 'Mauritius'), ('Indian/Mayotte', 'Mayotte'), ('Indian/Reunion', 'Reunion')]), ('Mexico', [('Mexico/BajaNorte', 'BajaNorte'), ('Mexico/BajaSur', 'BajaSur'), ('Mexico/General', 'General')]), ('Other', [('CET', 'CET'), ('CST6CDT', 'CST6CDT'), ('Cuba', 'Cuba'), ('EET', 'EET'), ('EST', 'EST'), ('EST5EDT', 'EST5EDT'), ('Egypt', 'Egypt'), ('Eire', 'Eire'), ('GB', 'GB'), ('GB-Eire', 'GB-Eire'), ('Greenwich', 'Greenwich'), ('HST', 'HST'), ('Hongkong', 'Hongkong'), ('Iceland', 'Iceland'), ('Iran', 'Iran'), ('Israel', 'Israel'), ('Jamaica', 'Jamaica'), ('Japan', 'Japan'), ('Kwajalein', 'Kwajalein'), ('Libya', 'Libya'), ('MET', 'MET'), ('MST', 'MST'), ('MST7MDT', 'MST7MDT'), ('NZ', 'NZ'), ('NZ-CHAT', 'NZ-CHAT'), ('Navajo', 'Navajo'), ('PRC', 'PRC'), ('PST8PDT', 'PST8PDT'), ('Poland', 'Poland'), ('Portugal', 'Portugal'), ('ROC', 'ROC'), ('ROK', 'ROK'), ('Singapore', 'Singapore'), ('Turkey', 'Turkey'), ('UCT', 'UCT'), ('UTC', 'UTC'), ('Universal', 'Universal'), ('W-SU', 'W-SU'), ('WET', 'WET'), ('Zulu', 'Zulu')]), ('Pacific', [('Pacific/Apia', 'Apia'), ('Pacific/Auckland', 'Auckland'), ('Pacific/Bougainville', 'Bougainville'), ('Pacific/Chatham', 'Chatham'), ('Pacific/Chuuk', 'Chuuk'), ('Pacific/Easter', 'Easter'), ('Pacific/Efate', 'Efate'), ('Pacific/Enderbury', 'Enderbury'), ('Pacific/Fakaofo', 'Fakaofo'), ('Pacific/Fiji', 'Fiji'), ('Pacific/Funafuti', 'Funafuti'), ('Pacific/Galapagos', 'Galapagos'), ('Pacific/Gambier', 'Gambier'), ('Pacific/Guadalcanal', 'Guadalcanal'), ('Pacific/Guam', 'Guam'), ('Pacific/Honolulu', 'Honolulu'), ('Pacific/Johnston', 'Johnston'), ('Pacific/Kanton', 'Kanton'), ('Pacific/Kiritimati', 'Kiritimati'), ('Pacific/Kosrae', 'Kosrae'), ('Pacific/Kwajalein', 'Kwajalein'), ('Pacific/Majuro', 'Majuro'), ('Pacific/Marquesas', 'Marquesas'), ('Pacific/Midway', 'Midway'), ('Pacific/Nauru', 'Nauru'), ('Pacific/Niue', 'Niue'), ('Pacific/Norfolk', 'Norfolk'), ('Pacific/Noumea', 'Noumea'), ('Pacific/Pago_Pago', 'Pago_Pago'), ('Pacific/Palau', 'Palau'), ('Pacific/Pitcairn', 'Pitcairn'), ('Pacific/Pohnpei', 'Pohnpei'), ('Pacific/Ponape', 'Ponape'), ('Pacific/Port_Moresby', 'Port_Moresby'), ('Pacific/Rarotonga', 'Rarotonga'), ('Pacific/Saipan', 'Saipan'), ('Pacific/Samoa', 'Samoa'), ('Pacific/Tahiti', 'Tahiti'), ('Pacific/Tarawa', 'Tarawa'), ('Pacific/Tongatapu', 'Tongatapu'), ('Pacific/Truk', 'Truk'), ('Pacific/Wake', 'Wake'), ('Pacific/Wallis', 'Wallis'), ('Pacific/Yap', 'Yap')]), ('US', [('US/Alaska', 'Alaska'), ('US/Aleutian', 'Aleutian'), ('US/Arizona', 'Arizona'), ('US/Central', 'Central'), ('US/East-Indiana', 'East-Indiana'), ('US/Eastern', 'Eastern'), ('US/Hawaii', 'Hawaii'), ('US/Indiana-Starke', 'Indiana-Starke'), ('US/Michigan', 'Michigan'), ('US/Mountain', 'Mountain'), ('US/Pacific', 'Pacific'), ('US/Samoa', 'Samoa')])], default='America/Toronto', max_length=50, verbose_name='time zone'),
+ ),
+ ]
diff --git a/judge/models/problem.py b/judge/models/problem.py
index aa436724fb..53681eca91 100644
--- a/judge/models/problem.py
+++ b/judge/models/problem.py
@@ -4,7 +4,7 @@
from django.conf import settings
from django.contrib.contenttypes.fields import GenericRelation
from django.core.cache import cache
-from django.core.exceptions import ValidationError
+from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator, RegexValidator
from django.db import models
from django.db.models import CASCADE, Exists, F, FilteredRelation, OuterRef, Q, SET_NULL
@@ -120,6 +120,9 @@ class Problem(models.Model):
name = models.CharField(max_length=100, verbose_name=_('problem name'), db_index=True,
help_text=_('The full name of the problem, as shown in the problem list.'),
validators=[disallowed_characters_validator])
+ include_test_cases = models.BooleanField(verbose_name=_('include test cases'),
+ help_text=_('If true, the inputs and otuputs of every test case will '
+ 'be automatically added after the body.'), default=False)
description = models.TextField(verbose_name=_('problem body'), validators=[disallowed_characters_validator])
authors = models.ManyToManyField(Profile, verbose_name=_('creators'), blank=True, related_name='authored_problems',
help_text=_('These users will be able to edit the problem, '
@@ -454,6 +457,7 @@ def markdown_style(self):
def save(self, *args, **kwargs):
super(Problem, self).save(*args, **kwargs)
+
if self.code != self.__original_code:
try:
problem_data = self.data_files
@@ -462,6 +466,13 @@ def save(self, *args, **kwargs):
else:
problem_data._update_code(self.__original_code, self.code)
+ if self.include_test_cases:
+ try:
+ self.data_files.setup_test_cases_content()
+ self.data_files.save()
+ except ObjectDoesNotExist:
+ pass
+
save.alters_data = True
def is_solved_by(self, user):
diff --git a/judge/models/problem_data.py b/judge/models/problem_data.py
index 5b182e523c..a30d559648 100644
--- a/judge/models/problem_data.py
+++ b/judge/models/problem_data.py
@@ -1,11 +1,14 @@
import errno
import os
+from zipfile import ZipFile
+import yaml
from django.db import models
from django.utils.translation import gettext_lazy as _
from judge.utils.problem_data import ProblemDataStorage
+
__all__ = ['problem_data_storage', 'problem_directory_file', 'ProblemData', 'ProblemTestCase', 'CHECKERS']
problem_data_storage = ProblemDataStorage()
@@ -38,6 +41,8 @@ class ProblemData(models.Model):
upload_to=problem_directory_file)
generator = models.FileField(verbose_name=_('generator file'), storage=problem_data_storage, null=True, blank=True,
upload_to=problem_directory_file)
+ infer_from_zip = models.BooleanField(verbose_name=_('infer test cases from zip'), null=True, blank=True)
+ test_cases_content = models.TextField(verbose_name=_('test cases content'), blank=True)
output_prefix = models.IntegerField(verbose_name=_('output prefix length'), blank=True, null=True)
output_limit = models.IntegerField(verbose_name=_('output limit length'), blank=True, null=True)
feedback = models.TextField(verbose_name=_('init.yml generation feedback'), blank=True)
@@ -53,9 +58,22 @@ def __init__(self, *args, **kwargs):
super(ProblemData, self).__init__(*args, **kwargs)
self.__original_zipfile = self.zipfile
+ if not self.zipfile:
+ # Test cases not loaded through the site, but some data has been found within the problem folder
+ if self.has_yml():
+ self.feedback = 'Warning: problem data found within the file system, but none has been setup '\
+ 'using this site. No actions are needed if the problem is working as intended; '\
+ "otherwise, you can "\
+ 'infer the testcases using the existing zip file (one entry per file within the '\
+ "zip) or "\
+ 'rebuild the test cases using the existing yml file as a template (only works'\
+ 'with simple problems).'
+
def save(self, *args, **kwargs):
+ zipfile = self.zipfile
if self.zipfile != self.__original_zipfile:
- self.__original_zipfile.delete(save=False)
+ self.__original_zipfile.delete(save=False) # This clears both zip fields (original and current)
+ self.zipfile = zipfile # Needed to restore the newly uploaded zip file when replacing an old one
return super(ProblemData, self).save(*args, **kwargs)
def has_yml(self):
@@ -74,6 +92,189 @@ def _update_code(self, original, new):
self.save()
_update_code.alters_data = True
+ def setup_test_cases_content(self):
+ self.test_cases_content = ''
+
+ if self.zipfile:
+ zip = ZipFile(self.zipfile)
+
+ last = 0
+ content = []
+ test_cases = ProblemTestCase.objects.filter(dataset_id=self.problem.pk)
+
+ for i, tc in enumerate([x for x in test_cases if x.is_pretest]):
+ self.append_tescase_to_statement(zip, content, tc, i)
+ last = i
+
+ if last > 0:
+ last += 1
+
+ for i, tc in enumerate([x for x in test_cases if not x.is_pretest]):
+ self.append_tescase_to_statement(zip, content, tc, i + last)
+
+ self.test_cases_content = '\n'.join(content)
+
+ def append_tescase_to_statement(self, zip, content, tc, i):
+ content.append(f'## Sample Input {i+1}')
+ content.append('')
+
+ if tc.is_private:
+ content.append('*Hidden: this is a private test case!* ')
+
+ else:
+ content.append('```')
+ content.append(zip.read(tc.input_file).decode('utf-8'))
+ content.append('```')
+
+ content.append('')
+ content.append(f'## Sample Output {i+1}')
+ content.append('')
+
+ if tc.is_private:
+ content.append('*Hidden: this is a private test case!* ')
+
+ else:
+ content.append('```')
+ content.append(zip.read(tc.output_file).decode('utf-8'))
+ content.append('```')
+
+ content.append('')
+
+ def infer_test_cases_from_zip(self):
+ # Just infers the zip data into ProblemTestCase objects, without changes in the database.
+ # It will try to mantain existing test cases data if the input and output entries are the same.
+ if not self.zipfile:
+ # The zip file will be loaded from the file system if not provided
+ files = problem_data_storage.listdir(self.problem.code)[1]
+ zipfiles = [x for x in files if '.zip' in x]
+
+ if len(zipfiles) > 0:
+ self.zipfile = _problem_directory_file(self.problem.code, zipfiles[0])
+ else:
+ raise FileNotFoundError
+
+ files = sorted(ZipFile(self.zipfile).namelist())
+ input = [x for x in files if '.in' in x or ('input' in x and '.' in x)]
+ output = [x for x in files if '.out' in x or ('output' in x and '.' in x)]
+
+ cases = []
+ for i in range(len(input)):
+ list = ProblemTestCase.objects.filter(dataset_id=self.problem.pk, input_file=input[i],
+ output_file=output[i])
+ if len(list) >= 1:
+ # Multiple test-cases for the same data is allowed, but strange. Using object.get() produces an
+ # exception.
+ ptc = list[0]
+ else:
+ ptc = ProblemTestCase()
+ ptc.dataset = self.problem
+ ptc.is_pretest = False
+ ptc.is_private = False
+ ptc.order = i
+ ptc.input_file = input[i]
+ ptc.output_file = output[i]
+ ptc.points = 0
+
+ cases.append(ptc)
+
+ return cases
+
+ def reload_test_cases_from_yml(self):
+ cases = []
+ if self.has_yml():
+ yml = problem_data_storage.open('%s/init.yml' % self.problem.code)
+ doc = yaml.safe_load(yml)
+
+ # Load same YML data as in site/judge/utils/problem_data.py -> ProblemDataCompiler()
+ if doc.get('archive'):
+ self.zipfile = _problem_directory_file(self.problem.code, doc['archive'])
+
+ if doc.get('generator'):
+ self.generator = _problem_directory_file(self.problem.code, doc['generator'])
+
+ if doc.get('pretest_test_cases'):
+ self.pretest_test_cases = doc['pretest_test_cases']
+
+ if doc.get('output_limit_length'):
+ self.output_limit = doc['output_limit_length']
+
+ if doc.get('output_prefix_length'):
+ self.output_prefix = doc['output_prefix_length']
+
+ if doc.get('unicode'):
+ self.unicode = doc['unicode']
+
+ if doc.get('nobigmath'):
+ self.nobigmath = doc['nobigmath']
+
+ if doc.get('checker'):
+ self.checker = doc['checker']
+
+ if doc.get('hints'):
+ for h in doc['hints']:
+ if h == 'unicode':
+ self.unicode = True
+ if h == 'nobigmath':
+ self.nobigmath = True
+
+ if doc.get('pretest_test_cases'):
+ cases += self._load_test_case_from_doc(doc, 'pretest_test_cases', True)
+
+ if doc.get('test_cases'):
+ cases += self._load_test_case_from_doc(doc, 'test_cases', False)
+
+ return cases
+
+ def _load_test_case_from_doc(self, doc, field, is_pretest):
+ cases = []
+ for i, test in enumerate(doc[field]):
+ ptc = ProblemTestCase()
+ ptc.dataset = self.problem
+ ptc.is_pretest = is_pretest
+ ptc.order = i
+
+ if test.get('type'):
+ ptc.type = test['type']
+
+ if test.get('in'):
+ ptc.input_file = test['in']
+
+ if test.get('out'):
+ ptc.output_file = test['out']
+
+ if test.get('points'):
+ ptc.points = test['points']
+ else:
+ ptc.points = 0
+
+ if test.get('is_private'):
+ ptc.is_private = test['is_private']
+
+ if test.get('generator_args'):
+ args = []
+ for arg in test['generator_args']:
+ args.append(arg)
+
+ ptc.generator_args = '\n'.join(args)
+
+ if test.get('output_prefix_length'):
+ ptc.output_prefix = doc['output_prefix_length']
+
+ if test.get('output_limit_length'):
+ ptc.output_limit = doc['output_limit_length']
+
+ if test.get('checker'):
+ chk = test['checker']
+ if isinstance(chk, str):
+ ptc.checker = chk
+ else:
+ ptc.checker = chk['name']
+ ptc.checker_args = chk['args']
+
+ cases.append(ptc)
+
+ return cases
+
class ProblemTestCase(models.Model):
dataset = models.ForeignKey('Problem', verbose_name=_('problem data set'), related_name='cases',
@@ -88,7 +289,8 @@ class ProblemTestCase(models.Model):
output_file = models.CharField(max_length=100, verbose_name=_('output file name'), blank=True)
generator_args = models.TextField(verbose_name=_('generator arguments'), blank=True)
points = models.IntegerField(verbose_name=_('point value'), blank=True, null=True)
- is_pretest = models.BooleanField(verbose_name=_('case is pretest?'))
+ is_pretest = models.BooleanField(verbose_name=_('case is pretest?'), default=False)
+ is_private = models.BooleanField(verbose_name=_('case is private?'), default=False)
output_prefix = models.IntegerField(verbose_name=_('output prefix length'), blank=True, null=True)
output_limit = models.IntegerField(verbose_name=_('output limit length'), blank=True, null=True)
checker = models.CharField(max_length=10, verbose_name=_('checker'), choices=CHECKERS, blank=True)
diff --git a/judge/utils/problem_data.py b/judge/utils/problem_data.py
index b2fe1e2600..a0660afd9d 100644
--- a/judge/utils/problem_data.py
+++ b/judge/utils/problem_data.py
@@ -78,10 +78,12 @@ def make_checker(case):
if batch:
case.points = None
case.is_pretest = batch['is_pretest']
+ case.is_private = batch['is_private']
else:
if case.points is None:
raise ProblemDataError(_('Points must be defined for non-batch case #%d.') % i)
data['is_pretest'] = case.is_pretest
+ data['is_private'] = case.is_private
if not self.generator:
if case.input_file not in self.files:
@@ -107,7 +109,7 @@ def make_checker(case):
data['checker'] = make_checker(case)
else:
case.checker_args = ''
- case.save(update_fields=('checker_args', 'is_pretest'))
+ case.save(update_fields=('checker_args', 'is_pretest', 'is_private'))
(batch['batched'] if batch else cases).append(data)
elif case.type == 'S':
batch_count += 1
@@ -134,6 +136,7 @@ def make_checker(case):
'points': case.points,
'batched': [],
'is_pretest': case.is_pretest,
+ 'is_private': case.is_private,
'dependencies': dependencies,
}
if case.generator_args:
@@ -153,6 +156,7 @@ def make_checker(case):
if not batch:
raise ProblemDataError(_('Attempt to end batch outside of one in case #%d.') % i)
case.is_pretest = batch['is_pretest']
+ case.is_private = batch['is_private']
case.input_file = ''
case.output_file = ''
case.generator_args = ''
diff --git a/judge/views/problem.py b/judge/views/problem.py
index 20be48b6da..36faaed4bf 100644
--- a/judge/views/problem.py
+++ b/judge/views/problem.py
@@ -204,6 +204,14 @@ def get_context_data(self, **kwargs):
context['og_image'] = self.object.og_image or metadata[1]
context['enable_comments'] = settings.DMOJ_ENABLE_COMMENTS
+ if self.object.include_test_cases:
+ try:
+ context['test_cases'] = self.object.data_files.test_cases_content
+ except ObjectDoesNotExist:
+ context['test_cases'] = ''
+ else:
+ context['test_cases'] = ''
+
context['vote_perm'] = self.object.vote_permission_for_user(user)
if context['vote_perm'].can_vote():
try:
diff --git a/judge/views/problem_data.py b/judge/views/problem_data.py
index e29048b01c..4aebc7a195 100644
--- a/judge/views/problem_data.py
+++ b/judge/views/problem_data.py
@@ -63,7 +63,7 @@ def clean_generator(self):
class Meta:
model = ProblemData
- fields = ['zipfile', 'generator', 'unicode', 'nobigmath', 'output_limit', 'output_prefix',
+ fields = ['zipfile', 'generator', 'infer_from_zip', 'unicode', 'nobigmath', 'output_limit', 'output_prefix',
'checker', 'checker_args']
widgets = {
'checker_args': HiddenInput,
@@ -75,7 +75,7 @@ class ProblemCaseForm(ModelForm):
class Meta:
model = ProblemTestCase
- fields = ('order', 'type', 'input_file', 'output_file', 'points', 'is_pretest', 'output_limit',
+ fields = ('order', 'type', 'input_file', 'output_file', 'points', 'is_pretest', 'is_private', 'output_limit',
'output_prefix', 'checker', 'checker_args', 'generator_args', 'batch_dependencies')
widgets = {
'generator_args': HiddenInput,
@@ -210,18 +210,69 @@ def post(self, request, *args, **kwargs):
data_form.zip_valid = False
cases_formset = self.get_case_formset(valid_files, post=True)
+
+ # This options only appears when file are found in the file-system but no test-case exists in the BBDD.
+ infer_from_zip = request.POST.get('perform_infer_test_cases', 0) != 0
+ rebuild_from_yml = request.POST.get('perform_rebuild_test_cases', 0) != 0
+
if data_form.is_valid() and cases_formset.is_valid():
data = data_form.save()
- for case in cases_formset.save(commit=False):
- case.dataset_id = problem.id
- case.save()
- for case in cases_formset.deleted_objects:
- case.delete()
+
+ if infer_from_zip:
+ self.infer_test_cases_from_zip(data)
+
+ elif rebuild_from_yml:
+ for case in data.reload_test_cases_from_yml():
+ case.save()
+
+ if infer_from_zip or rebuild_from_yml:
+ # The data.zipfile property has been loaded from the file system
+ valid_files = sorted(ZipFile(data.zipfile).namelist())
+ cases_formset = self.get_case_formset(valid_files, post=True)
+ data.save()
+
+ else:
+ for case in cases_formset.save(commit=False):
+ case.dataset_id = problem.id
+ case.save()
+
+ for case in cases_formset.deleted_objects:
+ case.delete()
+
+ if data.infer_from_zip:
+ if not data.zipfile:
+ # Removing all entries to keep consistency (infering from no zip)
+ for case in ProblemTestCase.objects.filter(dataset_id=self.object.pk):
+ case.delete()
+ else:
+ self.infer_test_cases_from_zip(data)
+
+ # Enabling the 'display test cases' option for the problem should display the test cases after
+ # the problem statement. Otherwise, the problem data must be saved additionally to display the
+ # data for the first time. So, to avoid this additional step, the problem is the one which
+ # calls to setup the test-cases within problem data. Also, the update is needed to refresh the
+ # cache so must be called from here anyways.
+ data.problem.save()
+
ProblemDataCompiler.generate(problem, data, problem.cases.order_by('order'), valid_files)
return HttpResponseRedirect(request.get_full_path())
+
return self.render_to_response(self.get_context_data(data_form=data_form, cases_formset=cases_formset,
valid_files=valid_files))
+ def infer_test_cases_from_zip(self, data):
+ old = ProblemTestCase.objects.filter(dataset_id=self.object.pk)
+ infer = data.infer_test_cases_from_zip()
+
+ # When inferinf test-cases, new ones can be created but also existing
+ # ones can be reused. Only the old-ones should be removed.
+ for case in old:
+ if case not in infer:
+ case.delete()
+
+ for case in infer:
+ case.save()
+
put = post
diff --git a/templates/problem/data.html b/templates/problem/data.html
index bb5f423175..3f2179fb4c 100644
--- a/templates/problem/data.html
+++ b/templates/problem/data.html
@@ -5,7 +5,19 @@