diff --git a/compilar.bat b/compilar.bat new file mode 100644 index 0000000..92f5377 --- /dev/null +++ b/compilar.bat @@ -0,0 +1,2 @@ +python setup.py bdist_msi +pause \ No newline at end of file diff --git a/fetcher.py b/fetcher.py new file mode 100644 index 0000000..18aae60 --- /dev/null +++ b/fetcher.py @@ -0,0 +1,27 @@ +import json +from urllib import request, error + +quizes_url = "https://roinujnosde.pythonanywhere.com/quizes/" +detalhe_quiz_url = "https://roinujnosde.pythonanywhere.com/quizes/%d/" + +def pegar_dados_do_quiz(id): + """Acessa a URL externa, baixa informações sobre o Quiz com o ID informado e as retorna""" + url = detalhe_quiz_url % int(id) + try: + with request.urlopen(url) as response: + data = json.loads(response.read()) + except(error.URLError): + return None + + #Remove as não aprovadas da lista + data['pergunta_set'][:] = [pergunta for pergunta in data['pergunta_set'] if pergunta['aprovada']] + return data + +def pegar_list_de_quizes(tree): + """Acessa a URL externa, baixa informações sobre os quizes e os retorna""" + try: + with request.urlopen(quizes_url) as response: + data = json.loads(response.read()) + return data + except(error.URLError): + return None diff --git a/icon.ico b/icon.ico new file mode 100644 index 0000000..2425394 Binary files /dev/null and b/icon.ico differ diff --git a/interfaces.py b/interfaces.py new file mode 100644 index 0000000..88f6071 --- /dev/null +++ b/interfaces.py @@ -0,0 +1,285 @@ +from tkinter import * +from tkinter import font, ttk, messagebox +import fetcher +from enum import Enum +import webbrowser +from datetime import datetime + +TAMANHO_PEQUENO = 8 +TAMANHO_MEDIO = 10 +TAMANHO_GRANDE = 13 + +class TelaInicial: + """Classe que representa a tela principal do programa""" + def __init__(self, master=None): + self.janela_quiz = None + self.cache = None + master.title('PflowQuiz') + master.iconbitmap(default='icon.ico') + global TAMANHO_MEDIO + self.fonte_media = font.Font(size=TAMANHO_MEDIO) + + self.container1 = Frame(master) + self.container1.pack(pady=5) + + self.container2 = Frame(master) + self.container2['pady'] = 5 + self.container2.pack() + + self.container3 = Frame(master) + self.container3['pady'] = 10 + self.container3.pack() + + self.container4 = Frame(master) + self.container4.pack(side=RIGHT, padx=(0, 10)) + + self.label = Label(text='Escolha o quiz para jogar:', master=self.container1) + self.label.config(font=self.fonte_media) + self.label.pack() + + self.quiz_tree = ttk.Treeview(show=['headings'], master=self.container2, selectmode='browse', columns=('nome', 'autor', 'data', 'perguntas')) + self.quiz_tree.heading('nome', text='Nome', command= lambda: self.__ordenar('nome', False)) + self.quiz_tree.column('nome', width=150, minwidth=150, anchor='center') + self.quiz_tree.heading('autor', text='Autor', command= lambda: self.__ordenar('autor', False)) + self.quiz_tree.column('autor', width=120, minwidth=120, anchor='center') + self.quiz_tree.heading('data', text='Data de criação', command= lambda: self.__ordenar('data', False)) + self.quiz_tree.column('data', width=120, minwidth=120, anchor='center') + self.quiz_tree.heading('perguntas', text='Perguntas', command= lambda: self.__ordenar('perguntas', True)) + self.quiz_tree.column('perguntas', width=120, minwidth=70, anchor='center') + self.quiz_tree.pack(padx=20) + + self.filtrar_label = Label(text='Filtrar: ', master=self.container3) + self.filtrar_label.pack(side=LEFT) + self.filtrar_entry = Entry(self.container3) + self.filtrar_entry.bind('', lambda key: self.__atualizar_tabela(key, False, True)) + self.filtrar_entry.pack(side=LEFT, padx=(0, 90)) + self.atualizar_button = Button(self.container3, text='Atualizar lista', command=self.__atualizar_tabela) + self.atualizar_button.pack(side=LEFT, padx=15) + self.jogar_button = Button(self.container3, text='Jogar', command=self.__iniciar_quiz) + self.jogar_button.pack(side=RIGHT, padx=15) + + self.info_contato_label = Label(self.container4, text='Contato: pflowdev@outlook.com') + self.info_contato_label.bind('', self.__abrir_url_contato) + self.info_contato_label.pack() + + self.__atualizar_tabela() + + def __ordenar(self, coluna, reverso): + """Ordena os itens da tabela alfabeticamente""" + tv = self.quiz_tree + itens = [(tv.set(item, coluna), item) for item in tv.get_children()] + + itens.sort(reverse=reverso) + + for i in range(0, len(itens)): + tv.move(itens[i][1], '', i) + + tv.heading(coluna, command= lambda: self.__ordenar(coluna, not reverso)) + + def __abrir_url_contato(self, evento): + """Abre a URL de contato""" + webbrowser.open('mailto:pflowdev@outlook.com') + + def __iniciar_quiz(self): + """Inicia o Quiz selecionado""" + #Se a janela ainda não existe + if not self.janela_quiz or not self.janela_quiz.winfo_exists(): + #Pega o ID do Quiz selecionado + selecionados = self.quiz_tree.selection() + if len(selecionados) == 0: + messagebox.showerror(title="Erro", message="Selecione um Quiz para poder continuar!") + return + id_selecionado = selecionados[0] + #Pega os dados do Quiz no webapp + quiz = fetcher.pegar_dados_do_quiz(id_selecionado) + if quiz == None: + messagebox.showerror(title="Erro", message="Não foi possível baixar o quiz. \nVerifique sua conexão com a internet e tente novamente!") + return + #Gera a Janela de jogo com esse Quiz + self.janela_quiz = Jogo(quiz, self) + else: + self.janela_quiz.deiconify() + + def __limpar_tabela(self): + """Limpa a Treeview contendo os dados dos quizes""" + self.quiz_tree.delete(*self.quiz_tree.get_children()) + + def __atualizar_tabela(self, key=None, limpar_filtro=True, usar_cache=False): + """Limpa e preenche a Treeview com dados atualizados""" + self.__limpar_tabela() + if limpar_filtro: + self.filtrar_entry.delete(0, 'end') + + if not usar_cache or self.cache == None: + self.cache = fetcher.pegar_list_de_quizes(self.quiz_tree) + + if self.cache == None: + messagebox.showerror(title="Erro", message="Não foi possível baixar a lista de quizes. \nVerifique sua conexão com a internet e tente novamente!") + return + + for quiz in self.cache: + texto = self.filtrar_entry.get().lower() + if key != None and texto != '': + if texto not in quiz['nome'].lower() and texto not in quiz['autor'].lower(): + continue + + if not quiz['aprovado']: + continue + if quiz['quantidade_perguntas_aprovadas'] == 0: + continue + + self.quiz_tree.insert('', 'end', quiz['id'], values=[quiz['nome'], quiz['autor'], quiz['data_criacao'], + quiz['quantidade_perguntas_aprovadas']]) + self.__ordenar('data', False) + + + +class Jogo(Toplevel): + """Classe que representa a tela de jogo""" + class Direcao(enum.Enum): + ANTERIOR = 1 + PROXIMO = 2 + + def __init__(self, quiz, tela_inicial): + super().__init__() + self.tela_inicial = tela_inicial + self.quiz = quiz + self.quantidade_perguntas = len(self.quiz['pergunta_set']) + self.pergunta_atual = 0 + self.escolhas_salvas = {} + + self.title(quiz['nome']) + #self.attributes('-topmost', 'true') + + self.container1 = Frame(self) + self.container1.pack(anchor=W, padx=80) + + self.container2 = Frame(self) + self.container2.pack(anchor=W, padx=80) + + self.container3 = Frame(self) + self.container3.pack() + + self.titulo_pergunta = Label(self.container1, width=35, wraplength=250) + self.titulo_pergunta.pack(pady=10, anchor=W) + + self.resposta_selecionada = IntVar(value=0) + self.resposta1_radio = Radiobutton(self.container2, variable=self.resposta_selecionada, value=0) + self.resposta1_radio.pack(anchor=W) + self.resposta2_radio = Radiobutton(self.container2, variable=self.resposta_selecionada, value=1) + self.resposta2_radio.pack(anchor=W) + self.resposta3_radio = Radiobutton(self.container2, variable=self.resposta_selecionada, value=2) + self.resposta3_radio.pack(anchor=W) + self.resposta4_radio = Radiobutton(self.container2, variable=self.resposta_selecionada, value=3) + self.resposta4_radio.pack(anchor=W) + + self.anterior_button = Button(self.container3, text="Anterior", command= lambda: self.trocar_pergunta(Jogo.Direcao.ANTERIOR)) + self.anterior_button.pack(side=LEFT, padx=30) + self.proximo_button = Button(self.container3, command= lambda: self.trocar_pergunta(Jogo.Direcao.PROXIMO)) + self.proximo_button.pack(side=RIGHT, padx= 30, pady=15) + + self.__atualizar_botoes() + self.__atualizar_pergunta() + self.__atualizar_respostas() + + self.focus() + + def __atualizar_botoes(self): + """Ativa ou desativa o botão Anterior ou muda o nome do botão de avançar entre Finalizar/Próximo, de acordo com o index da pergunta atual""" + if self.pergunta_atual == 0: + state = 'disabled' + else: + state = 'normal' + self.anterior_button['state'] = state + + if self.pergunta_atual == (self.quantidade_perguntas - 1): + texto = "Finalizar" + else: + texto = "Próximo" + self.proximo_button.config(text=texto) + + def __atualizar_pergunta(self): + """Atualiza o texto da Label de pergunta de acordo com a atual""" + titulo = str(self.pergunta_atual + 1) + ". " + self.quiz['pergunta_set'][self.pergunta_atual]['titulo_pergunta'] + self.titulo_pergunta.config(text=titulo) + + def __atualizar_respostas(self): + """Atualiza os textos dos campos de resposta de acordo com a pergunta atual""" + self.resposta_selecionada.set(self.escolhas_salvas.get(self.pergunta_atual, 0)) + self.resposta1_radio.config(text="A) " + self.quiz['pergunta_set'][self.pergunta_atual]['resposta_set'][0]['texto_resposta']) + self.resposta2_radio.config(text="B) " + self.quiz['pergunta_set'][self.pergunta_atual]['resposta_set'][1]['texto_resposta']) + self.resposta3_radio.config(text="C) " + self.quiz['pergunta_set'][self.pergunta_atual]['resposta_set'][2]['texto_resposta']) + self.resposta4_radio.config(text="D) " + self.quiz['pergunta_set'][self.pergunta_atual]['resposta_set'][3]['texto_resposta']) + + def __salvar_escolha(self): + """Salva a resposta escolhida pelo usuário""" + self.escolhas_salvas[self.pergunta_atual] = self.resposta_selecionada.get() + + def trocar_pergunta(self, direcao): + """Troca para a pergunta anterior, para a próxima de acordo com o parâmetro 'direcao' ou finaliza o jogo caso esteja na última pergunta""" + self.__salvar_escolha() + if direcao == Jogo.Direcao.PROXIMO: + if self.pergunta_atual == (self.quantidade_perguntas - 1): + self.tela_inicial.janela_quiz = FimDeJogo(self.quiz['nome'], self.get_pontuacao()) + self.destroy() + return + self.pergunta_atual += 1 + else: + self.pergunta_atual -= 1 + self.__atualizar_botoes() + self.__atualizar_pergunta() + self.__atualizar_respostas() + + def get_pontuacao(self): + corretas = {} + index_pergunta = 0 + for pergunta in self.quiz['pergunta_set']: + index_resposta = 0 + for resposta in pergunta['resposta_set']: + if resposta['correta']: + corretas[index_pergunta] = index_resposta + index_resposta += 1 + index_pergunta += 1 + + acertos = 0 + for pergunta in corretas.keys(): + if corretas[pergunta] == self.escolhas_salvas[pergunta]: + acertos += 1 + + pontuacao = (acertos / len(corretas)) * 100 + return "%d%%" % pontuacao + +class FimDeJogo(Toplevel): + """Classe que representa a tela de fim de jogo""" + def __init__(self, nome_quiz, pontuacao): + super().__init__() + self.title('Fim de jogo') + global TAMANHO_GRANDE, TAMANHO_MEDIO, TAMANHO_PEQUENO + self.fonte_grande = font.Font(size=TAMANHO_GRANDE) + self.fonte_media = font.Font(size=TAMANHO_MEDIO) + self.fonte_pequena = font.Font(size=TAMANHO_PEQUENO) + + self.container1 = Frame(self) + self.container1.pack() + + self.container2 = Frame(self) + self.container2.pack() + + self.mensagem_de_fim = Label(self.container1, font=self.fonte_grande, text="Você finalizou o quiz %s!" % nome_quiz) + self.mensagem_de_fim.pack(pady=15, padx=20) + + self.pontuacao_titulo = Label(self.container2, font=self.fonte_media, text="Sua pontuação:") + self.pontuacao_titulo.pack() + + self.pontuacao_label = Label(self.container2, font=self.fonte_media, text=pontuacao) + self.pontuacao_label.pack(pady=(15, 25)) + + self.mensagem_adicionar_quiz = Label(self.container2, font=self.fonte_pequena, wraplength=220, text="Que tal adicionar o seu próprio quiz ao jogo? Clique aqui e faça seu cadastro!") + self.mensagem_adicionar_quiz.bind("", self.__abrir_pagina_cadastro) + self.mensagem_adicionar_quiz.pack(pady=(0, 5)) + + self.focus() + + def __abrir_pagina_cadastro(self, evento): + """Abre a URL da página de cadastro""" + webbrowser.open('http://roinujnosde.pythonanywhere.com/register') diff --git a/pflow_quiz.py b/pflow_quiz.py new file mode 100644 index 0000000..11825fc --- /dev/null +++ b/pflow_quiz.py @@ -0,0 +1,10 @@ +from tkinter import Tk +from interfaces import TelaInicial + +def main(): + root = Tk() + inicio = TelaInicial(root) + root.mainloop() + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..8eac34f --- /dev/null +++ b/setup.py @@ -0,0 +1,66 @@ +import sys +from cx_Freeze import setup, Executable + + +# GUI applications require a different base on Windows (the default is for a +# console application). +base = None +if sys.platform == "win32": + base = "Win32GUI" + +build_exe_options = {'include_files':[("icon.ico", "icon.ico"),]} + +shortcut_table = [ + ("DesktopShortcut", # Shortcut + "DesktopFolder", # Directory_ + "PflowQuiz", # Name + "TARGETDIR", # Component_ + "[TARGETDIR]pflow_quiz.exe",# Target + None, # Arguments + None, # Description + None, # Hotkey + None, # Icon + 0, # IconIndex + None, # ShowCmd + 'TARGETDIR' # WkDir + ), + ("StartMenuShortcut", # Shortcut + "StartMenuFolder", # Directory_ + "PflowQuiz", # Name + "TARGETDIR", # Component_ + "[TARGETDIR]pflow_quiz.exe",# Target + None, # Arguments + None, # Description + None, # Hotkey + None, # Icon + 0, # IconIndex + None, # ShowCmd + 'TARGETDIR' # WkDir + ) + ] + +# Now create the table dictionary +msi_data = {"Shortcut": shortcut_table} + +# Change some default MSI options and specify the use of the above defined tables +bdist_msi_options = {'data': msi_data, 'install_icon': 'icon.ico'} + +setup( name = "PflowQuiz", + version = "1.0", + author = "Edson Passos", + options = { + "build_exe": { + "include_files": ["icon.ico",] + }, + "bdist_msi": bdist_msi_options, + }, + executables = [ + Executable( + "pflow_quiz.py", + base=base, + shortcutName="PflowQuiz", + shortcutDir="DesktopFolder", + icon="icon.ico", + ) + ] + )