Finished Version 3

This commit is contained in:
Vivek Santayana 2021-11-01 13:58:15 +00:00
parent 774f279315
commit 30156205ca
32 changed files with 3928 additions and 2 deletions

2
.gitignore vendored
View File

@ -138,3 +138,5 @@ dmypy.json
# Cython debug symbols # Cython debug symbols
cython_debug/ cython_debug/
# Secret File for Keys
secret.py

5
Dockerfile Normal file
View File

@ -0,0 +1,5 @@
FROM python:3.10-alpine
WORKDIR /app
COPY . .
RUN pip install -r requirements.txt
CMD ["python","app.py"]

View File

@ -1,3 +1,81 @@
# masks-personality-quiz # Which *Masks* Playbook Are You? — a Personality Quiz
A personality quiz to determine which Masks playbook you should play. ## About the Project
I started work on the first version of this personality quiz over a year ago, when I decided to teach myself programming.
The original version ran on JavaScript and was rendered and evaluated entirely by the browser.
This version runs entirely on the server, and the quiz results are evaluated by the server rather than the client.
The back-end runs on Python 3.10, using the Flask framework.
The web pages are templated using Jinja.
The web site is rendered using html and the Bootstrap CSS framework.
There have been some elements of the interface enhanced using rudimentary JavaScript, including the jQuery library.
Of all of these programming languages, Python has proved to be a far more intuitive language to learn, and generally programming this has been a lot more enjoyable and a lot less frustrating than the earlier iteration that used JavaScript.
The quiz runs by rendering a form with all of the questions and options on the browser.
The browser then submits the responses via a POST request to the server.
The server evaluates the responses and renders the results accordingly.
There is no information stored on the client.
All data transacted between the client and the server is stored as a Flask session for the duration that the user is on the web site.
If the user closes the site, the session data is deleted and all answers submitted are lost.
There are, of course, some considerable inefficiencies in the way I have set up the templating.
I have written the text for all the various web pages in the templates of the respective pages.
Ideally, I would have preferred having a single template for all of the web pages or views, and then have the content served on those pages render dynamically based on the URL query.
But that was a level of programming that was gratuitous at this point, as really most of the pages were serving static content anyway so it did not matter just now.
Moreover, the algorithm with which the quiz calculates Labels is highly inaccurate.
It is difficult to find an algorithm that simulates Label shifts like during play because of how many variables and conditions there are to what constitutes a valid Label shift, and the fact that which Labels are being shifted might be complicated by the existence of custom Labels like ‘Soldier’.
So the Labels that the quiz returns are highly inaccurate, often not adding up to the right numbers.
In addition, this is the first time I am storing the code of the quiz on my git repo, and having a much more streamlined version control process.
I have also Dockerised the app so it can be deployed seamlessly.
## Set Up
To run an instance of this app, you will need Docker and Docker Compose installed.
To set up an instance, you will need to clone the repository,
You will also need to create a module called `secret.py` and enter a secret key for Flask to use to encrypt session data.
The easiest way to do this is to make a copy of `secret.py.example`, remove `.example` from the filename, and then add a random string as a value for the variable defined therein.
After you have created `secret.py`, run `sudo docker-compose up -d` from the root folder.
The current set-up in the `docker-compose.yml` does not expose the container to the internet, but exposes it on an internal Docker network.
This can be changed by amending the `docker-compose.yml` entry on `line 8` to:
```yml
- 5000:5000
```
## Version History
### Current Version: 3.0.0 (1 November 2021)
### Changelog
- Re-built the quiz to run on the server side rather than on the client.
- Built using Python, Flask, Jinja, and html primarily.
- Changed the layout to a cleaner, more accessible and modern style using the Bootstrap CSS framework,
- Expanded to 30 questions.
- Refined the algorithm by which the Joined duplicates another playbook based on the second-highest scoring match rather than selecting a playbook entirely at random.
- Dockerised the quiz and hosting it on my web server, serving it via the front-end reverse proxy.
### Past Versions
Because I only started using Git relatively recently, I have not uploaded the code from the older versions onto the repo.
The repo starts with version 3.
#### Version 2 (2 May 2020)
- First added the functionality to determine character Labels.
- Re-wrote many of the questions, expanded the quiz to 25 questions.
- Added extensible databases to allow for more questions to be added dynamically.
- Added the functionality to filter Playbooks based on source books.
- Added a glossary to explain game terms.
#### Version 1 (20 April 2020)
- First made the quiz to run entirely in the browser using JavaScript.
- Rudimentary quiz that returned the playbook most suited.

5
app.py Normal file
View File

@ -0,0 +1,5 @@
from interface import create_app
app = create_app()
if __name__ == '__main__': app.run(host='0.0.0.0')

0
data/__init__.py Normal file
View File

23
data/labels.py Normal file
View File

@ -0,0 +1,23 @@
labels = {
'danger': {
'flavour': 'seeing yourself as threatening, strong, bloody-knuckled, and risky. Other people see you as a danger when they think they should steer clear of you because you might bring them harm. You see yourself as a danger when you believe you can take down other dangerous threats, and when you think you yourself are a threat to other people.'
},
'freak': {
'flavour': 'seeing yourself as strange, unusual, unique, and powerful. Other people see you as a freak when they think youre odd, unlike them, something unnatural or outside of their understanding. You see yourself as a freak when you accept and own the things you can do that no one else can, and when you think you dont belong with the people and the world around you.'
},
'saviour': {
'flavour': 'seeing yourself as defending, guarding, protecting, and stalwart. Other people see you as a savior when they think of you as noble or selfsacrificing, or a bit overbearing and moralizing. You see yourself as a savior when you think of yourself as a martyr, someone who gladly sacrifices to protect and defend others.'
},
'superior': {
'flavour': 'seeing yourself as smart, capable, crafty, and quick. Other people see you as superior when they think youre the smartest person in the room, an arrogant and egotistical jerk. You see yourself as superior when you think youre cleverer than everyone else, and when you know exactly what to say to make the people around you do what you want.'
},
'mundane': {
'flavour': 'seeing yourself as normal, human, empathetic, and understanding. Other people see you as mundane when they think of you as all too normal and uninteresting, but also comprehending and sympathetic. You see yourself as mundane when you think youre regular, just a person, not special, and focused on normal human things like feelings and emotions.'
},
'soldier': {
'flavour': 'You work for a metahuman law enforcement agency (A.E.G.I.S.) that keeps the world safe from all manner of superhuman, supernatural, and extraterrestrial threats. You volunteered to work with a team of young superheroes as part of a new A.E.G.I.S. program designed to keep Halcyon City safe.',
'custom': True,
'default_value': 2,
'playbooks': ['soldier']
}
}

