Se você usou wget
para baixar finance.zip
antes das 2024-04-10T08:30:00-04:00, certifique-se de copiar, colar e executar este comando dentro do seu diretório finance
para baixar uma versão mais recente de helpers.py
:
[ -f helpers.py ] && curl -O https://cdn.cs50.net/2024/x/psets/9/finance/helpers.py
C$50 Finanças
Implemente um site no qual usuários podem "comprar" e "vender" ações, como abaixo.
Histórico
Se você não tem certeza sobre o que significa comprar e vender ações (por ex., as porcentagens de uma empresa), vá aqui para um tutorial.
Você está prestes a implementar o C$50 Finanças, um aplicativo web no qual você pode gerenciar portfólios de ações. Essa ferramenta não só permitirá que você confira os preços reais das ações e os valores dos portfólios, como também permitirá que você compre (ok, "compre") e venda (ok, "venda") ações consultando os preços das ações.
De fato, existem ferramentas (uma é conhecida como IEX) que permitem que você baixe cotações de ações pela API (interface de programação de aplicativos) usando URLs como https://api.iex.cloud/v1/data/core/quote/nflx?token=API_KEY
. Observe como o símbolo da Netflix (NFLX) está embutido neste URL; é assim que o IEX sabe de quem deve retornar os dados. Esse link não retornará nenhum dado porque o IEX exige que você use uma chave de API, mas se retornasse, você veria uma resposta no formato JSON (JavaScript Object Notation) como este:
{
"avgTotalVolume":6787785,
"calculationPrice":"tops",
"change":1.46,
"changePercent":0.00336,
"close":null,
"closeSource":"official",
"closeTime":null,
"companyName":"Netflix Inc.",
"currency":"USD",
"delayedPrice":null,
"delayedPriceTime":null,
"extendedChange":null,
"extendedChangePercent":null,
"extendedPrice":null,
"extendedPriceTime":null,
"high":null,
"highSource":"IEX real time price",
"highTime":1699626600947,
"iexAskPrice":460.87,
"iexAskSize":123,
"iexBidPrice":435,
"iexBidSize":100,
"iexClose":436.61,
"iexCloseTime":1699626704609,
"iexLastUpdated":1699626704609,
"iexMarketPercent":0.00864679844447232,
"iexOpen":437.37,
"iexOpenTime":1699626600859,
"iexRealtimePrice":436.61,
"iexRealtimeSize":5,
"iexVolume":965,
"lastTradeTime":1699626704609,
"latestPrice":436.61,
"latestSource":"IEX real time price",
"latestTime":"9:31:44 AM",
"latestUpdate":1699626704609,
"latestVolume":null,
"low":null,
"lowSource":"IEX real time price",
"lowTime":1699626634509,
"marketCap":192892118443,
"oddLotDelayedPrice":null,
"oddLotDelayedPriceTime":null,
"open":null,
"openTime":null,
"openSource":"official",
"peRatio":43.57,
"previousClose":435.15,
"previousVolume":2735507,
"primaryExchange":"NASDAQ",
"symbol":"NFLX",
"volume":null,
"week52High":485,
"week52Low":271.56,
"ytdChange":0.4790450244167119,
"isUSMarketOpen":true
}
Observe como, entre as chaves, há uma lista separada por vírgulas de pares chave-valor, com dois pontos separando cada chave do seu valor. Faremos algo muito parecido, com o Yahoo Finance.
Vamos nos concentrar agora em obter o código de distribuição deste problema!
Começando
Faça login em cs50.dev, clique na sua janela de terminal e execute cd
por si só. Você deve descobrir que o prompt da janela de terminal se parece com o abaixo:
$
Em seguida, execute
wget https://cdn.cs50.net/2024/x/psets/9/finance.zip
para baixar um ZIP chamado finance.zip
no seu espaço de código.
Então execute
unzip finance.zip
para criar uma pasta chamada finance
. Você não precisará mais do arquivo ZIP, então você pode executar
rm finance.zip
e responder com "y" seguido de Enter no prompt para remover o arquivo ZIP que você baixou.
Agora digite
cd finance
seguido de Enter para ir para (por ex., abrir) esse diretório. Seu prompt agora deve se parecer com o abaixo.
finance/ $
Execute ls
por si só e você verá alguns arquivos e pastas:
app.py finance.db helpers.py requirements.txt static/ templates/
Se você tiver algum problema, siga estes mesmos passos novamente e veja se consegue determinar onde errou!
Execução
Inicie o servidor web integrado ao Flask (dentro de finance/
):
$ flask run
Visite a URL exibida por flask
para ver o código de distribuição em ação. Você não conseguirá fazer login ou se registrar por enquanto!
Dentro de finance/
, execute sqlite3 finance.db
para abrir finance.db
com o sqlite3
. Se você executar .schema
no prompt do SQLite, observe como finance.db
vem com uma tabela chamada users
(usuários). Dê uma olhada em sua estrutura (por ex., esquema). Observe como, por padrão, os novos usuários receberão US$ 10.000 em dinheiro. Mas se você executar SELECT * FROM users;
, não haverá (ainda!) usuários (por ex., linhas) lá para navegar.
Outra forma de visualizar finance.db
é com um programa chamado phpLiteAdmin. Clique em finance.db
no navegador de arquivos do seu espaço de código e, em seguida, clique no link mostrado abaixo do texto "Please visit the following link to authorize GitHub Preview". Você deverá ver informações sobre o próprio banco de dados, bem como uma tabela, users
, assim como viu no prompt sqlite3
com .schema
.
Entendimento
app.py
Abra app.py
. No início do arquivo há várias importações, entre elas o módulo SQL da CS50 e algumas funções auxiliares. Mais sobre elas em breve.
Depois de configurar o Flask, observe como esse arquivo desabilita o cache das respostas (desde que você esteja no modo de depuração, o que é ativado por padrão no seu código no Code50), para que você não faça uma alteração em algum arquivo e o seu navegador não perceba. Observe em seguida como ele configura o Jinja com um "filtro" personalizado, usd
, uma função (definida em helpers.py
) que facilitará a formatação dos valores como dólares americanos (USD). Ele configura ainda o Flask para armazenar sessões no sistema de arquivos local (ou seja, disco) em vez de armazená-las dentro de cookies (assinalados digitalmente), que é o padrão do Flask. O arquivo então configura o módulo SQL da CS50 para usar o finance.db
.
Depois disso, há várias rotas, das quais somente duas estão totalmente implementadas: login
e logout
. Leia a implementação de login
primeiro. Observe como ele usa db.execute
(da biblioteca da CS50) para consultar finance.db
. E observe como ele usa check_password_hash
para comparar hashes das senhas dos usuários. Observe também como login
"lembra" que um usuário está conectado armazenando seu user_id
, um INTEIRO, em session
. Dessa forma, qualquer uma das rotas desse arquivo pode verificar qual usuário, se houver, está conectado. Finalmente, observe como, depois que o usuário tiver conectado com sucesso, login
redirecionará para "/", levando o usuário para sua página inicial. Enquanto isso, observe como
logoutsimplesmente limpa
session`, efetivamente desconectando o usuário.
Observe como a maioria das rotas são "decoradas" com @login_required
(uma função também definida em helpers.py
). Esse decorador garante que, se um usuário tentar visitar qualquer uma dessas rotas, ele será primeiro redirecionado para login
para se conectar.
Observe também como a maioria das rotas suporta GET e POST. No entanto, a maioria delas (por enquanto!) simplesmente retorna um "pedido de desculpas", já que ainda não foram implementadas.
helpers.py
Em seguida, dê uma olhada em helpers.py
. Ah, aí está a implementação de apology
. Observe como ela acaba renderizando um template, apology.html
. Por acaso ela também define internamente outra função, escape
, que ela simplesmente usa para substituir caracteres especiais em pedidos de desculpas. Ao definir escape
dentro de apology
, nós escopaamos a primeira apenas para a segunda; nenhuma outra função conseguirá (ou precisará) chamá-la.
O próximo no arquivo é login_required
. Não se preocupe se ela for um pouco críptica, mas se você já se perguntou como uma função pode retornar outra função, aqui está um exemplo!
Depois dela vem lookup
, uma função que, dado um símbolo
(ex.: NFLX), retorna uma cotação de ações para uma empresa na forma de um dict
com duas chaves: price
, cujo valor é um float
; e symbol
, cujo valor é uma str
, uma versão padronizada (em caixa alta) do símbolo de uma ação, independentemente de como aquele símbolo foi escrito em caixa alta ou baixa quando passado para lookup
.
Observação. Se você começou esse problema em 2023, observe que lookup
não retorna mais uma chave de name
, portanto, remova-a de qualquer consulta que a espere. Nenhum nome precisa ser exibido em nenhuma página.
O último no arquivo é usd
, uma função curta que simplesmente formata um float
como USD (ex.: 1234.56
é formatado como $1,234.56
).
requirements.txt
Agora dê uma olhada rápida em requirements.txt
. Esse arquivo simplesmente prescreve os pacotes dos quais esse aplicativo dependerá.
static/
Dê uma olhada também em static/
, dentro do qual está styles.css
. É lá que fica parte do CSS inicial. Fique à vontade para alterá-lo como quiser.
templates/
Agora olhe em templates/
. Em login.html
há, essencialmente, um formulário HTML, estilizado com Bootstrap. Enquanto isso, em apology.html
, há um template para um pedido de desculpas. Lembre-se de que apology
em helpers.py
recebeu dois argumentos: message
, que foi passado para render_template
como o valor de bottom
, e, opcionalmente, code
, que foi passado para render_template
como o valor de top
. Observe em apology.html
como esses valores são usados no final! E aqui está o motivo 0:-)
Por último, vem layout.html
. Ele é um pouco maior do que o normal, mas isso ocorre principalmente porque ele vem com uma "navbar" (barra de navegação) sofisticada e otimizada para dispositivos móveis, também baseada no Bootstrap. Observe como ele define um bloco, main
, dentro do qual os templates (incluindo apology.html
e login.html
) devem ir. Ele também inclui suporte para o message flashing do Flask, para que você possa transmitir mensagens de uma rota para outra para que o usuário as veja.
Especificação
register
Conclua a implementação de register
de forma que permita que um usuário se cadastre para uma conta por meio de um formulário.
- Exija que o usuário insira um nome de usuário, implementado como um campo de texto cujo
name
éusername
. Renderize um pedido de desculpas se a entrada do usuário estiver em branco ou se o nome de usuário já existir.- Observe que
cs50.SQL.execute
lançará uma exceçãoValueError
se você tentarINSERT
um nome de usuário duplicado porque criamos umUNIQUE INDEX
emusers.username
. Portanto, certifique-se de usartry
eexcept
para determinar se o nome de usuário já existe.
- Observe que
- Exija que o usuário insira uma senha, implementada como um campo de texto cujo
name
épassword
, e em seguida a mesma senha novamente, implementada como um campo de texto cujoname
éconfirmation
. Renderize um pedido de desculpas se uma das entradas estiver em branco ou se as senhas não corresponderem. - Envie a entrada do usuário via
POST
para/register
. INSERT
o novo usuário emusers
, armazenando um hash da senha do usuário, não a senha em si. Criptografe a senha do usuário comgenerate_password_hash
Provavelmente você vai querer criar um novo template (ex.:register.html
) que seja bem semelhante alogin.html
.
Depois de implementar register
corretamente, você deve conseguir se cadastrar para obter uma conta e conectar-se (já que login
e logout
já funcionam)! E você deve conseguir ver suas linhas pelo phpLiteAdmin ou sqlite3
.
quote
Conclua a implementação de quote
de forma que permita que um usuário procure o preço atual de uma ação.
- Exija que o usuário insira o símbolo de uma ação, implementado como um campo de texto cujo
name
ésymbol
. - Envie a entrada do usuário via
POST
para/quote
. - Provavelmente você vai querer criar dois novos templates (ex.:
quote.html
equoted.html
). Quando um usuário visita/quote
via GET, renderize um desses templates, dentro do qual deve haver um formulário HTML que envia para/quote
via POST. Em resposta a um POST,quote
pode renderizar esse segundo template, incorporando a ele um ou mais valores delookup
.
buy
Complete a implementação de buy
de forma que permita que o usuário compre ações.
- Exija que o usuário informe um símbolo de ação, implementado como um campo de texto cujo
name
sejasymbol
. Renderize um pedido de desculpas se a entrada estiver em branco ou o símbolo não existir (de acordo com o valor de retorno delookup
). - Exija que o usuário insira um número de ações, implementado como um campo de texto cujo
name
sejashares
. Renderize um pedido de desculpas se a entrada não for um número inteiro positivo. - Envie a entrada do usuário via
POST
para/buy
. - Após a conclusão, redirecione o usuário para a página inicial.
- Provavelmente, você desejará chamar
lookup
para consultar o preço atual de uma ação. - Provavelmente, você desejará
SELECIONAR
quanto dinheiro o usuário tem atualmente emusers
. - Adicione uma ou mais tabelas novas ao
finance.db
para manter o controle da compra. Armazene informações suficientes para saber quem comprou o quê, a que preço e quando.- Use tipos SQLite apropriados.
- Defina índices
UNIQUE
em quaisquer campos que devem ser exclusivos. - Defina índices (não
UNIQUE
) em quaisquer campos pelos quais você pesquisará (como por meio deSELECT
comWHERE
).
- Renderize um pedido de desculpas, sem concluir a compra, se o usuário não puder comprar o número de ações ao preço atual.
- Você não precisa se preocupar com condições de corrida (ou usar transações).
Depois de implementar buy
corretamente, você poderá ver as compras dos usuários em suas novas tabelas no phpLiteAdmin ou sqlite3
.
index
Complete a implementação de index
de forma que exiba uma tabela HTML resumindo, para o usuário atualmente conectado, quais ações o usuário possui, o número de ações possuídas, o preço atual de cada ação e o valor total de cada participação (ou seja, ações vezes preço). Exiba também o saldo atual de caixa do usuário e o total geral (ou seja, valor total das ações mais o dinheiro).
- Provavelmente, você desejará executar vários
SELECT
s. Dependendo de como você implementar sua(s) tabela(s), você poderá encontrar GROUP BY HAVING SUM e/ou WHERE de seu interesse. - Provavelmente, você desejará chamar
lookup
para cada ação.
sell
Complete a implementação de sell
para que permita que o usuário venda ações de uma ação (que ele possui).
- Requeira que o usuário insira um símbolo de ação, implementado como um menu
select
cujoname
sejasymbol
. Renderize um pedido de desculpas se o usuário não selecionar uma ação ou se (de alguma forma, depois de enviar) o usuário não possuir nenhuma ação daquela ação. - Exija que o usuário insira um número de ações, implementado como um campo de texto cujo
name
sejashares
. Renderize um pedido de desculpas se a entrada não for um número inteiro positivo ou se o usuário não possuir tantas ações da ação. - Envie a entrada do usuário via
POST
para/sell
. - Após a conclusão, redirecione o usuário para a página inicial.
- Você não precisa se preocupar com condições de corrida (ou usar transações).
history
Complete a implementação de history
de forma que exiba uma tabela HTML resumindo todas as transações de um usuário, listando linha por linha cada compra e venda.
- Para cada linha, deixe claro se uma ação foi comprada ou vendida e inclua o símbolo da ação, o preço (de compra ou venda), o número de ações compradas ou vendidas e a data e hora em que a transação ocorreu.
- Talvez seja necessário alterar a tabela criada para
buy
ou complementá-la com uma tabela adicional. Tente minimizar redundâncias.
Toque pessoal
Implemente pelo menos um toque pessoal de sua escolha:
- Permita que os usuários alterem suas senhas.
- Permita que os usuários adicionem dinheiro extra à sua conta.
- Permita que os usuários comprem mais ações ou vendam ações que já possuem no próprio
index
, sem precisar digitar os símbolos das ações manualmente. - Implemente algum outro recurso de escopo comparável.
Passo a passo
Observe que Brian menciona que lookup
retornará o nome da ação. Conforme acima, agora ele retorna apenas o preço e o símbolo.
Testes
Certifique-se de testar seu aplicativo da web manualmente, como
- registrando um novo usuário e verificando se a página do portfólio é carregada com as informações corretas,
- solicitando uma cotação usando um símbolo de ação válido,
- comprando uma ação várias vezes, verificando se o portfólio exibe totais corretos,
- vendendo todas ou algumas ações, verificando o portfólio novamente e
- verificando se a página do histórico mostra todas as transações do usuário conectado.
Também teste alguns usos inesperados, como
- inserindo strings alfabéticas em formulários quando apenas números são esperados,
- inserindo zero ou números negativos em formulários quando apenas números positivos são esperados,
- inserindo valores de pontos flutuantes em formulários quando apenas inteiros são esperados,
- tentando gastar mais dinheiro do que o usuário tem,
- tentando vender mais ações do que o usuário tem,
- inserindo um símbolo de ação inválido e
- incluindo caracteres potencialmente perigosos, como
'
e;
em consultas SQL.
Você também pode verificar a validade do HTML clicando no botão I ♥ VALIDATOR no rodapé de cada uma das suas páginas, que enviará seu HTML para validator.w3.org.
Após a satisfação, para testar seu código com check50
, execute o abaixo.
check50 cs50/problems/2024/x/finance
Esteja ciente de que check50
testará todo o seu programa como um todo. Se você o executar antes de concluir todas as funções necessárias, ele poderá relatar erros em funções que são realmente corretas, mas dependem de outras funções.
Estilo
style50 app.py
Solução do corpo docente
Você é bem-vindo para estilizar seu próprio aplicativo de forma diferente, mas é assim que a solução do corpo docente se parece!
Sinta-se à vontade para se registrar para obter uma conta e brincar com o app. Não use uma senha que você usa em outros sites.
É razoável olhar para o HTML e CSS do corpo docente.
Dicas
- Para formatar um valor como um valor em dólar dos EUA (com centavos listados em duas casas decimais), você pode usar o filtro
usd
em seus modelos Jinja (imprimindo valores como{{ value | usd }}
em vez de{{ value }}
. - Dentro de
cs50.SQL
há um métodoexecute
cujo primeiro argumento deve ser umastr
de SQL. Se essastr
contiver parâmetros de ponto de interrogação aos quais valores devem ser vinculados, esses valores podem ser fornecidos como parâmetros nomeados adicionais paraexecute
. Veja a implementação delogin
para um exemplo. O valor de retorno deexecute
é o seguinte:- Se
str
for umSELECT
, entãoexecute
retornará umalist
de zero ou mais objetosdict
, dentro dos quais estão chaves e valores representando campos de uma tabela e células, respectivamente. - Se
str
for umINSERT
, e a tabela na qual os dados foram inseridos contiver umaPRIMARY KEY
de autoincremento, entãoexecute
retornará o valor da chave primária da linha recém-inserida. - Se
str
for umDELETE
ou umUPDATE
, entãoexecute
retornará o número de linhas excluídas ou atualizadas porstr
.
- Se
- Lembre-se que
cs50.SQL
registrará em sua janela de terminal todas as consultas que você executar viaexecute
(para que você possa confirmar se elas estão conforme o esperado). - Certifique-se de usar parâmetros vinculados a ponto de interrogação (ou seja, um paramstyle de
named
) ao chamar o métodoexecute
do CS50, à laWHERE ?
. Não use f-strings,format
ou+
(ou seja, concatenação), para que você não corra o risco de um ataque de injeção de SQL. - Se (e somente se) já estiver confortável com SQL, você pode usar SQLAlchemy Core ou Flask-SQLAlchemy (ou seja, SQLAlchemy ORM) em vez de
cs50.SQL
. - Você pode adicionar arquivos estáticos adicionais em
static/
. - Provavelmente você desejará consultar a documentação do Jinja ao implementar seus modelos.
- É razoável pedir a outras pessoas para testar (e tentar acionar erros em) seu site.
- Você pode alterar a estética dos sites, como via
- Você pode achar a documentação do Flask e a documentação do Jinja úteis!
FAQs
ImportError: No module named ‘application’
Por padrão, flask
procura um arquivo chamado app.py
em seu diretório de trabalho atual (porque configuramos o valor de FLASK_APP
, uma variável de ambiente, para ser app.py
). Se ver este erro, provavelmente você executou flask
no diretório errado!
OSError: [Errno 98] Address already in use
Se, ao executar flask
, você vir este erro, provavelmente (ainda) tem flask
sendo executado em outra aba. Certifique-se de encerrar aquele outro processo, como com ctrl-c, antes de iniciar flask
novamente. Se você não tiver nenhuma outra aba, execute fuser -k 8080/tcp
para encerrar quaisquer processos que (ainda) estejam escutando na porta TCP 8080.
Como enviar
Em seu terminal, execute o abaixo para enviar seu trabalho.
submit50 cs50/problems/2024/x/finance
Por que minha submissão passa pelo check50, mas mostra "Sem resultados" em meu Gradebook após executar submit50?
Em alguns casos, submit50
pode não avaliar a atribuição devido a (1) formatação inconsistente em seu arquivo app.py
e/ou (2) arquivos adicionais desnecessários sendo enviados com o conjunto de problemas. Para corrigir esses problemas, execute black app.py
na pasta finance
. Resolva quaisquer problemas que sejam revelados. Em seguida, examine o conteúdo de sua pasta finance
. Exclua arquivos estranhos, como sessões do flask ou outros arquivos que não fazem parte de sua implementação do conjunto de problemas. Além disso, execute check50
novamente para garantir que sua submissão ainda funcione. Por fim, execute o comando submit50
acima novamente. Seu resultado aparecerá em seu Gradebook em alguns minutos.
Observe que se houver uma pontuação numérica ao lado de sua submissão de finanças na área submissions
do seu Gradebook, o procedimento discutido acima não se aplica a você. Provavelmente, você não atendeu totalmente aos requisitos do conjunto de problemas e deve confiar no check50
para obter pistas sobre que trabalho resta.