From 8fec9557cc7adae1c35ed42a17135ad69d04b4a1 Mon Sep 17 00:00:00 2001 From: FherStk Date: Wed, 27 Dec 2023 11:14:34 +0000 Subject: [PATCH] Infering test cases from a loaded zip file or an existing yml file, also able to hide test-cases from the problem statement respecting the judge order (pretest firsts). fixes --- judge/admin/problem.py | 2 +- .../0147_infer_test_cases_from_zip.py | 85 ++++++++ judge/models/problem.py | 13 +- judge/models/problem_data.py | 206 +++++++++++++++++- judge/utils/problem_data.py | 6 +- judge/views/problem.py | 8 + judge/views/problem_data.py | 65 +++++- templates/problem/data.html | 25 ++- templates/problem/problem.html | 1 + 9 files changed, 396 insertions(+), 15 deletions(-) create mode 100644 judge/migrations/0147_infer_test_cases_from_zip.py 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 @@