248
data/playbooks.py Normal file
View File

@ -0,0 +1,248 @@
playbooks = {
# Core Playbooks
'beacon': {
'source': 'core',
'flavour': 'You don’t have to do this. You could probably have a safe, decent, simple life. It’d be nice, but... come on. Superpowers! Aliens! Wizards! Time travel! You’re out of your depth but who cares? This is awesome. Everybody should try it.',
'moment': 'This is the moment when you show them exactly why you belong here. You do any one thing, take out any one enemy, no matter how insane, no matter how ridiculous, because thats you. Their jaws are gonna drop when youre done. Of course, pulling off a stunt like this tends to bring unwanted attention and a dangerous reputation…',
'labels': {
'danger': -1,
'freak': -1,
'saviour': 2,
'superior': 0,
'mundane': 2
}
},
'bull': {
'source': 'core',
'flavour': 'You’re big, strong, and tough. You know what fighting really is, and you’re good at it. Sure, you’ve got a soft side, too. But you only show that to the people you care about the most. Everybody else? They can eat your fist.',
'moment': 'This is what you do best. You let loose, all the pent up strength and rage and glee, and you break whatever stands in your way. You are a walking demolition crew. What can stand up to you? Nothing. Not buildings. Not structures. Not enemies. Nothing. Of course, now the people who changed you know exactly where to find you…',
'labels': {
'danger': 2,
'freak': 1,
'saviour': -1,
'superior': 1,
'mundane': -1
}
},
'delinquent': {
'source': 'core',
'flavour': 'You’ve got these cool powers. But everyone keeps telling you how to use ’em. You know what they need? Someone to give them trouble, to make sure they don’t always get their way. And hey! You’re the perfect hero to do it.',
'moment': 'moment of truthThis is when you show them what you really are. Whether youre the hero underneath the rebel facade… or the one who can make the hard choices heroes cant make. You do whatever it takes to show that truth, whether its saving the day from a terrible villain or stopping a bad guy once and for all. Of course, once youve shown what you really are, theres no going back to playing the clown…',
'labels': {
'danger': 0,
'freak': 0,
'saviour': -1,
'superior': 2,
'mundane': 1
}
},
'doomed': {
'source': 'core',
'flavour': 'Something about your powers dooms you. It’s just a matter of time before your doom comes for you. Until then, though... you’ve got a nemesis who needs fighting and a world that needs saving. After all, it’s better to burn out than fade away.',
'moment': 'The prickly tingling fear of your doom, always in your head—it holds you back most of the time. But right here, right now? It gives you the confidence to do anything. After all, whats the worst that could happen? Is it worse than your doom? Do impossible things. Do anything. But mark a doomsign after youre finished.',
'labels': {
'danger': 1,
'freak': 1,
'saviour': 1,
'superior': -1,
'mundane': 0
}
},
'janus': {
'source': 'core',
'flavour': 'Wake up. Breakfast. School. Work. Homework. Sleep. Repeat. It burns you up, being stuck in this life, unable to make a real difference. That is... until you put on the mask. And then, you can be someone else: a hero.',
'moment': 'The mask is a lie, and some piece of you has always known that. Doesnt matter if others can see it. Youre the one that can do the impossible. Mask off. Costume on. And youre going to save the damn day. Of course, you better hope nobody nasty is watching…',
'labels': {
'danger': 0,
'freak': -1,
'saviour': 0,
'superior': 0,
'mundane': 3
}
},
'legacy': {
'source': 'core',
'flavour': 'You’re the latest in a storied heroic lineage, a family that shares a name and a cause. Now, everybody is watching and waiting to see if you’ve got what it takes to uphold that tradition. No pressure, right?',
'moment': 'This is the moment when you prove how much the mantle belongs to you. You seize control of all your powers, and you defeat even impossible odds to prove you are worthy of the name you carry. You accomplish feats even your predecessors couldnt do. Of course, after you prove something like that, you can expect still more responsibilities to be placed on your shoulders…',
'labels': {
'danger': -1,
'freak': 0,
'saviour': 2,
'superior': 0,
'mundane': 1
}
},
'nova': {
'source': 'core',
'flavour': 'You’re a font of power. Channel it, and you can remake the world into exactly what you want. Unleash it, and you can do miracles. It’s wonderful... and terrifying. Lose control for even a second, and other people get hurt.',
'moment': 'Your minds eye opens, and you can see the world around you like never before. You can control it, at will, with ease. Of course, warping reality tends to have ramifications down the line, but in your moment of godhood… how could you possibly be worried?',
'labels': {
'danger': 1,
'freak': 2,
'saviour': 0,
'superior': 0,
'mundane': -1
}
},
'outsider': {
'source': 'core',
'flavour': 'You’re not from here. Your home is an amazing place, full of beauty and wonder. But there’s something to this place, something special that you’re missing back home. Something... human. So yeah, you’ll be hanging around. At least for now.',
'moment': 'You embrace your home and call them for aid. They will answer your call—in force!—arriving exactly when you need them to turn the tide. They fight and serve you for the rest of the battle. Of course, when all is said and done… theyd probably like to take you home with them. You did, after all, just prove yourself worthy.',
'labels': {
'danger': -1,
'freak': 1,
'saviour': 0,
'superior': 2,
'mundane': 0
}
},
'protege': {
'source': 'core',
'flavour': 'You proved yourself to an experienced hero. They think you’ve got what it takes. They’ve been training you for a while, and now you have to decide... do you want to be them? Or will you find your own path?',
'moment': 'The moment that you show who you really are: your mentor, or something different. You can do whatever your mentor could do and more. You can do the incredible, even the things they always failed to accomplish. Of course, theyre not going to see you the same way, no matter which path you choose…',
'labels': {
'danger': -1,
'freak': 0,
'saviour': 1,
'superior': 2,
'mundane': 0
}
},
'transformed': {
'source': 'core',
'flavour': 'You can recall a time not too long ago when you looked... normal. When you didn’t feel their stares. When you didn’t hear their gasps. When no one thought of you as a monster. Those were the days, huh.',
'moment': 'Its so easy to forget that youre not your body, and youre not the voice in your head—youre both. Be the monster, and save them anyway. Smash down walls, and speak softly. Because when you embrace it, you can do anything. Of course, putting on a display like this is sure to rile up those who see only the monster when they look at you…',
'labels': {
'danger': 1,
'freak': 3,
'saviour': 0,
'superior': -1,
'mundane': -1
}
},
# Limited Edition Playbooks
# Halcyon City Herald Collection Supplement
'innocent': {
'source': 'halcyon',
'flavour': 'Time travel is great! Or so you thought, until you landed in a strange new world with a dark, broken, damaged, dangerous, adult version of yourself. Not what you had wanted to become. Question is, what are you going to do about it?',
'moment': 'Youve fought, struggled, and worked so hard to figure out who you are, whether youre just the same as your future self or whether youre different… but right now, thats all out the window. The distinction between your future self and your present self vanishes in the face of the trial before you, and you become exactly the powerful, adamant figure that everyone fears or hopes you will one day become. You can do exactly what your future self could do, and everyone around you sees them in you more clearly than ever. Of course, after this its going to be hard to treat you as two different people…',
'labels': {
'danger': 0,
'freak': 0,
'saviour': 1,
'superior': -1,
'mundane': 2
}
},
'joined': {
'source': 'halcyon',
'flavour': 'You’d be nothing without them—your partner, your sibling, your friend, your rival, your other half. You’re tied to their powers and to them, through and through. The rest of the world only ever sees you two as halves of a whole—not as two separate people. And the two of you aren’t sure if they’re right.',
'moment': 'Youre on your own. Its like missing an arm. Like fighting naked. Like holding your breath. Youre missing something vital… but youre moving faster than ever, thinking faster than ever, doing things you couldnt even do while relying on both of your strength combined. And its hitting you, hard—you can do this. Without them. And you can win. Its going to be hard to come down off this high and rejoin with them afterwards, isnt it?',
'labels': {
'danger': 0,
'freak': 0,
'saviour': 0,
'superior': 0,
'mundane': 0
}
},
'newborn': {
'source': 'halcyon',
'flavour': 'You’re a brand new being, created through scientific inquiry, feat of engineering, or random chance. This world is all new to you, full of wonder and adventure. It’s not easy, though—everyone has an opinion about who you are and what you should do. It’s time to find out for yourself who you really are.',
'moment': 'Something snaps into focus, and suddenly youre a full thing, true and complete. Youd never have known how fragmented you were before, if not for here, this moment. Youre not a series of individual lessons. Youre not a series of subroutines and programs. Youre… a person. This must be what its like to be… human. And this fullness? It gives you a control over yourself, a unity of purpose youve never experienced before. Of course, now that youre showing off all your potential, its only a matter of time before someone comes forward to reduce you to a machine again…',
'labels': {
'danger': 1,
'freak': 2,
'saviour': 0,
'superior': 1,
'mundane': -2
}
},
'reformed': {
'source': 'halcyon',
'flavour': 'Villainy used to be a way of life for you. Then you saw just what your selfishness and hate created. The supervillain life is hard to quit. But you know this best: sometimes the villain needs saving too.',
'moment': 'Youve seen your greatest mistakes, and the rest of the world has, too. Theyre all watching you now, judging every move you make. When everything is on the line and your back is against the wall, though, youll show them what youre made of— that being a hero is a choice. An act of will. And youve got what it takes to save the day. Of course, afterward, you can expect both sides, hero and villain, to deeply question where your loyalties truly lie…',
'labels': {
'danger': 2,
'freak': 1,
'saviour': -1,
'superior': 0,
'mundane': 0
}
},
'star': {
'source': 'halcyon',
'flavour': 'Being a hero isn’t just about doing right. It’s about being seen doing right. Let them think you’re shallow for loving the spotlight and the cameras, for making speeches, for smiling so much. You’ll be a hero in all the ways that matter.',
'moment': 'Sometimes it can be hard to tell where the show stops and where you begin—but not today. Not now. Because right now, there is no show. Right now, you are the thing you pretend to be—bold and bright and beautiful and amazing and powerful and confident. Right now, you draw strength from your audience, comfort from their belief in you, and you can do anything they think you can. Of course, after such an impassioned performance, your audience will just have even more demands…',
'labels': {
'danger': -1,
'freak': 1,
'saviour': 1,
'superior': 2,
'mundane': -1
}
},
# Secrets of A.E.G.I.S. Supplement
'brain': {
'source': 'aegis',
'flavour': 'You’ve always been the smartest kid in the room. Your inventions are world-class, your tactical plans are flawless, and your mind is a steel-trap memory palace of extraordinary ideas. If only the others knew how sometimes, none of that seems to matter. None of that keeps the shadows at bay. None of that can make up for what you did... or might do.',
'moment': 'Sooner or later, all the super powers, elite training, and experience are helpless in the face of evil or disaster. Thats when somebody like you, gifted as you are with a peerless intellect, can rise to the occasion. Your plan, your invention, or your lightning-fast thought processes save the day, in a way no one else could have foreseen. Of course, after youve shown how different you are from them, that distance between you and the others is now that much greater. And the world is only going to pull you farther apart…',
'labels': {
'danger': 0,
'freak': 0,
'saviour': 1,
'superior': 2,
'mundane': -1
}
},
'soldier': {
'source': 'aegis',
'flavour': 'You’re an agent of something greater than you—a real force fighting to make the world a better place. Through them, you stand for something important. You just hope that, when push comes to shove, you stand for the right thing.',
'moment': 'Freedom isnt free. But not every mission ends in tragedy. When things look bleakest, when your back is against the wall, when it seems like the dawn will never come… you find a way forward without violence. Your enemies lay down their arms and surrender; your allies step back from the brink of chaos. Of course, the people youve saved arent going to forget what youve done here today; they may even come to see you as a symbol of the higher cause you claim to serve…',
'labels': {
'danger': -1,
'freak': 0,
'saviour': 2,
'superior': 1,
'mundane': 0,
'soldier': 2
}
},
# Unbound Supplement
'harbinger': {
'source': 'unbound',
'flavour': 'You&rsquo;re from the future, and you know how things turn out. You came back with a mission &mdash; to make sure history changes for the better.<br>But things are scrambled. Your memories, not quite right. You&rsquo;re not sure how this present becomes your future. So until you can figure it out, you might as well do what you can, where you can, all the while trying to connect the dots between your world and this one.',
'moment': 'Everything you do could affect the future. For all you know, saving that one guy means that now the future is full of pterodactyls. The ripples are always so hard to track, and youre not sure if youve helped or hurt&mdash; not really. Until now. In this moment, its all clear. You can see the course of events laid out before you like a river, and you know exactly what you have to do to ensure the future outcome you want. Of course, after this, youve changed enough of the timeline to invalidate your prior research&mdash; reset all the names in your “Connecting the dots” section.',
'labels': {
'danger': 0,
'freak': 0,
'saviour': 2,
'superior': 1,
'mundane': -1
}
},
'nomad': {
'source': 'unbound',
'flavour': 'Maybe one time you had a home. A life with a schedule. But if you did, that was ages ago. You&rsquo;ve been on your own, bouncing around space, time and everything in between, for years. <br>Except now, you&rsquo;ve left those farscapes and come back to Earth. And letting other people into your life is way harder than travelling to other dimensions ever was.',
'moment': 'You basically exist with one foot out the door, ready to leave this place, to go back out into the wide expanse of the universe. Youve never fully committed. That is, until today. Until right now. Now, you pour everything you have and everything you are into this moment. You pull off tricks no one from this planet has ever seen before. You use your tools in ways no one here could have ever imagined. You devote yourself, here and now, to a cause, and you achieve your goal in ways that you never couldve if youd only stayed home. Of course, now youve proved to everyone that you really dont belong here, and the very skills that let you succeed are the ones you earned from out there&hellip;',
'labels': {
'danger': 0,
'freak': 2,
'saviour': -1,
'superior': 2,
'mundane': -1
}
},
'scion': {
'source': 'unbound',
'flavour': 'You&rsquo;re a child&mdash;not an acolyte, not a creation, just the friggin&rsquo; kid&mdash;of a true villain. And when anyone who knows looks at you, all they can see is your parent. Like you don&rsquo;t even matter. Well, forget that. You&rsquo;re out to prove yourself as someone different from them, and how better to do that than to be a superhero?',
'moment': 'People have always tried to define you by your lineage. As if from the moment you were born, you were meant to be some villain to be defeated. But&hellip;theyre right, arent they? That darkness is in you. So right here, right now, youre not fighting it&mdash;youre embracing it. Both hero and villain, and greater besides. Youre overcoming impossible odds in ways no hero would approve of, and no villain could comprehend. Of course, after seeing what you can really do when you embrace the whole of yourself, the rest of the world isnt going to forget who you really are&hellip;',
'labels': {
'danger': 1,
'freak': 0,
'saviour': 1,
'superior': 0,
'mundane': 0
}
}
}

