diff --git a/apps/home/routes.py b/apps/home/routes.py index ac6bf0d..55a3843 100644 --- a/apps/home/routes.py +++ b/apps/home/routes.py @@ -23,7 +23,7 @@ from jinja2 import TemplateNotFound from apps.audit_mixin import get_remote_addr, check_user_category from apps.authentication.forms import GroupForm -from apps.utils import update_running_labs_stats, parse_lab_expiration, datetime_from_ts, epoch_from_datetime, update_category_stats, update_stats_lab_instances_answers, utcnow, compute_lab_score, secure_filename +from apps.utils import update_running_labs_stats, parse_lab_expiration, parse_group_expiration, datetime_from_ts, epoch_from_datetime, update_category_stats, update_stats_lab_instances_answers, utcnow, compute_lab_score, secure_filename from sqlalchemy import desc @@ -1018,7 +1018,22 @@ def edit_group(group_id): has_changes = False for field in ["groupname", "description", "organization", "expiration", "accesstoken"]: - new_value = request.form[field] if request.form[field] else None + new_value = request.form[field] if request.form[field] else None + if field == "expiration": + try: + new_value = parse_group_expiration(new_value) + except ValueError: + current_app.logger.error( + f"Failed to update group due to invalid expiration: {new_value!r}" + ) + return render_template( + "pages/groups_edit.html", + msg_fail=_("Invalid expiration date, please use the format YYYY-MM-DD."), + group=group, + action_name=action_name, + users=users_info, + return_path="home_blueprint.view_groups" + ) if getattr(group, field) != new_value: setattr(group, field, new_value) has_changes = True diff --git a/apps/templates/pages/groups_edit.html b/apps/templates/pages/groups_edit.html index 11f0e66..b41fb38 100644 --- a/apps/templates/pages/groups_edit.html +++ b/apps/templates/pages/groups_edit.html @@ -103,7 +103,7 @@
{{ _('The date after which this group and its resources will no longer be available. Can be used, for instance, to setup a due date for running a lab (for an exam, contest, CTF, etc). Default: never expires.') }}
re library, pretty much like this: "
+"provided by the users by using regular expression matching. Please choose"
+" the Lab below and, for each question, provide the expected regex which "
+"the answer should match to be considered correct (the calculation is done"
+" in Python using re library, pretty much like this: "
"re.match(fr\"^{expected_answer}$\", answer)). Only the "
"registered questions in the answer sheet will be used actually used for "
"validating the score (for example: if your Lab has 5 questions and the "
-"answer sheet only contains 3 expected answers, then people who provide the 3"
-" correct ones will be considered 100%% correct."
-msgstr ""
-"O gabarito do lab pode ser usado para validar automaticamente as respostas "
-"fornecidas pelos usuários por meio de correspondência com expressões "
-"regulares. Escolha o Lab abaixo e, para cada pergunta, forneça a regex "
-"esperada que a resposta deve corresponder para ser considerada correta (o "
-"cálculo é feito em Python usando a biblioteca re, mais ou menos"
-" assim: re.match(fr\"^{expected_answer}$\", answer)). Apenas as"
-" perguntas registradas no gabarito serão de fato usadas para validar a "
-"pontuação (por exemplo: se o seu Lab tem 5 perguntas e o gabarito contém "
-"apenas 3 respostas esperadas, então quem fornecer as 3 corretas será "
-"considerado 100%% correto."
+"answer sheet only contains 3 expected answers, then people who provide "
+"the 3 correct ones will be considered 100%% correct."
+msgstr ""
+"O gabarito do lab pode ser usado para validar automaticamente as "
+"respostas fornecidas pelos usuários por meio de correspondência com "
+"expressões regulares. Escolha o Lab abaixo e, para cada pergunta, forneça"
+" a regex esperada que a resposta deve corresponder para ser considerada "
+"correta (o cálculo é feito em Python usando a biblioteca re,"
+" mais ou menos assim: re.match(fr\"^{expected_answer}$\", "
+"answer)). Apenas as perguntas registradas no gabarito serão de "
+"fato usadas para validar a pontuação (por exemplo: se o seu Lab tem 5 "
+"perguntas e o gabarito contém apenas 3 respostas esperadas, então quem "
+"fornecer as 3 corretas será considerado 100%% correto."
#: apps/templates/pages/lab_answers_sheet.html:71
msgid "Choose the Lab:"
@@ -2588,11 +2637,12 @@ msgstr "Confirmar Finalização do Lab"
#: apps/templates/pages/lab_instance_view.html:213
msgid ""
-"Are you sure you want to Finish this Lab? (Make sure to save any data you "
-"want! After finishing the lab, resources will be deleted.)"
+"Are you sure you want to Finish this Lab? (Make sure to save any data you"
+" want! After finishing the lab, resources will be deleted.)"
msgstr ""
-"Tem certeza de que deseja Finalizar este Lab? (Certifique-se de salvar todos"
-" os dados que desejar! Após finalizar o lab, os recursos serão excluídos.)"
+"Tem certeza de que deseja Finalizar este Lab? (Certifique-se de salvar "
+"todos os dados que desejar! Após finalizar o lab, os recursos serão "
+"excluídos.)"
#: apps/templates/pages/lab_instance_view.html:217
#: apps/templates/pages/lab_instance_view.html:239
@@ -2657,7 +2707,9 @@ msgstr "Bifurcando Lab"
#: apps/templates/pages/labs_edit.html:73
#, python-format
msgid "You are forking \"%(name)s\". Nothing is saved until you press Submit."
-msgstr "Você está bifurcando \"%(name)s\". Nada é salvo até você pressionar Enviar."
+msgstr ""
+"Você está bifurcando \"%(name)s\". Nada é salvo até você pressionar "
+"Enviar."
#: apps/templates/pages/labs_edit.html:109
msgid "Enter lab title..."
@@ -2668,7 +2720,9 @@ msgid "Display order"
msgstr "Ordem de exibição"
#: apps/templates/pages/labs_edit.html:119
-msgid "Labs are listed by ascending display order, then by title. Default is 1000."
+msgid ""
+"Labs are listed by ascending display order, then by title. Default is "
+"1000."
msgstr ""
"Os Labs são listados por ordem de exibição crescente e, em seguida, por "
"título. O padrão é 1000."
@@ -2678,12 +2732,14 @@ msgstr ""
msgid "Lab extended description"
msgstr "Descrição estendida do Lab"
-#: apps/templates/pages/labs_edit.html:188 apps/templates/pages/labs_edit.html:219
+#: apps/templates/pages/labs_edit.html:188
+#: apps/templates/pages/labs_edit.html:219
#: apps/templates/pages/labs_edit.html:296
msgid "Version history"
msgstr "Histórico de versões"
-#: apps/templates/pages/labs_edit.html:189 apps/templates/pages/labs_edit.html:220
+#: apps/templates/pages/labs_edit.html:189
+#: apps/templates/pages/labs_edit.html:220
#: apps/templates/pages/labs_edit.html:297
msgid "Versions"
msgstr "Versões"
@@ -2697,19 +2753,19 @@ msgstr "Manifesto Kubernetes do Lab"
#, python-brace-format
msgid ""
"Add here your Kubernetes Manifest file containing Pods, Deployments and "
-"Services. You should customize the item names with variables that will be "
-"replaced during resource creation, example: ${pod_hash}"
+"Services. You should customize the item names with variables that will be"
+" replaced during resource creation, example: ${pod_hash}"
msgstr ""
"Adicione aqui o seu arquivo de Manifesto Kubernetes contendo Pods, "
"Deployments e Services. Você deve personalizar os nomes dos itens com "
-"variáveis que serão substituídas durante a criação dos recursos, exemplo: "
-"${pod_hash}"
+"variáveis que serão substituídas durante a criação dos recursos, exemplo:"
+" ${pod_hash}"
#: apps/templates/pages/labs_edit.html:228
msgid "You can also attach data files to this Lab (see Lab Data below)."
msgstr ""
-"Você também pode anexar arquivos de dados a este Lab (veja Lab Data"
-" abaixo)."
+"Você também pode anexar arquivos de dados a este Lab (veja Lab "
+"Data abaixo)."
#: apps/templates/pages/labs_edit.html:232
msgid "Kubernetes Manifest Template:"
@@ -2732,7 +2788,9 @@ msgstr "Forçar Atualização"
msgid ""
"Read %(link_start)smore information%(link_end)s about the Kubernetes "
"manifest."
-msgstr "Leia %(link_start)smais informações%(link_end)s sobre o manifesto Kubernetes."
+msgstr ""
+"Leia %(link_start)smais informações%(link_end)s sobre o manifesto "
+"Kubernetes."
#: apps/templates/pages/labs_edit.html:253
msgid "Lab Data"
@@ -2744,27 +2802,29 @@ msgid ""
"Attach data files to be mounted into the Lab Pods. Each file becomes a "
"ConfigMap named %(name)s and is limited to 950 KiB."
msgstr ""
-"Anexe arquivos de dados para serem montados nos Pods do Lab. Cada arquivo se"
-" torna um ConfigMap chamado %(name)s e é limitado a 950 KiB."
+"Anexe arquivos de dados para serem montados nos Pods do Lab. Cada arquivo"
+" se torna um ConfigMap chamado %(name)s e é limitado a 950 KiB."
#: apps/templates/pages/labs_edit.html:255
msgid ""
-"Each attached file is published as its own Kubernetes ConfigMap named labdata-XXXX (the exact name is shown next to each file). To make a "
-"file available inside a container, declare a volume pointing to"
-" that ConfigMap and a matching volumeMount, for example:"
+"Each attached file is published as its own Kubernetes ConfigMap named "
+"labdata-XXXX (the exact name is shown next to each file). To"
+" make a file available inside a container, declare a volume "
+"pointing to that ConfigMap and a matching volumeMount, for "
+"example:"
msgstr ""
"Cada arquivo anexado é publicado como um ConfigMap Kubernetes próprio, "
-"chamado labdata-XXXX (o nome exato é exibido ao lado de cada "
-"arquivo). Para disponibilizar um arquivo dentro de um contêiner, declare um "
-"volume apontando para esse ConfigMap e um "
+"chamado labdata-XXXX (o nome exato é exibido ao lado de cada"
+" arquivo). Para disponibilizar um arquivo dentro de um contêiner, declare"
+" um volume apontando para esse ConfigMap e um "
"volumeMount correspondente, por exemplo:"
#: apps/templates/pages/labs_edit.html:265
msgid "Click to add lab data files"
msgstr "Clique para adicionar arquivos de Lab Data"
-#: apps/templates/pages/labs_edit.html:272 apps/templates/pages/labs_edit.html:884
+#: apps/templates/pages/labs_edit.html:272
+#: apps/templates/pages/labs_edit.html:884
msgid "ConfigMap name (use it in your manifest)"
msgstr "Nome do ConfigMap (use-o no seu manifesto)"
@@ -2796,7 +2856,8 @@ msgstr "Arquivos anexados"
msgid "back to the top"
msgstr "voltar ao topo"
-#: apps/templates/pages/labs_edit.html:508 apps/templates/pages/labs_view.html:161
+#: apps/templates/pages/labs_edit.html:508
+#: apps/templates/pages/labs_view.html:161
msgid "Fork"
msgstr "Bifurcar"
@@ -2808,27 +2869,33 @@ msgstr "Restaurar Lab"
msgid "Delete Lab"
msgstr "Excluir Lab"
-#: apps/templates/pages/labs_edit.html:527 apps/templates/pages/labs_view.html:208
+#: apps/templates/pages/labs_edit.html:527
+#: apps/templates/pages/labs_view.html:208
msgid "Confirm restore lab"
msgstr "Confirmar restauração do laboratório"
-#: apps/templates/pages/labs_edit.html:533 apps/templates/pages/labs_view.html:215
+#: apps/templates/pages/labs_edit.html:533
+#: apps/templates/pages/labs_view.html:215
msgid "Are you sure you want to restore this lab?"
msgstr "Tem certeza de que deseja restaurar este laboratório?"
-#: apps/templates/pages/labs_edit.html:537 apps/templates/pages/labs_view.html:219
+#: apps/templates/pages/labs_edit.html:537
+#: apps/templates/pages/labs_view.html:219
msgid "Restore lab"
msgstr "Restaurar laboratório"
-#: apps/templates/pages/labs_edit.html:550 apps/templates/pages/labs_view.html:185
+#: apps/templates/pages/labs_edit.html:550
+#: apps/templates/pages/labs_view.html:185
msgid "Confirm delete lab"
msgstr "Confirmar exclusão do laboratório"
-#: apps/templates/pages/labs_edit.html:556 apps/templates/pages/labs_view.html:192
+#: apps/templates/pages/labs_edit.html:556
+#: apps/templates/pages/labs_view.html:192
msgid "Are you sure you want to delete Lab"
msgstr "Tem certeza de que deseja excluir o Laboratório"
-#: apps/templates/pages/labs_edit.html:560 apps/templates/pages/labs_view.html:196
+#: apps/templates/pages/labs_edit.html:560
+#: apps/templates/pages/labs_view.html:196
msgid "Delete lab"
msgstr "Excluir laboratório"
@@ -2838,8 +2905,8 @@ msgstr "Histórico de versões:"
#: apps/templates/pages/labs_edit.html:582
msgid ""
-"Restoring a version loads it back into the editor; it becomes a new version "
-"once you save the Lab."
+"Restoring a version loads it back into the editor; it becomes a new "
+"version once you save the Lab."
msgstr ""
"Restaurar uma versão a carrega de volta no editor; ela se torna uma nova "
"versão quando você salva o Lab."
@@ -2856,7 +2923,8 @@ msgstr "Salvo em (UTC)"
msgid "Author"
msgstr "Autor"
-#: apps/templates/pages/labs_edit.html:672 apps/templates/pages/labs_edit.html:677
+#: apps/templates/pages/labs_edit.html:672
+#: apps/templates/pages/labs_edit.html:677
msgid "Failed to upload image:"
msgstr "Falha ao enviar a imagem:"
@@ -2865,8 +2933,8 @@ msgid ""
"Warning: this file is still referenced in the Lab Guide. Remove the "
"reference before deleting."
msgstr ""
-"Aviso: este arquivo ainda é referenciado no Guia do Lab. Remova a referência"
-" antes de excluir."
+"Aviso: este arquivo ainda é referenciado no Guia do Lab. Remova a "
+"referência antes de excluir."
#: apps/templates/pages/labs_edit.html:746
msgid ""
@@ -2876,17 +2944,21 @@ msgstr ""
"Aviso: este arquivo ainda é referenciado na descrição estendida do Lab. "
"Remova a referência antes de excluir."
-#: apps/templates/pages/labs_edit.html:757 apps/templates/pages/labs_edit.html:762
-#: apps/templates/pages/labs_edit.html:950 apps/templates/pages/labs_edit.html:955
+#: apps/templates/pages/labs_edit.html:757
+#: apps/templates/pages/labs_edit.html:762
+#: apps/templates/pages/labs_edit.html:950
+#: apps/templates/pages/labs_edit.html:955
msgid "Failed to remove file:"
msgstr "Falha ao remover o arquivo:"
-#: apps/templates/pages/labs_edit.html:761 apps/templates/pages/labs_edit.html:954
+#: apps/templates/pages/labs_edit.html:761
+#: apps/templates/pages/labs_edit.html:954
#: apps/templates/pages/labs_edit.html:1192
msgid "Unknown error"
msgstr "Erro desconhecido"
-#: apps/templates/pages/labs_edit.html:919 apps/templates/pages/labs_edit.html:924
+#: apps/templates/pages/labs_edit.html:919
+#: apps/templates/pages/labs_edit.html:924
msgid "Failed to upload lab data file:"
msgstr "Falha ao enviar o arquivo de Lab Data:"
@@ -2895,8 +2967,8 @@ msgid ""
"Warning: this ConfigMap is still referenced in the Kubernetes Manifest. "
"Remove the reference before deleting."
msgstr ""
-"Aviso: este ConfigMap ainda é referenciado no Manifesto Kubernetes. Remova a"
-" referência antes de excluir."
+"Aviso: este ConfigMap ainda é referenciado no Manifesto Kubernetes. "
+"Remova a referência antes de excluir."
#: apps/templates/pages/labs_edit.html:1055
msgid "Duplicated question name found:"
@@ -3056,7 +3128,8 @@ msgstr "Filtrar por Categoria:"
msgid "Filter by Status:"
msgstr "Filtrar por Status:"
-#: apps/templates/pages/labs_view.html:86 apps/templates/pages/labs_view.html:135
+#: apps/templates/pages/labs_view.html:86
+#: apps/templates/pages/labs_view.html:135
#: apps/templates/pages/labs_view.html:140
msgid "Completed"
msgstr "Concluído"
@@ -3112,13 +3185,14 @@ msgstr "Registros de Plataforma"
#: apps/templates/pages/lti_management.html:55
msgid ""
-"LTI 1.3 platform registrations (created by dynamic registration). Rotate a "
-"registration's signing key or show its public key (PEM) to paste into the "
-"LMS when it cannot fetch /lti/jwks/."
+"LTI 1.3 platform registrations (created by dynamic registration). Rotate "
+"a registration's signing key or show its public key (PEM) to paste into "
+"the LMS when it cannot fetch /lti/jwks/."
msgstr ""
-"Registros de plataforma LTI 1.3 (criados por registro dinâmico). Rotacione a"
-" chave de assinatura de um registro ou exiba sua chave pública (PEM) para "
-"colar no LMS quando ele não conseguir obter /lti/jwks/."
+"Registros de plataforma LTI 1.3 (criados por registro dinâmico). "
+"Rotacione a chave de assinatura de um registro ou exiba sua chave pública"
+" (PEM) para colar no LMS quando ele não conseguir obter "
+"/lti/jwks/."
#: apps/templates/pages/lti_management.html:61
msgid "Client ID"
@@ -3156,12 +3230,12 @@ msgstr "Gerar Token"
#: apps/templates/pages/lti_management.html:107
msgid ""
-"One-time credentials for the dynamic registration endpoint. The token itself"
-" is shown only once, at mint time; only its hash is stored."
+"One-time credentials for the dynamic registration endpoint. The token "
+"itself is shown only once, at mint time; only its hash is stored."
msgstr ""
-"Credenciais de uso único para o endpoint de registro dinâmico. O token em si"
-" é exibido apenas uma vez, no momento da geração; somente o seu hash é "
-"armazenado."
+"Credenciais de uso único para o endpoint de registro dinâmico. O token em"
+" si é exibido apenas uma vez, no momento da geração; somente o seu hash é"
+" armazenado."
#: apps/templates/pages/lti_management.html:112
msgid "ID"
@@ -3206,14 +3280,14 @@ msgstr "Expurgar Chaves Aposentadas"
#: apps/templates/pages/lti_management.html:160
msgid ""
-"Rotated keys stay published in /lti/jwks/ during a grace period"
-" so platforms validating cached tokens keep finding the old key. Purge them "
-"once the grace period has passed."
+"Rotated keys stay published in /lti/jwks/ during a grace "
+"period so platforms validating cached tokens keep finding the old key. "
+"Purge them once the grace period has passed."
msgstr ""
"As chaves rotacionadas permanecem publicadas em /lti/jwks/ "
-"durante um período de carência, para que as plataformas que validam tokens "
-"em cache continuem encontrando a chave antiga. Expurgue-as assim que o "
-"período de carência terminar."
+"durante um período de carência, para que as plataformas que validam "
+"tokens em cache continuem encontrando a chave antiga. Expurgue-as assim "
+"que o período de carência terminar."
#: apps/templates/pages/lti_management.html:165
msgid "Key File"
@@ -3241,7 +3315,9 @@ msgstr "Validade (horas)"
#: apps/templates/pages/lti_management.html:203
msgid "Registration URL (shown once — hand it to the LMS admin):"
-msgstr "URL de registro (exibida uma única vez — entregue ao administrador do LMS):"
+msgstr ""
+"URL de registro (exibida uma única vez — entregue ao administrador do "
+"LMS):"
#: apps/templates/pages/lti_management.html:207
msgid "Copy URL"
@@ -3270,8 +3346,8 @@ msgid ""
"period."
msgstr ""
"A nova chave é publicada imediatamente; a chave antiga é aposentada, mas "
-"permanece publicada em /lti/jwks/ até que você a expurgue após "
-"o período de carência."
+"permanece publicada em /lti/jwks/ até que você a expurgue "
+"após o período de carência."
#: apps/templates/pages/lti_management.html:259
msgid "Delete retired key files older than the grace period."
@@ -3378,11 +3454,11 @@ msgstr ""
#: apps/templates/pages/my_support_thread_view.html:81
msgid ""
-"This conversation is finished. Start a new one from the chat button at the "
-"bottom-right."
+"This conversation is finished. Start a new one from the chat button at "
+"the bottom-right."
msgstr ""
-"Esta conversa foi finalizada. Inicie uma nova pelo botão de chat no canto "
-"inferior direito."
+"Esta conversa foi finalizada. Inicie uma nova pelo botão de chat no canto"
+" inferior direito."
#: apps/templates/pages/my_support_threads.html:21
msgid "My Support Cases"
@@ -3435,11 +3511,13 @@ msgstr "Página não encontrada"
#: apps/templates/pages/page-404.html:76
msgid ""
-"The page you are looking for might have been removed, had its name changed, "
-"or is temporarily unavailable. Please check the URL for any mistakes."
+"The page you are looking for might have been removed, had its name "
+"changed, or is temporarily unavailable. Please check the URL for any "
+"mistakes."
msgstr ""
-"A página que você procura pode ter sido removida, ter tido seu nome alterado"
-" ou estar temporariamente indisponível. Verifique se há erros na URL."
+"A página que você procura pode ter sido removida, ter tido seu nome "
+"alterado ou estar temporariamente indisponível. Verifique se há erros na "
+"URL."
#: apps/templates/pages/page-404.html:79 apps/templates/pages/page-500.html:127
msgid "Return to Home"
@@ -3455,12 +3533,13 @@ msgstr "Erro interno do servidor"
#: apps/templates/pages/page-500.html:124
msgid ""
-"Our servers are currently experiencing technical difficulties. Our team has "
-"been notified and is working to resolve the issue as quickly as possible."
+"Our servers are currently experiencing technical difficulties. Our team "
+"has been notified and is working to resolve the issue as quickly as "
+"possible."
msgstr ""
-"Nossos servidores estão enfrentando dificuldades técnicas no momento. Nossa "
-"equipe foi notificada e está trabalhando para resolver o problema o mais "
-"rápido possível."
+"Nossos servidores estão enfrentando dificuldades técnicas no momento. "
+"Nossa equipe foi notificada e está trabalhando para resolver o problema o"
+" mais rápido possível."
#: apps/templates/pages/page-500.html:128
msgid "Contact Support"
@@ -3488,11 +3567,11 @@ msgstr "Já tem uma conta?"
#: apps/templates/pages/register.html:131
msgid ""
-"Invalid character. Use letters, numbers, dot (.), underscore (_) or hyphen "
-"(-)"
+"Invalid character. Use letters, numbers, dot (.), underscore (_) or "
+"hyphen (-)"
msgstr ""
-"Caractere inválido. Use letras, números, ponto (.), sublinhado (_) ou hífen "
-"(-)"
+"Caractere inválido. Use letras, números, ponto (.), sublinhado (_) ou "
+"hífen (-)"
#: apps/templates/pages/reset_password.html:40
msgid "Enter your email or username"
@@ -3538,7 +3617,9 @@ msgstr "Executar"
#: apps/templates/pages/run_lab.html:140
#, python-format
msgid "Visit %(link_start)sLab documentation%(link_end)s for more information."
-msgstr "Visite a %(link_start)sdocumentação do Lab%(link_end)s para mais informações."
+msgstr ""
+"Visite a %(link_start)sdocumentação do Lab%(link_end)s para mais "
+"informações."
#: apps/templates/pages/run_lab.html:178
msgid "Starting lab..."
@@ -3575,13 +3656,13 @@ msgstr "Comece a usar os recursos do Lab e o guia do Lab!"
#: apps/templates/pages/run_lab_status.html:117
msgid ""
-"We could not confirm all resources became ready within 5 minutes. Your Lab "
-"may still be provisioning in the background — open the Lab page below to "
-"check it."
+"We could not confirm all resources became ready within 5 minutes. Your "
+"Lab may still be provisioning in the background — open the Lab page below"
+" to check it."
msgstr ""
"Não conseguimos confirmar que todos os recursos ficaram prontos em 5 "
-"minutos. Seu Lab pode ainda estar sendo provisionado em segundo plano — abra"
-" a página do Lab abaixo para verificar."
+"minutos. Seu Lab pode ainda estar sendo provisionado em segundo plano — "
+"abra a página do Lab abaixo para verificar."
#: apps/templates/pages/run_lab_status.html:118
msgid "Taking more than expected, but still waiting for resources."
@@ -3589,11 +3670,11 @@ msgstr "Está demorando mais que o esperado, mas ainda aguardando os recursos."
#: apps/templates/pages/run_lab_status.html:119
msgid ""
-"Almost there — provisioning can take a few minutes on busy clusters. Please "
-"bear with us a little longer."
+"Almost there — provisioning can take a few minutes on busy clusters. "
+"Please bear with us a little longer."
msgstr ""
-"Quase lá — o provisionamento pode levar alguns minutos em clusters ocupados."
-" Aguarde mais um pouco, por favor."
+"Quase lá — o provisionamento pode levar alguns minutos em clusters "
+"ocupados. Aguarde mais um pouco, por favor."
#: apps/templates/pages/run_lab_status.html:120
msgid "Still waiting for all resources to be provisioned."
@@ -3601,7 +3682,9 @@ msgstr "Ainda aguardando o provisionamento de todos os recursos."
#: apps/templates/pages/run_lab_status.html:121
msgid "This is taking a little longer than usual — thank you for your patience!"
-msgstr "Isto está demorando um pouco mais que o normal — obrigado pela sua paciência!"
+msgstr ""
+"Isto está demorando um pouco mais que o normal — obrigado pela sua "
+"paciência!"
#: apps/templates/pages/run_lab_status.html:138
msgid "Check the running Lab"
@@ -3760,8 +3843,8 @@ msgstr "Você deve aguardar até que seu usuário seja aprovado!"
#: apps/templates/pages/waiting_approval.html:79
msgid ""
-"Your note below was already sent to the administrators and can no longer be "
-"changed. Please contact an administrator if you need to update it."
+"Your note below was already sent to the administrators and can no longer "
+"be changed. Please contact an administrator if you need to update it."
msgstr ""
"Sua nota abaixo já foi enviada aos administradores e não pode mais ser "
"alterada. Entre em contato com um administrador se precisar atualizá-la."
@@ -3777,13 +3860,15 @@ msgstr "aguarde!"
#: apps/templates/pages/waiting_approval.html:94
msgid ""
-"To help the administrators identify you, please leave a note below with a "
-"reference (e.g., your institution, course, professor, or who referred you to"
-" HackInSDN). Note that once saved, the note can no longer be changed."
+"To help the administrators identify you, please leave a note below with a"
+" reference (e.g., your institution, course, professor, or who referred "
+"you to HackInSDN). Note that once saved, the note can no longer be "
+"changed."
msgstr ""
-"Para ajudar os administradores a identificá-lo, deixe uma nota abaixo com "
-"uma referência (ex.: sua instituição, curso, professor ou quem o indicou ao "
-"HackInSDN). Observe que, uma vez salva, a nota não pode mais ser alterada."
+"Para ajudar os administradores a identificá-lo, deixe uma nota abaixo com"
+" uma referência (ex.: sua instituição, curso, professor ou quem o indicou"
+" ao HackInSDN). Observe que, uma vez salva, a nota não pode mais ser "
+"alterada."
#: apps/templates/pages/waiting_approval.html:97
msgid "Ex: I'm a student of Prof. X at University Y"
@@ -3800,88 +3885,91 @@ msgstr "Salvar nota"
#~ msgstr "Pontuação da resposta do lab (%):"
#~ msgid ""
-#~ "The lab answer sheet can be used "
-#~ "to automatically validate the answers "
+#~ "The lab answer sheet can be used"
+#~ " to automatically validate the answers "
#~ "provided by the users by using "
#~ "regular expression matching. Please choose "
-#~ "the Lab below and, for each question,"
-#~ " provide the expected regex which the"
-#~ " answer should match to be considered"
-#~ " correct (the calculation is done in "
-#~ "Python using re library, pretty "
-#~ "much like this: "
+#~ "the Lab below and, for each "
+#~ "question, provide the expected regex "
+#~ "which the answer should match to "
+#~ "be considered correct (the calculation "
+#~ "is done in Python using re"
+#~ " library, pretty much like this: "
#~ "re.match(fr\"^{expected_answer}$\", answer)). "
#~ "Only the registered questions in the "
#~ "answer sheet will be used actually "
#~ "used for validating the score (for "
-#~ "example: if your Lab has 5 questions"
-#~ " and the answer sheet only contains "
-#~ "3 expected answers, then people who "
-#~ "provide the 3 correct ones will be"
-#~ " considered 100% correct."
+#~ "example: if your Lab has 5 "
+#~ "questions and the answer sheet only "
+#~ "contains 3 expected answers, then people"
+#~ " who provide the 3 correct ones "
+#~ "will be considered 100% correct."
#~ msgstr ""
-#~ "O gabarito do lab pode ser usado "
-#~ "para validar automaticamente as respostas "
-#~ "fornecidas pelos usuários por meio de "
-#~ "correspondência com expressões regulares. Escolha"
-#~ " o Lab abaixo e, para cada "
-#~ "pergunta, forneça a regex esperada que "
-#~ "a resposta deve corresponder para ser "
-#~ "considerada correta (o cálculo é feito "
-#~ "em Python usando a biblioteca "
-#~ "re, mais ou menos assim: "
-#~ "re.match(fr\"^{expected_answer}$\", answer)). "
-#~ "Apenas as perguntas registradas no gabarito"
-#~ " serão de fato usadas para validar "
-#~ "a pontuação (por exemplo: se o seu"
-#~ " Lab tem 5 perguntas e o gabarito"
-#~ " contém apenas 3 respostas esperadas, "
-#~ "então quem fornecer as 3 corretas "
-#~ "será considerado 100% correto."
+#~ "O gabarito do lab pode ser usado"
+#~ " para validar automaticamente as respostas"
+#~ " fornecidas pelos usuários por meio "
+#~ "de correspondência com expressões regulares."
+#~ " Escolha o Lab abaixo e, para "
+#~ "cada pergunta, forneça a regex esperada"
+#~ " que a resposta deve corresponder "
+#~ "para ser considerada correta (o cálculo"
+#~ " é feito em Python usando a "
+#~ "biblioteca re, mais ou menos "
+#~ "assim: re.match(fr\"^{expected_answer}$\", "
+#~ "answer)). Apenas as perguntas "
+#~ "registradas no gabarito serão de fato"
+#~ " usadas para validar a pontuação (por"
+#~ " exemplo: se o seu Lab tem 5"
+#~ " perguntas e o gabarito contém apenas"
+#~ " 3 respostas esperadas, então quem "
+#~ "fornecer as 3 corretas será considerado"
+#~ " 100% correto."
#~ msgid "View Feedback"
#~ msgstr "Ver Feedback"
#~ msgid ""
-#~ "You can also attach data files to "
-#~ "this Lab (see Lab Data below). "
-#~ "Each attached file is published as "
-#~ "its own Kubernetes ConfigMap named labdata-XXXX (the exact name is "
-#~ "shown next to each file). To make "
-#~ "a file available inside a container, "
-#~ "declare a volume pointing to "
-#~ "that ConfigMap and a matching "
-#~ "volumeMount, for example:"
+#~ "If you have identified a defect or"
+#~ " any unexpected behaviour, we kindly "
+#~ "ask you to report it by opening"
+#~ " an issue in our GitHub repository."
+#~ " Please describe the steps required "
+#~ "to reproduce the problem, the result "
+#~ "you expected and the result you "
+#~ "obtained."
#~ msgstr ""
-#~ "Você também pode anexar arquivos de "
-#~ "dados a este Lab (veja Lab "
-#~ "Data abaixo). Cada arquivo anexado é"
-#~ " publicado como um ConfigMap Kubernetes "
-#~ "próprio, chamado labdata-XXXX (o "
-#~ "nome exato é exibido ao lado de "
-#~ "cada arquivo). Para disponibilizar um "
-#~ "arquivo dentro de um contêiner, declare "
-#~ "um volume apontando para esse "
-#~ "ConfigMap e um volumeMount "
-#~ "correspondente, por exemplo:"
+#~ "Caso tenha identificado um defeito ou"
+#~ " qualquer comportamento inesperado, solicitamos"
+#~ " que o relate abrindo uma issue "
+#~ "em nosso repositório no GitHub. Descreva"
+#~ " os passos necessários para reproduzir "
+#~ "o problema, o resultado esperado e "
+#~ "o resultado obtido."
#~ msgid ""
-#~ "If you have identified a defect or"
-#~ " any unexpected behaviour, we kindly ask"
-#~ " you to report it by opening an"
-#~ " issue in our GitHub repository. Please"
-#~ " describe the steps required to "
-#~ "reproduce the problem, the result you "
-#~ "expected and the result you obtained."
+#~ "You can also attach data files to"
+#~ " this Lab (see Lab Data "
+#~ "below). Each attached file is published"
+#~ " as its own Kubernetes ConfigMap "
+#~ "named labdata-XXXX (the exact "
+#~ "name is shown next to each file)."
+#~ " To make a file available inside "
+#~ "a container, declare a volume "
+#~ "pointing to that ConfigMap and a "
+#~ "matching volumeMount, for example:"
#~ msgstr ""
-#~ "Caso tenha identificado um defeito ou "
-#~ "qualquer comportamento inesperado, solicitamos "
-#~ "que o relate abrindo uma issue em "
-#~ "nosso repositório no GitHub. Descreva os"
-#~ " passos necessários para reproduzir o "
-#~ "problema, o resultado esperado e o "
-#~ "resultado obtido."
+#~ "Você também pode anexar arquivos de "
+#~ "dados a este Lab (veja Lab "
+#~ "Data abaixo). Cada arquivo anexado "
+#~ "é publicado como um ConfigMap Kubernetes"
+#~ " próprio, chamado labdata-XXXX "
+#~ "(o nome exato é exibido ao lado"
+#~ " de cada arquivo). Para disponibilizar "
+#~ "um arquivo dentro de um contêiner, "
+#~ "declare um volume apontando para"
+#~ " esse ConfigMap e um "
+#~ "volumeMount correspondente, por "
+#~ "exemplo:"
#~ msgid "Open an issue on GitHub"
#~ msgstr "Abrir uma issue no GitHub"
diff --git a/apps/utils.py b/apps/utils.py
index 1db9053..cf18cdb 100644
--- a/apps/utils.py
+++ b/apps/utils.py
@@ -216,6 +216,16 @@ def parse_lab_expiration(expiration):
return int(exp_date.timestamp())
+def parse_group_expiration(expiration):
+ """Convert the group expiration date submitted by the form (ISO date,
+ YYYY-MM-DD) into a datetime, since Groups.expiration is a DateTime column
+ and the raw string would be rejected by the database driver. Empty values
+ mean "never expires"; anything else raises ValueError."""
+ if not expiration or not expiration.strip():
+ return None
+ return datetime.datetime.strptime(expiration.strip(), "%Y-%m-%d")
+
+
def datetime_from_ts(timestamp):
try:
dt = datetime.datetime.fromtimestamp(timestamp, tz=datetime.timezone.utc)
diff --git a/scripts/i18n_ptbr.py b/scripts/i18n_ptbr.py
index 79a7322..a671b2d 100644
--- a/scripts/i18n_ptbr.py
+++ b/scripts/i18n_ptbr.py
@@ -843,6 +843,7 @@
"You don't have permission to edit this group.": "Você não tem permissão para editar este grupo.",
"Only admins can create/change System groups.": "Somente administradores podem criar/alterar grupos de Sistema.",
"No changes were made to the group.": "Nenhuma alteração foi feita no grupo.",
+ "Invalid expiration date, please use the format YYYY-MM-DD.": "Data de expiração inválida, use o formato AAAA-MM-DD.",
"Failed to update group.": "Falha ao atualizar o grupo.",
"Group updated successfully": "Grupo atualizado com sucesso",
"Invalid Lab provided for filtering.": "Lab inválido fornecido para filtragem.",
diff --git a/tests/test_groups.py b/tests/test_groups.py
index 24566fb..08a23fd 100644
--- a/tests/test_groups.py
+++ b/tests/test_groups.py
@@ -22,6 +22,7 @@
other test modules that share the same singleton app/database (apps/config.py
reads DATA_DIR once at import time, so every module ends up on one DB).
"""
+import datetime
import os
import sys
import tempfile
@@ -177,6 +178,30 @@ def test_admin_can_create_group(self, client, ids):
assert group.organization == "ORG-NEW"
logout(client)
+ def test_admin_can_create_group_with_expiration(self, client, ids):
+ login(client, "gradmin", "admin123")
+ resp = client.post(
+ "/groups/edit/new",
+ data=group_form(groupname="ExpiringGroup", expiration="2026-07-31"),
+ follow_redirects=True,
+ )
+ assert resp.status_code == 200
+ assert b"Group updated successfully" in resp.data
+
+ group = Groups.query.filter_by(groupname="ExpiringGroup").first()
+ assert group.expiration == datetime.datetime(2026, 7, 31)
+ logout(client)
+
+ def test_invalid_expiration_is_rejected(self, client, ids):
+ login(client, "gradmin", "admin123")
+ resp = client.post(
+ "/groups/edit/new",
+ data=group_form(groupname="BadExpiration", expiration="07/31/2026"),
+ )
+ assert b"Invalid expiration date" in resp.data
+ assert Groups.query.filter_by(groupname="BadExpiration").first() is None
+ logout(client)
+
def test_student_cannot_create_group(self, client, ids):
login(client, "grstudent", "stud123")
resp = client.post("/groups/edit/new", data=group_form(groupname="StudentGroup"))
diff --git a/tests/test_utils.py b/tests/test_utils.py
index ac003ca..19e6e5b 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -1,6 +1,6 @@
"""Pytest suite for apps/utils.py.
-Covers the pure helpers (utcnow, parse_lab_expiration, datetime_from_ts,
+Covers the pure helpers (utcnow, parse_lab_expiration, parse_group_expiration, datetime_from_ts,
secure_filename, format_duration, list_files, remove_empty_folders) and the
DB-backed helpers (check_pre_approved, update_running_labs_stats,
update_category_stats, update_stats_lab_instances_answers).
@@ -86,6 +86,18 @@ def test_parse_lab_expiration_hours(self):
expected = utils.utcnow() + datetime.timedelta(hours=4)
assert abs(ts - int(expected.timestamp())) < 5
+ def test_parse_group_expiration_empty(self):
+ assert utils.parse_group_expiration("") is None
+ assert utils.parse_group_expiration(None) is None
+ assert utils.parse_group_expiration(" ") is None
+
+ def test_parse_group_expiration_iso_date(self):
+ assert utils.parse_group_expiration("2026-07-31") == datetime.datetime(2026, 7, 31)
+
+ def test_parse_group_expiration_invalid(self):
+ with pytest.raises(ValueError):
+ utils.parse_group_expiration("07/31/2026")
+
def test_datetime_from_ts_valid(self):
result = utils.datetime_from_ts(0)
assert result == "1970-01-01T00:00:00+00:00"