2763
data/questions.py Normal file

File diff suppressed because it is too large Load Diff

30
data/sources.py Normal file
View File

@ -0,0 +1,30 @@
sources = {
'core': {
'title': 'Masks: a New Generation',
'type':'core book',
'author':'Brenadan Conway',
'publisher':'Magpie Games',
'year':'2016'
},
'halcyon': {
'title':'Halcyon City Herald Collection',
'type':'setting supplement',
'author':'Elizabeth Chaipraditkul, et al.',
'publisher':'Magpie Games',
'year':'2017'
},
'aegis': {
'title':'Secrets of A.E.G.I.S.',
'type':'setting supplement',
'author':'Cam Banks, et al.',
'publisher':'Magpie Games',
'year':'2018'
},
'unbound': {
'title':'Unbound',
'type':'setting supplement',
'author':'Misha Bushyager, et al.',
'publisher':'Magpie Games',
'year':'2018'
}
}

9
docker-compose.yml Normal file
View File

@ -0,0 +1,9 @@
version: '3.9'
services:
quiz:
build: .
container_name: masks_quiz
ports:
- 5000
restart: unless-stopped

14
interface/__init__.py Normal file
View File

@ -0,0 +1,14 @@
from flask import Flask
from secret import key
def create_app():
app = Flask(__name__)
app.config['SECRET_KEY'] = key
from .views import views
from .error_handlers import error_handler
app.register_blueprint(views, url_prefix = '/')
app.register_blueprint(error_handler)
return app

View File

@ -0,0 +1,7 @@
from flask import render_template, Blueprint, request
error_handler = Blueprint('error_handlers', __name__)
@error_handler.app_errorhandler(404)
def not_found(e):
return render_template("404.html")

View File

@ -0,0 +1,84 @@
body {
padding: 60px 0;
}
.wrapper-answer {
padding-left: 5px;
height: fit-content;
}
.wrapper-radio {
vertical-align: middle;
}
.wrapper-option {
vertical-align: middle;
border-bottom: 0.25px solid;
border-collapse: collapse;
border-spacing: 0;
}
.wrapper-option.last-option {
border-bottom: none;
}
.question {
margin: 0 auto 24px auto;
}
.sourcebook-title {
font-style: italic;
display: inline;
}
.small-caps {
font-variant-caps: small-caps;
}
.right-padded-cell {
padding-right: 10px;
}
.label-score {
text-align: right;
}
h1 {
padding: 30pt 0 60pt;
}
.section-head {
margin: 40px 0px 20px 0px;
}
.result, .glossary {
margin: 30pt 0 0pt
}
.graph-column {
display: inline-block;
}
.take-quiz {
margin: 10px auto 10px auto;
display: block;
}
.right-margin {
margin-right: 30px;
}
.left-margin {
margin-left: 30px;
}
.centre-buttons {
text-align: center;
margin: 30px 0 0 0;
}
@media only screen and (max-width: 400px) {
.graph-column {
display: none;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

2
interface/static/js/jquery.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,13 @@
const menuItems = document.getElementsByClassName("nav-link");
for(let i = 0; i< menuItems.length; i++) {
if(menuItems[i].pathname == window.location.pathname) {
menuItems[i].classList.add("active")
}
}
const dropdownItems = document.getElementsByClassName("dropdown-item");
for(let i = 0; i< dropdownItems.length; i++) {
if(dropdownItems[i].pathname == window.location.pathname) {
dropdownItems[i].classList.add("active")
$( "#" + dropdownItems[i].id ).closest( ".dropdown" ).find(".dropdown-toggle").addClass("active")
}
}

View File

@ -0,0 +1,11 @@
{% extends "base.html" %}
{% block title %} Page Not Found {% endblock %}
{% block content %}
<h1>Error: Page Not Found</h1>
<div class="container">
<a href="/">Return to the Home Page</a>
</div>
{% endblock %}

View File

@ -0,0 +1,89 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- Bootstrap .css below -->
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC"
crossorigin="anonymous">
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@5.15.4/css/fontawesome.min.css"
crossorigin="anonymous"
/>
<!-- Custom .css below -->
<link
rel="stylesheet"
href="{{ url_for('static', filename='css/style.css') }}"
/>
<!-- Favicons -->
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
<link rel="apple-touch-icon" href="{{ url_for('static', filename='favicon.png') }}">
<title>
{% block title %}{% endblock %} | Which Masks Playbook Are You? &mdash; a Personality Quiz
</title>
</head>
<body>
<!-- Create Nav Bar -->
{% include "navbar.html" %}
<!-- Flash Messages -->
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
<symbol id="check-circle-fill" fill="currentColor" viewBox="0 0 16 16">
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zm-3.97-3.03a.75.75 0 0 0-1.08.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-.01-1.05z"/>
</symbol>
<symbol id="info-fill" fill="currentColor" viewBox="0 0 16 16">
<path d="M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16zm.93-9.412-1 4.705c-.07.34.029.533.304.533.194 0 .487-.07.686-.246l-.088.416c-.287.346-.92.598-1.465.598-.703 0-1.002-.422-.808-1.319l.738-3.468c.064-.293.006-.399-.287-.47l-.451-.081.082-.381 2.29-.287zM8 5.5a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/>
</symbol>
<symbol id="exclamation-triangle-fill" fill="currentColor" viewBox="0 0 16 16">
<path d="M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566zM8 5c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995A.905.905 0 0 1 8 5zm.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2z"/>
</symbol>
</svg>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
{% if category == 'error' %}
<div class="alert alert-danger alter-dismissable fade show" role="alert">
<div class="container">
<svg class="bi flex-shrink-0 me-2" width="24" height="24" role="img" aria-label="Danger:"><use xlink:href="#exclamation-triangle-fill"/></svg>
{{ message|safe }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
</div>
{% endif %}
{% endfor %}
{% endif %}
{% endwith %}
<!-- Create Content Block -->
<div class="container">
{% block content %} {% endblock %}
</div>
<!-- Bootstrap Scripts Below -->
<script
src="https://code.jquery.com/jquery-3.2.1.slim.min.js"
integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN"
crossorigin="anonymous"
></script>
<script
src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js"
integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM"
crossorigin="anonymous"></script>
<!-- JQuery Below -->
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<script>window.jQuery || document.write('<script src="{{ url_for('static', filename='js/jquery.js') }}">\x3C/script>')</script>
<!-- Custom .js below -->
<script
type="text/javascript"
src="{{ url_for('static', filename='js/script.js') }}"
></script>
</body>
</html>

View File

@ -0,0 +1,28 @@
{% extends "base.html" %}
{% block title %} Compatibility and Accessibility {% endblock %}
{% block content %}
<h1>Compatibility and Accessibility</h1>
<h3 class="section-head">Browser Compatibility</h3>
<p>
This web app was made using the Bootstrap CSS framework. Most CSS components, browser elements, and JavaScript code should be compatible with most contemporary browsers. This version of the quiz reduces its use of JavaScript to process and evaluate results, and uses JavaScript for animations and other effects in the interface. So it should be compatible more widely.
</p>
<p>
If you notice any incompatibilities or strange browser behaviour, please let me know and I'll try and see how I can fix it.
</p>
<h3 class="section-head">Accessibility</h3>
<p>
Bootstrap as a CSS web design framework is built with a lot of accessibility features and guidance in mind, and the supporting documentation gave some guidance on what fields and metadata to populate to make page elements accessible to screen readers. I used the existing Bootstrap CSS classes and templates, and followed most existing guidance to enter metadata for html elements. This should hopefully facilitate screen readers being able to parse the page effectively. If there are any issues with this, let me know and I can make sure the page is structured in a way that is screen reader friendly.
</p>
<p>
Besides that, the colour schemes and fonts have been the standard fonts to ensure that they are legible and familiar. All links are also navigable by the keyboard alone, and this should make it a lot easier to interact with the quiz (especially when having to answer several questions).
</p>
<p>
Admittedly, this quiz is rather long. There are a lot of questions to answer. Although not all questions are mandatory, and a user need not fill in all answers in order to proceed.
</p>
<h3 class="section-head">Web Hosting</h3>
<p>
Because of how I re-structured this to be a web app running on the server, the way in which I host this site has changed considerably. There may be issues with how the various ports forward to each other on the server. I will monitor this. If there are any problems with accessing this, let me know and I will try to fix it.
</p>
{% endblock %}

View File

@ -0,0 +1,23 @@
{% extends "base.html" %}
{% block title %} Development {% endblock %}
{% block content %}
<h1>Development</h1>
<p>
I started work on the first version of this personality quiz over a year ago, when I decided to teach myself programming. The original version ran on JavaScript and was rendered and evaluated entirely by the browser. This version runs entirely on the server, and the quiz results are evaluated by the server rather than the client.
</p>
<p>
The back-end runs on Python 3.10, using the Flask framework. The web pages are templated using Jinja. The web site is rendered using html and the Bootstrap CSS framework. There have been some elements of the interface enhanced using rudimentary JavaScript, including the jQuery library. Of all of these programming languages, Python has proved to be a far more intuitive language to learn, and generally programming this has been a lot more enjoyable and a lot less frustrating than the earlier iteration that used JavaScript.
</p>
<p>
The quiz runs by rendering a form with all of the questions and options on the browser. The browser then submits the responses via a POST request to the server. The server evaluates the responses and renders the results accordingly. There is no information stored on the client.
</p>
<p>
All data transacted between the client and the server is stored as a Flask session for the duration that the user is on the web site. If the user closes the site, the session data is deleted and all answers submitted are lost.
</p>
<p>
There are, of course, some considerable inefficiencies in the way I have set up the templating. I have written the text for all the various web pages in the templates of the respective pages. Ideally, I would have preferred having a single template for all of the web pages or views, and then have the content served on those pages render dynamically based on the URL query. But that was a level of programming that was gratuitous at this point, as really most of the pages were serving static content anyway so it did not matter just now.
</p>
<p>
In addition, this is the first time I am storing the code of the quiz <a href="" title="Link to the code on my Git Repo">on my Git repo</a>, and having a much more streamlined version control process.
</p>
{% endblock %}

View File

@ -0,0 +1,35 @@
{% extends "base.html" %}
{% block title %} Home | Which Masks Playbook Are You? &mdash; a Personality Quiz {% endblock %}
{% block content %}
<h1>Home page</h1>
<h3 class="section-head">Background</h3>
<p>
This is a personality quiz to determine which playbook you are in the <em>Masks: a New Generation</em> table-top role-playing game. There are several personality quizzes out there for <em>D&amp;D</em>, and <a href="http://www.easydamus.com/character.html">this one by Easydamus</a> in particular inspired me. I thought it would be fun to make something similar for <em>Masks</em> as it is my favourite TRPG (more on that <a href="/masks">in the appropriate section</a>).
</p>
<p>
I began this project as a way of learning how to programme. I started with a farily rudimentary version of this quiz using JavaScript and html. The way that quiz worked was that it was processed on the client side, as the quiz logic was downloaded onto a browser and the scripts were run there. I had contemplated re-making the quiz using a server-side framework eventually, and in this version I taught myself how to make the quiz run on a server instead. I taught myself Python, Flask, and some rudimentary Jinja to make this.
</p>
<p>
If you want to take the quiz, you can do so here:
<a href="/quiz" class="btn btn-success take-quiz">Take the Quiz</a>
</p>
<h3 class="section-head">What is a Table-Top Role-Playing Game?</h3>
<p>
A table-top role-playing game is a collaborative story-telling game in which a group of players work together to tell a story, with a framework of rules that govern how players share narrative control and resolve uncertain outcomes.
</p>
<p>
The most famous TRPG is <em>Dungeons &amp; Dragons</em>, currently published by Wizards of the Coast. For decades, it has defined what this hobby has been perceived as becuase it is the most widely-played TRPG in the world and, as a consequence, is also most frequently referenced in pop culture. There are many other games out there &mdash; with different rulesets, settings, artwork, feel, and narrative purpose &mdash; coverying myriad genres of storytelling like fantasy, action, adventure, horror, sci-fi, cyberpunk, et cetera. Each of these games is referred to in the community as a game system.
</p>
<p>
<em>Masks</em> is another such role-playing game. Its specific hook is that it is about teenage superheroes who are juggling the emotional pressures of being a teenager with their awesome adventures of fighting villains. You can read more about <em>Masks</em> particularly in the relevant section.
</p>
<p>
The way these games work is players usually play characters through which they interact with the fictional world. These characters usually fall into a number of different archetypes depending on the system. The most common archetypes, or 'character classes', are from fantasy games like <em>D&amp;D</em>, like the Fighter, Wizard, Rogue, or Cleric. These classes in <em>D&amp;D</em> are determined by the various roles in a fantasy adventuring party that the characters play (with some loose narrative flavour around how their class is their calling in life). These archetypes also have some flavour text around common personality traits or tropes about them.
</p>
<p>
In <em>Masks</em>, the archetypes, referred to as 'playbooks', are all about being teenagers and getting up to teenage shenanigans. The archetypes are all determined by the personalities that the characters have. There are about 20 different archetypes to choose from, so naturally it can be really difficult to choose the one most suitable. So, I designed a personality quiz to help choose the best character archetype!
</p>
{% endblock %}

View File

@ -0,0 +1,11 @@
{% extends "base.html" %}
{% block title %} About Masks {% endblock %}
{% block content %}
<h1>About <em>Masks</em></h1>
<p><em>Masks</em> is a game by Brendan Conway using the Powered by the Apocalypse engine. For those unfamiliar with the term, Powered by the Apocalypse is a category of game systems that are based on the overall structure of the <em>Apocalypse World</em> system by Vincent and Meguey Baker. The core concept behind this system is to encourage collaborate and improvisational story-telling by streamlining the mechanics. This is in sharp contrast to traditional TRPGs that have very granular rules and hierarchical authority between a Game Master and the players.</p>
<p>If you imagine all table-top role-playing games to exist on a spectrum between chess and improv theatre with <em>D&amp;D</em> fifth edition right in the middle, PbtA games are much closes to the improv theatre side of the axis, whereas traditional games, while fourth edition <em>D&amp;D</em> would be closer to chess because of its wargame heritage.</p>
<p><em>Masks</em> is without a doubt my favourite role-playing game system, and I would strongly recommend trying it to anyone who hasn't played TRPGs before, or hasn't played Masks before. What I like most about Masks is its concept of playing teenage superheroes, and all the emotional drama and angst that that entails. For me, it has been so much fun to revisit a difficult age but from a perspective of greater maturity and in a setting where the very differences that I had been victimised for are framed not as liabilities but as superpowers: things that made characters special. This was an age when I really loved reading comic books, and that is why revisiting it with this zany aesthetic is such a delight.</p>
<h3 class="section-head">Note on Content</h3>
<p>The names of the playbooks, as well as their associated flavour text, are by Brendan Conway, taken from the player sheets that are part of the game documents made freely-available on-line.</p>
<p>Special thanks to the members of the <em>Masks</em> Discord for their advice, feedback, and patience with the original quirks of the web hosting.</p>
{% endblock %}

View File

@ -0,0 +1,52 @@
<nav class="navbar fixed-top navbar-expand-md navbar-dark bg-dark">
<div class="container">
<a href="/" class="navbar-brand mb-0 h1">Masks Personality Quiz | V.S.</a>
<button
class="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#navbar"
aria-controls="navbar"
aria-expanded="false"
aria-label="Toggle Navigation"
>
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbar">
<ul class="navbar-nav">
<li class="nav-item" id="nav-home">
<a class="nav-link" id="link-home" href="/">Home</a>
</li>
<li class="nav-item" id="nav-quiz">
<a class="nav-link" id="link-quiz" href="/quiz">Take the Quiz</a>
</li>
<li class="nav-item dropdown" id="nav-about">
<a
class="nav-link dropdown-toggle"
id="dropdown-about"
role="button"
href="/masks"
data-bs-toggle="dropdown"
aria-expanded="false"
>
About
</a>
<ul
class="dropdown-menu"
aria-labelledby="dropdown-about"
>
<li>
<a href="/masks" id="link-masks" class="dropdown-item">About <em>Masks</em></a>
</li>
<li>
<a href="https://git.vsnt.uk/viveksantayana/masks-personality-quiz" id="link-dev" class="dropdown-item">View the Code</a>
</li>
<li>
<a href="/compatibility" id="link-compatibility" class="dropdown-item">Compatibility and Accessibility</a>
</li>
</ul>
</li>
</ul>
</div>
</div>
</nav>

View File

@ -0,0 +1,82 @@
{% extends "base.html" %}
{% block title %} Take the Quiz {% endblock %}
{% block content %}
<h1>Take the Quiz</h1>
<p>
Please answer the following {{ questions|length }} questions. None of the questions are mandatory, and you can skip questions you are unsure of or do not want to answer. But you cannot leave the quiz blank. The more questions you answer, the better results you will get.
</p>
<p>
You will also be able to select which source books you would like to see results from. You can exclude source books to narrow down the range of playbooks, or add all of them to have a full range of playboosk to choose from.
</p>
<form method="POST">
<div class="form-group">
{% for question in questions -%}
<fieldset class="question">
<legend>
<strong>Question {{ loop.index }}</strong>: {{ question['question']|safe }}
</legend>
<table>
<tbody>
{% set name = 'q'~loop.index %}
{% for answer in question['answers'] -%}
<tr class="wrapper-option{% if loop.index == question['answers']|length %} last-option{% endif %}">
<td class="wrapper-radio">
<input
class="form-check-input"
type="radio"
name="{{ name }}"
id="{{ name }}.{{ loop.index }}"
value="{{ loop.index }}"
{% if 'submission' in session %}
{{'checked' if session['submission'][name] == loop.index|lower else '' }}
{% endif%}
>
</td>
<td class="wrapper-answer">
<label
class="form-check-label"
for="{{ name }}.{{ loop.index }}"
>
{{ answer['text']|safe }}
</label>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</fieldset>
{% endfor %}
<fieldset class="source-filters">
<h5>
Please select which sourcebooks you would like to include results from.
</h5>
{% for source in sources -%}
<input
type="checkbox"
name="{{ source }}"
id="{{ source }}"
{% if 'submission' in session %}
{{'' if source not in session['submission'] else 'checked' }}
{% else %}
checked="true"
{% endif %}
class="form-check-input"
>
<label
for="{{ source }}"
class="form-check-label sourcebook-title"
>
{{ sources[source]['title'] }}
</label>
{% endfor %}
</fieldset>
<div class="centre-buttons">
<button type="submit" class="btn btn-success right-margin" href="/results">Submit</button>
<a class="btn btn-danger left-margin" href="/reset">Reset</a>
</div>
</div>
</form>
{% endblock %}

View File

@ -0,0 +1,153 @@
{% extends "base.html" %}
{% block title %} Your Results {% endblock %}
{% block content %}
<h1>Your Results</h1>
<a name="top-anchor"></a>
{% if results['selected_playbooks']|length > 1 %}
<h3>Your Playbooks are:</h3>
{% else%}
<h3>Your Playbook is:</h3>
{% endif %}
{%for playbook in results['selected_playbooks'] -%}
<div class="result">
<h4>The <span class="small-caps">{{ playbook[0]|upper }}{{ playbook[1:] }}</span></h4>
{% if playbook == 'joined' %}
<h6>
You have duplicated The <span class="small-caps">{{ results['joined_cloned'][0]|upper }}{{ results['joined_cloned'][1:] }}</span>
</h6>
{% endif %}
<p>
{{ playbooks[playbook]['flavour']|safe }}
</p>
{% set source = sources[playbooks[playbook]['source']] %}
<p>From the <em>{{ source['title'] }}</em> {{ source['type'] }} by {{ source['author'] }}, published by {{ source['publisher'] }} in {{ source['year'] }}.</p>
<p>
<strong><a href="#momentoftruth" title="Click to the the glossary." class="small-caps">Moment of Truth</a></strong>: {{ playbooks[playbook]['moment']|safe }}
</p>
<p>
<strong><span class="small-caps">Labels</span></strong>: <br />
<table>
<tbody>
{% for label in results['selected_playbooks'][playbook] -%}
<tr>
<td class="right-padded-cell">
<strong><a href="#{{ label }}" title="Click to see the glossary." class="small-caps">{{ label[0]|upper}}{{ label[1:]}}</a></strong>:
</td>
<td class="label-score">
{{ results['selected_playbooks'][playbook][label] }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</p>
</div>
{% endfor %}
<h3 class="section-head">How You Fared:</h3>
<p>
This chart represents how many points you got for each playbook from the source books you selected in all the questions you answered.
</p>
<div class="graph">
<table>
<tbody>
{% for playbook in results['playbooks'] -%}
<tr>
<td class="right-padded-cell">
{{ loop.index }}.
</td>
<td class="small-caps right-padded-cell">
The {{ playbook[0]|upper }}{{ playbook[1:] }}
</td>
<td class="right-padded-cell">
{{ (results['playbooks'][playbook]*100/results['max_score'])|round(0) }}&#37;
</td>
<td>
<span class="graph-column">{% for i in range( (results['playbooks'][playbook]*20/results['max_score'])|round(0, 'ceil')|int) -%}X{% endfor %}</span>
({{ results['playbooks'][playbook] }})
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="glossary">
<table>
<h3 class="small-caps section-head">
Glossary
</h3>
<h4 class="small-caps section-head">Labels</h4>
<p>In <em>Masks</em>, your character&rsquo;s attributes are defined by their &lsquo;labels&rsquo;. This is like the character ability scores in <em>D&amp;D</em>. Mechanically, it gives bonuses or penalties to your rolls. Narratively, it reflects your chances of success given your self image and your confidence in your abilities.</p>
<p>Because everyone plays a teenager, your labels are all about the different dimensions of your self image, and these labels keep shifting all the time as the story progresses and events change the way you see yourself. And this happens a <em>lot</em>. Because, teenagers.</p>
<p>Labels range on a scale of -2 to +3</p>
<tbody>
<tr class="wrapper-option">
<td class="right-padded-cell small-caps">
<a name="danger" href="#top-anchor" title="Back to the top">Danger</a>
</td>
<td>
{{ labels['danger']['flavour'] }}
{{ results['display_labels'].remove('danger') if results['display_labels'].remove('danger') is not none else '' }}
</td>
</tr>
<tr class="wrapper-option">
<td class="right-padded-cell small-caps">
<a name="freak" href="#top-anchor" title="Back to the top">Freak</a>
</td>
<td>
{{ labels['freak']['flavour'] }}
{{ results['display_labels'].remove('freak') if results['display_labels'].remove('freak') is not none else '' }}
</td>
</tr>
<tr class="wrapper-option">
<td class="right-padded-cell small-caps">
<a name="saviour" href="#top-anchor" title="Back to the top">Saviour</a>
</td>
<td>
{{ labels['saviour']['flavour'] }}
{{ results['display_labels'].remove('saviour') if results['display_labels'].remove('saviour') is not none else '' }}
</td>
</tr>
<tr class="wrapper-option">
<td class="right-padded-cell small-caps">
<a name="superior" href="#top-anchor" title="Back to the top">Superior</a>
</td>
<td>
{{ labels['superior']['flavour'] }}
{{ results['display_labels'].remove('superior') if results['display_labels'].remove('superior') is not none else '' }}
</td>
</tr>
<tr class="wrapper-option{% if results['display_labels']|length == 1 %} last-option{% endif %}">
<td class="right-padded-cell small-caps">
<a name="mundane" href="#top-anchor" title="Back to the top">Mundane</a>
</td>
<td>
{{ labels['mundane']['flavour'] }}
{{ results['display_labels'].remove('mundane') if results['display_labels'].remove('mundane') is not none else '' }}
</td>
</tr>
{% for label in results['display_labels'] -%}
<tr class="right-padded-cell wrapper-option{% if loop.index == results['display_labels']|length %} last-option{% endif %}">
<td class="small-caps">
<a name="{{ label }}" href="#top-anchor" title="Back to the top">{{ label[0]|upper }}{{ label[1:] }}</a>
</td>
<td>
{{ labels[label]['flavour'] }}
{{ results['display_labels'].remove(label) if results['display_labels'].remove(label) is not none else '' }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
<h4 class="section-head">Your <a href="#top-anchor" class="small-caps" title="Back to the top" name="momentoftruth">Moment of Truth</a></h4>
<p>
In <em>Masks</em>, your <span class="small-caps">Moment of Truth</span> is a defining moment in your character&rsquo;s life when you are in the spotlight. All eyes are on you, and it is when you prove who you really are. For a moment, you grow into the best version of yourself and you show to everyone who you really are.
</p>
<p>
What this means mechanically is that, for one scene, you get to narrate what happens as per the script of your moment of truth. You take down a powerful threat, harness unbelievable power, or prove yourself to everyone watching. And then, it locks one of your <span class="small-caps">Labels</span>. That <span class="small-caps">Label</span> can no longer shift.
</p>
</div>
{% endblock %}

42
interface/views.py Normal file
View File

@ -0,0 +1,42 @@
from flask import Blueprint, render_template, request, session, redirect
from flask.helpers import url_for
from data.labels import labels
from data.playbooks import playbooks
from data.questions import questions
from data.sources import sources
from quiz.validators import validate_submissions
from quiz.evaluation import evaluate_quiz
views = Blueprint('views', __name__)
@views.route('/compatibility')
def compatibility():
return render_template('compatibility.html')
@views.route('/')
def home():
return render_template('home.html')
@views.route('/masks')
def about():
return render_template('masks.html')
@views.route('/quiz', methods=['GET', 'POST'])
def quiz():
if request.method == 'POST':
session['submission'] = request.form
if validate_submissions(session['submission']):
return redirect(url_for('views.results'))
return render_template('quiz.html', questions=questions, sources=sources)
@views.route('/results')
def results():
if 'submission' not in session:
return redirect(url_for('views.quiz'))
results = evaluate_quiz(session['submission'])
return render_template('results.html', results = results, playbooks = playbooks, labels = labels, sources = sources)
@views.route('/reset')
def reset():
session.clear()
return redirect(url_for('views.quiz'))

0
quiz/__init__.py Normal file
View File

86
quiz/evaluation.py Normal file
View File

@ -0,0 +1,86 @@
from data.sources import sources
from data.playbooks import playbooks
from data.labels import labels
from data.questions import questions
from random import randrange
def evaluate_quiz(submission):
# Set up dictionaries to count scores
results = {
'playbooks' : {},
'selected_playbooks': {},
'display_labels': [],
'max_score': 0
}
for playbook in playbooks:
if playbooks[playbook]['source'] in set.intersection(set(sources), set(submission)):
results['playbooks'][playbook] = 0
for answer in submission:
if answer.startswith('q'):
qno = int(answer[1:]) - 1
ano = int(submission[answer]) - 1
match_list = questions[qno]['answers'][ano]['matches']
for match in match_list:
if match in results['playbooks']: results['playbooks'][match] += match_list[match]
results['max_score'] += max(match_list.values())
high_score = max(results['playbooks'].values())
for playbook, score in results['playbooks'].items():
if score == high_score:
results['selected_playbooks'][playbook] = {}
for label in labels:
if 'custom' not in labels[label] or not labels[label]['custom']:
results['selected_playbooks'][playbook][label] = playbooks[playbook]['labels'][label]
if label not in results['display_labels']: results['display_labels'].append(label)
else:
if playbook in labels[label]['playbooks']:
if label in playbooks[playbook]['labels']:
results['selected_playbooks'][playbook][label] = playbooks[playbook]['labels'][label]
else:
results['selected_playbooks'][playbook][label] = labels[label]['default_value']
if label not in results['display_labels']: results['display_labels'].append(label)
if 'joined' in results['selected_playbooks']:
if len(results['selected_playbooks']) > 1:
l = list(results['selected_playbooks'])
l.remove('joined')
i = randrange(len(l)-1)
results['joined_cloned'] = l[i]
results['selected_playbooks']['joined'] = results['selected_playbooks'][l[i]].copy()
else:
d = results['playbooks'].copy()
del d['joined']
high_score = max(d.values())
p = []
for playbook, score in d.items():
if score == high_score:
p.append(playbook)
p_sel = p[randrange(len(p)-1)] if len(p) > 1 else p[0]
results['selected_playbooks'][p_sel] = {}
results['joined_cloned'] = p_sel
for label in labels:
if 'custom' not in labels[label] or not labels[label]['custom']:
results['selected_playbooks'][p_sel][label] = playbooks[playbook]['labels'][label]
if label not in results['display_labels']: results['display_labels'].append(label)
else:
if playbook in labels[label]['playbooks']:
if label in playbooks[playbook]['labels']:
results['selected_playbooks'][p_sel][label] = playbooks[playbook]['labels'][label]
else:
results['selected_playbooks'][p_sel][label] = labels[label]['default_value']
if label not in results['display_labels']: results['display_labels'].append(label)
results['selected_playbooks']['joined'] = results['selected_playbooks'][p_sel].copy()
for answer in submission:
if answer.startswith('q'):
qno = int(answer[1:]) - 1
ano = int(submission[answer]) - 1
for increase in questions[qno]['answers'][ano]['increase']:
for playbook in results['selected_playbooks']:
if increase in results['selected_playbooks'][playbook]:
if results['selected_playbooks'][playbook][increase] < 3: results['selected_playbooks'][playbook][increase] += 1
for decrease in questions[qno]['answers'][ano]['decrease']:
for playbook in results['selected_playbooks']:
if decrease in results['selected_playbooks'][playbook]:
if results['selected_playbooks'][playbook][decrease] > -2: results['selected_playbooks'][playbook][decrease] -= 1
return results

23
quiz/validators.py Normal file
View File

@ -0,0 +1,23 @@
from flask.helpers import flash
from data.sources import sources
from flask import flash
def validate_questions(submissions):
for key in submissions:
if 'q' in key and int(key[1:]):
return True
flash('<strong>Error</strong>: You cannot leave the quiz blank.', category='error')
return False
def validate_filters(submissions):
if not set.intersection(set(submissions.keys()), set(sources.keys())):
flash('<strong>Error</strong>: You must select at least one source book to show results from.', category='error')
return False
else:
return True
def validate_submissions(submissions):
if validate_questions(submissions) and validate_filters(submissions):
return True
else:
return False

7
requirements.txt Normal file
View File

@ -0,0 +1,7 @@
click==8.0.3
colorama==0.4.4
Flask==2.0.2
itsdangerous==2.0.1
Jinja2==3.0.2
MarkupSafe==2.0.1
Werkzeug==2.0.2

1
secret.py.example Normal file
View File

@ -0,0 +1 @@
key = ''