Просмотр исходного кода

Merge branch 'master' into discord

JoostSijm 6 лет назад
Родитель
Сommit
c1683cedbc
28 измененных файлов с 1139 добавлено и 128 удалено
  1. 11 1
      app/__init__.py
  2. 4 0
      app/flaskr.py
  3. 171 30
      app/models.py
  4. 1 1
      app/modules/auth/app.py
  5. 6 0
      app/modules/backend/modules/vote/__init__.py
  6. 130 0
      app/modules/backend/modules/vote/app.py
  7. 45 0
      app/modules/backend/modules/vote/templates/backend_vote/add_question.j2
  8. 28 0
      app/modules/backend/modules/vote/templates/backend_vote/codes.j2
  9. 46 0
      app/modules/backend/modules/vote/templates/backend_vote/create.j2
  10. 45 0
      app/modules/backend/modules/vote/templates/backend_vote/index.j2
  11. 84 0
      app/modules/backend/modules/vote/templates/backend_vote/view.j2
  12. 0 49
      app/modules/backend/templates/layout/backend.j2
  13. 0 21
      app/modules/backend/templates/layout/public.j2
  14. 1 1
      app/modules/backend/templates/public/private.j2
  15. 1 1
      app/modules/backend/templates/public/public.j2
  16. 6 0
      app/modules/vote/__init__.py
  17. 66 0
      app/modules/vote/app.py
  18. 52 0
      app/modules/vote/templates/vote/index.j2
  19. 113 0
      app/modules/vote/templates/vote/view.j2
  20. 9 0
      app/static/css/index.css
  21. 55 5
      app/static/js/index.js
  22. 51 0
      app/templates/layout/backend.j2
  23. 7 0
      app/templates/layout/page.j2
  24. 23 0
      app/templates/layout/public.j2
  25. 34 0
      migrations/versions/3d9ba6086a23_improvin_voting_models.py
  26. 30 0
      migrations/versions/c1d7ddb101a1_add_code.py
  27. 101 0
      migrations/versions/c98e920c06ae_add_ballots_remove_page_file_relation.py
  28. 19 19
      webpack.config.js

+ 11 - 1
app/__init__.py

@@ -13,6 +13,7 @@ from flask_login import LoginManager
 from flask_compress import Compress
 from flask_argon2 import Argon2
 from flask_menu import Menu
+from sqlalchemy import MetaData
 from dotenv import load_dotenv
 from flask_apscheduler import APScheduler
 from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore
@@ -50,7 +51,16 @@ COMPRESS_MIN_SIZE = 500
 Compress(app)
 Menu(app=app)
 
-db = SQLAlchemy(app)
+convention = {
+    "ix": 'ix_%(column_0_label)s',
+    "uq": "uq_%(table_name)s_%(column_0_name)s",
+    "ck": "ck_%(table_name)s_%(constraint_name)s",
+    "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
+    "pk": "pk_%(table_name)s"
+}
+
+metadata = MetaData(naming_convention=convention)
+db = SQLAlchemy(app, metadata=metadata)
 migrate = Migrate(app, db)
 argon2 = Argon2(app)
 

+ 4 - 0
app/flaskr.py

@@ -9,9 +9,11 @@ from app import app
 from app.modules.static import Static
 from app.modules.backend import Backend
 from app.modules.auth import Auth
+from app.modules.vote import Vote
 from app.modules.backend.modules.page import Backend_Page
 from app.modules.backend.modules.file import Backend_File
 from app.modules.backend.modules.user import Backend_User
+from app.modules.backend.modules.vote import Backend_Vote
 
 app.register_blueprint(Auth)
 app.register_blueprint(Static)
@@ -19,6 +21,8 @@ app.register_blueprint(Backend, url_prefix='/backend')
 app.register_blueprint(Backend_Page, url_prefix='/backend/page')
 app.register_blueprint(Backend_File, url_prefix='/backend/file')
 app.register_blueprint(Backend_User, url_prefix='/backend/user')
+app.register_blueprint(Backend_Vote, url_prefix='/backend/vote')
+app.register_blueprint(Vote, url_prefix='/vote')
 
 
 @app.errorhandler(404)

+ 171 - 30
app/models.py

@@ -3,8 +3,11 @@
 All models for module
 """
 
+from hashlib import blake2b
+import hmac
+
 from urllib.parse import quote
-from datetime import datetime
+from datetime import datetime, timedelta
 # from sqlalchemy.ext.hybrid import hybrid_method, hybrid_property
 import markdown
 from flask import Markup
@@ -15,28 +18,16 @@ from app import db, argon2, login_manager
 ALLOWED_EXTENSIONS = set(['txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif'])
 IMAGE_EXTENSIONS = set(['png', 'jpg', 'jpeg', 'gif'])
 
-page_file = db.Table(
-    'page_file',
-    db.Column(
-        'page_id',
-        db.Integer,
-        db.ForeignKey('page.id'),
-        primary_key=True
-    ),
-    db.Column(
-        'file_id',
-        db.Integer,
-        db.ForeignKey('file.id'),
-        primary_key=True
-    ),
-)
-
 
 class User(db.Model, UserMixin):
     """Model for User"""
     id = db.Column(db.Integer, primary_key=True)
     name = db.Column(db.String, unique=True, nullable=False)
     email = db.Column(db.String(255), unique=True)
+    discord = db.Column(db.String(255), unique=True)
+    game_id = db.Column(db.BigInteger, unique=True)
+    alt = db.Column(db.Boolean, server_default='f', default=False)
+    party_member = db.Column(db.Boolean, server_default='f', default=False)
     _password = db.Column("password", db.String(255))
     registration_at = db.Column(db.DateTime, default=datetime.utcnow)
     approved = db.Column(db.Boolean, server_default='f', default=False)
@@ -62,15 +53,21 @@ class User(db.Model, UserMixin):
         """Check if password is correct"""
         return argon2.check_password_hash(self.password, password)
 
+    def game_url(self):
+        """Give profile url"""
+        if self.game_id:
+            return "https://rivalregions.com/#slide/profile/%d" % self.game_id
+        return
+
 
-#   function_id = db.Column(
-#       db.Integer,
-#       db.ForeignKey("function.id")
-#   )
-#   user = db.relationship(
-#       "Function",
-#       backref=db.backref("users", lazy="dynamic")
-#   )
+    question_id = db.Column(
+        db.Integer,
+        db.ForeignKey("question.id")
+    )
+    user = db.relationship(
+        "Question",
+        backref=db.backref("users", lazy="dynamic")
+    )
 
 
 class Page(db.Model):
@@ -152,9 +149,153 @@ class File(db.Model):
         backref=db.backref("files", lazy="dynamic")
     )
 
-    files = db.relationship(
-        'Page',
-        secondary=page_file,
-        lazy='subquery',
-        backref=db.backref('pages', lazy=True)
+
+class Ballot(db.Model):
+    """Model for Ballot"""
+    id = db.Column(db.Integer, primary_key=True)
+    name = db.Column(db.String, nullable=False)
+    description = db.Column(db.String)
+    start_at = db.Column(db.DateTime, default=datetime.utcnow)
+    end_at = db.Column(db.DateTime, nullable=False)
+
+    user_id = db.Column(
+        db.Integer,
+        db.ForeignKey("user.id")
+    )
+    user = db.relationship(
+        "User",
+        backref=db.backref("options", lazy="dynamic")
+    )
+
+    priority_id = db.Column(
+        db.Integer,
+        db.ForeignKey("priority.id")
+    )
+    priority = db.relationship(
+        "Priority",
+        backref=db.backref("ballots", lazy="dynamic")
+    )
+
+    def active(self):
+        """Check if vote is active"""
+        today = datetime.today()
+        if self.start_at <= today <= self.end_at:
+            return True
+        return False
+
+
+class Priority(db.Model):
+    """Model for Priority"""
+    id = db.Column(db.Integer, primary_key=True)
+    name = db.Column(db.String, nullable=False)
+    description = db.Column(db.String, nullable=False)
+
+
+class Question(db.Model):
+    """Model for Question"""
+    id = db.Column(db.Integer, primary_key=True)
+    name = db.Column(db.String, nullable=False)
+    description = db.Column(db.String)
+    combined_approval_voting = db.Column(db.Boolean, server_default='f', default=False)
+
+    ballot_id = db.Column(
+        db.Integer,
+        db.ForeignKey("ballot.id")
+    )
+    ballot = db.relationship(
+        "Ballot",
+        backref=db.backref("questions", lazy="dynamic")
+    )
+
+    def has_voten(self, user_id):
+        """Check if user has voted"""
+        for option in self.options:
+            if option.votes.filter(Vote.user_id == user_id).first():
+                return True
+        return False
+
+
+    def get_option(self, option):
+        """Get the pro option"""
+        return self.options.filter(Option.name == option).first()
+
+
+    def score(self):
+        """Get winning option from question"""
+        if self.combined_approval_voting:
+            pro_option = self.get_option('Voor')
+            pro_votes = pro_option.votes.count()
+            against_option = self.get_option('Tegen')
+            against_votes = against_option.votes.count()
+            return pro_votes - against_votes
+
+        higest = Option()
+        for option in self.options:
+            if higest.votes.count() < option.votes.count():
+                higest = option
+        return higest.name
+
+
+class Option(db.Model):
+    """Model for Option"""
+    id = db.Column(db.Integer, primary_key=True)
+    name = db.Column(db.String)
+    description = db.Column(db.String)
+
+    user_id = db.Column(
+        db.Integer,
+        db.ForeignKey("user.id")
+    )
+    user = db.relationship(
+        "User",
+        backref=db.backref("eligible", lazy="dynamic")
+    )
+
+    question_id = db.Column(
+        db.Integer,
+        db.ForeignKey("question.id")
+    )
+    question = db.relationship(
+        "Question",
+        backref=db.backref("options", lazy="dynamic")
+    )
+
+
+class Vote(db.Model):
+    """Model for Vote"""
+    id = db.Column(db.Integer, primary_key=True)
+    datetime = db.Column(db.DateTime, default=datetime.utcnow)
+
+    option_id = db.Column(
+        db.Integer,
+        db.ForeignKey("option.id")
+    )
+    option = db.relationship(
+        "Option",
+        backref=db.backref("votes", lazy="dynamic")
+    )
+
+    user_id = db.Column(
+        db.Integer,
+        db.ForeignKey("user.id")
     )
+    user = db.relationship(
+        "User",
+        backref=db.backref("votes", lazy="dynamic")
+    )
+
+class Code(db.Model):
+    """Model for code"""
+    id = db.Column(db.Integer, primary_key=True)
+    expire_date = db.Column(db.DateTime, default=datetime.utcnow() + timedelta(days=20))
+    secret = db.Column(db.String(255), unique=True)
+
+    def get_digest(self, string):
+        """Generate digest on string"""
+        if isinstance(string, int):
+            string = str(string)
+        string = string.encode('utf-8')
+        secret = self.secret.encode('utf-8')
+        blake = blake2b(digest_size=3)
+        blake.update(secret + string)
+        return blake.hexdigest()

+ 1 - 1
app/modules/auth/app.py

@@ -92,4 +92,4 @@ def register():
 def logout():
     """Logout function for users"""
     logout_user()
-    return redirect(url_for('static.show'))
+    return redirect('')

+ 6 - 0
app/modules/backend/modules/vote/__init__.py

@@ -0,0 +1,6 @@
+
+"""
+Website login page
+"""
+
+from .app import BLUEPRINT as Backend_Vote

+ 130 - 0
app/modules/backend/modules/vote/app.py

@@ -0,0 +1,130 @@
+
+"""
+Backend vote module
+"""
+
+import hashlib
+import hmac
+
+from datetime import datetime
+from flask_login import login_required, current_user
+from flask_menu import register_menu
+from flask import render_template, request, flash, Blueprint, redirect, url_for
+from app.models import User, Ballot, Priority, Question, Option, Code
+from app import db
+
+
+BLUEPRINT = Blueprint(
+    'backend_vote',
+    __name__,
+    template_folder='templates'
+)
+
+
+@register_menu(BLUEPRINT, 'vote', 'Vote')
+@BLUEPRINT.route("/")
+@login_required
+def index():
+    """Ballots overview"""
+    ballots = Ballot.query.all()
+
+    return render_template(
+        'backend_vote/index.j2',
+        ballots=ballots,
+    )
+
+
+@BLUEPRINT.route("/codes")
+@login_required
+def codes():
+    """codes overview"""
+    code = Code.query.order_by(Code.expire_date.desc()).first()
+    users = User.query.all()
+
+    return render_template(
+        'backend_vote/codes.j2',
+        users=users,
+        code=code,
+    )
+
+
+@BLUEPRINT.route('/create', methods=["GET", "POST"])
+@login_required
+def create():
+    """Creating ballot"""
+    if request.method == 'POST':
+        ballot = Ballot()
+        print(request.form)
+        ballot.name = request.form['name']
+        ballot.description = request.form['description']
+        ballot.user_id = current_user.id
+
+        start_at = "%s %s" % (request.form['start_at_date'], request.form['start_at_time'])
+        ballot.start_at = datetime.strptime(start_at, "%Y-%m-%d %H:%M")
+        end_at = "%s %s" % (request.form['end_at_date'], request.form['end_at_time'])
+        ballot.end_at = datetime.strptime(end_at, "%Y-%m-%d %H:%M")
+
+        db.session.add(ballot)
+        db.session.commit()
+
+        flash('Page "%s" successfully created' % ballot.name, 'success')
+        return redirect(url_for('vote.view', ballot_id=ballot.id))
+
+    priorities = Priority.query.all()
+
+    return render_template(
+        'backend_vote/create.j2',
+        priorities=priorities
+    )
+
+
+@BLUEPRINT.route('/<int:ballot_id>', methods=["GET", "POST"])
+@login_required
+def view(ballot_id):
+    """View ballot"""
+    ballot = Ballot.query.get(ballot_id)
+    if request.method == 'POST':
+        option = Option()
+        option.question_id = request.form['question_id']
+        option.name = request.form['name']
+
+        db.session.add(option)
+        db.session.commit()
+
+    return render_template(
+        'backend_vote/view.j2',
+        ballot=ballot,
+    )
+
+
+@BLUEPRINT.route('/<int:ballot_id>/add_question', methods=["GET", "POST"])
+@login_required
+def add_question(ballot_id):
+    """Add question to ballot"""
+    ballot = Ballot.query.get(ballot_id)
+    if request.method == 'POST':
+        question = Question()
+        question.ballot_id = ballot.id
+        question.name = request.form['name']
+        question.description = request.form['description']
+        question.combined_approval_voting = 'combined_approval_voting' in request.form
+
+        db.session.add(question)
+        db.session.commit()
+
+        if question.combined_approval_voting:
+            options = ['Voor', 'Tegen', 'Onthouden']
+            for option_name in options:
+                option = Option()
+                option.question_id = question.id
+                option.name = option_name
+
+                db.session.add(option)
+
+            db.session.commit()
+        return redirect(url_for('vote.view', ballot_id=ballot.id))
+
+    return render_template(
+        'backend_vote/add_question.j2',
+        ballot=ballot,
+    )

+ 45 - 0
app/modules/backend/modules/vote/templates/backend_vote/add_question.j2

@@ -0,0 +1,45 @@
+{% extends "layout/backend.j2" %}
+{% block content %}
+<h1>Add question: {{ ballot.name }}</h1>
+<table class="table table-sm">
+    <tr>
+        <th scope="row">Description</th>
+        <td>{{ ballot.description }}</td>
+    </tr>
+    <tr>
+        <th scope="row">Start</th>
+        <td>{{ ballot.start_at }}</td>
+    </tr>
+    <tr>
+        <th scope="row">End</th>
+        <td>{{ ballot.end_at }}</td>
+    </tr>
+    <tr>
+        <th scope="row">User</th>
+        <td>{{ ballot.user.name }}</td>
+    </tr>
+    <tr>
+        <th scope="row">Priority</th>
+        <td>{{ ballot.priority.name }}</td>
+    </tr>
+</table>
+<form method="post">
+    <div class="form-group">
+        <label class="text-normal text-dark">Name</label>
+        <input type="text" class="form-control" name="name" placeholder="Name" required>
+    </div>
+    <div class="form-group">
+        <label class="text-normal text-dark">Description</label>
+        <textarea class="form-control" name="description" rows="10"></textarea>
+    </div>
+    <div class="form-group form-check">
+        <input class="form-check-input" type="checkbox" name="combined_approval_voting" value="1" id="combined_approval_voting_check">
+        <label class="form-check-label" for="combined_approval_voting_check">
+            Combined approval voting
+        </label>
+    </div>
+    <div class="form-group text-right">
+        <button class="btn btn-primary">Create</button>
+    </div>
+</form>
+{% endblock %}

+ 28 - 0
app/modules/backend/modules/vote/templates/backend_vote/codes.j2

@@ -0,0 +1,28 @@
+{% extends "layout/backend.j2" %}
+{% block content %}
+<h1>Codes</h1>
+<div class="table-responsive">
+    <table class="table table-striped table-sm">
+        <thead>
+            <tr>
+                <th>Name</th>
+                <th>Alt</th>
+                <th>Discord</th>
+                <th>Rival Regions</th>
+                <th>Code</th>
+            </tr>
+        </thead>
+        <tbody>
+            {% for user in users %}
+            <tr>
+                <td>{{ user.name }}</td>
+                <td>{{ user.alt }}</td>
+                <td>{{ user.discord }}</td>
+                <td>{% if user.game_id %}<a href="{{ user.game_url() }}" target="_blank">{{ user.game_id }}</a>{% endif %}</td>
+                <td>{{ code.get_digest(user.id) }}</td>
+            </tr>
+            {%- endfor -%}
+        </tbody>
+    </table>
+</div>
+{% endblock %}

+ 46 - 0
app/modules/backend/modules/vote/templates/backend_vote/create.j2

@@ -0,0 +1,46 @@
+{% extends "layout/backend.j2" %}
+{% block content %}
+<h1>Create vote</h1>
+<form method="post">
+    <div class="form-group">
+        <label class="text-normal text-dark">Name</label>
+        <input type="text" class="form-control" name="name" placeholder="Name" required>
+    </div>
+    <div class="form-group">
+        <label class="text-normal text-dark">Description</label>
+        <textarea class="form-control" name="description" rows="10"></textarea>
+    </div>
+    <div class="row">
+        <div class="form-group col">
+            <label for="example-datetime-local-input" class="form-label">Start date</label>
+            <input class="form-control" type="date" name="start_at_date" required>
+        </div>
+        <div class="form-group col">
+            <label for="example-time-input" class="form-label">Start time</label>
+            <input class="form-control" type="time" name="start_at_time" required>
+        </div>
+    </div>
+    <div class="row">
+        <div class="form-group col">
+            <label for="example-datetime-local-input" class="form-label">End date</label>
+            <input class="form-control" type="date" name="end_at_date" required>
+        </div>
+        <div class="form-group col">
+            <label for="example-time-input" class="form-label">End time</label>
+            <input class="form-control" type="time" name="end_at_time" required>
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="text-normal text-dark">Priority</label>
+        <select class="form-control" name="priority_id">
+            <option></option>
+            {% for prioritiy in priorities %}
+            <option value="{{ page.id }}">{{ priority.name }}</option>
+            {% endfor %}
+        </select>
+    </div>
+    <div class="form-group text-right">
+        <button class="btn btn-primary">Create</button>
+    </div>
+</form>
+{% endblock %}

+ 45 - 0
app/modules/backend/modules/vote/templates/backend_vote/index.j2

@@ -0,0 +1,45 @@
+{% extends "layout/backend.j2" %}
+{% block content %}
+<h1>Votes</h1>
+<div class="row">
+    <div class="col-sm">
+        <div class="card">
+            <div class="card-header">
+                Votes
+                <div class="float-right">
+                    <a href="{{ url_for('backend_vote.create') }}" class="btn btn-secondary btn-sm">Create</a>
+                </div>
+            </div>
+            <div class="card-body table-responsive">
+                <table class="table table-striped table-sm">
+                    <thead>
+                        <tr>
+                            <th></th>
+                            <th>Name</th>
+                            <th>Start</th>
+                            <th>End</th>
+                            <th>Priority</th>
+                            <th>Author</th>
+                        </tr>
+                    </thead>
+                    <tbody>
+                        {% for ballot in ballots %}
+                        <tr>
+                            <td>
+                                <a href="{{ url_for('backend_vote.view', ballot_id=ballot.id) }}"><button class="btn btn-secondary btn-sm">View</button></a>
+                                <a href="{{ url_for('vote.view', ballot_id=ballot.id) }}"><button class="btn btn-secondary btn-sm">Public</button></a>
+                            </td>
+                            <td>{{ ballot.name }}</td>
+                            <td>{{ ballot.start_at }}</td>
+                            <td>{{ ballot.end_at }}</td>
+                            <td>{{ ballot.priority.name }}</td>
+                            <td>{{ ballot.user.name }}</td>
+                        </tr>
+                        {%- endfor -%}
+                    </tbody>
+                </table>
+            </div>
+        </div>
+    </div>
+</div>
+{% endblock %}

+ 84 - 0
app/modules/backend/modules/vote/templates/backend_vote/view.j2

@@ -0,0 +1,84 @@
+{% extends "layout/backend.j2" %}
+{% block content %}
+<div class="row">
+	<div class="col-12 col-md">
+		<h1>Vote: {{ ballot.name }}</h1>
+	</div>
+	<div class="col-auto mb-3 text-right">
+		<a href="{{ url_for('backend_vote.add_question', ballot_id=ballot.id) }}"><button class="btn btn-secondary btn-sm">Add question</button></a>
+		<a href="{{ url_for('vote.view', ballot_id=ballot.id) }}"><button class="btn btn-secondary btn-sm">Public</button></a>
+	</div>
+</div>
+<table class="table table-sm">
+	<tr>
+		<th scope="row">Description</th>
+		<td>{{ ballot.description }}</td>
+	</tr>
+	<tr>
+		<th scope="row">Start</th>
+		<td>{{ ballot.start_at }}</td>
+	</tr>
+	<tr>
+		<th scope="row">End</th>
+		<td>{{ ballot.end_at }}</td>
+	</tr>
+	{% if ballot.active() %}
+    <tr>
+        <th scope="row">Te gaan</th>
+        <td class="countdown" date="{{ ballot.end_at }}"><span class="hours">00</span>:<span class="minutes">00</span>:<span class="seconds">00</span></td>
+    </tr>
+    {% endif %}
+	<tr>
+		<th scope="row">User</th>
+		<td>{{ ballot.user.name }}</td>
+	</tr>
+	<tr>
+		<th scope="row">Priority</th>
+		<td>{{ ballot.priority.name }}</td>
+	</tr>
+</table>
+<div class="row">
+	{% for question in ballot.questions %}
+	<div class="col-md-6 mb-4">
+		<div class="card">
+			<div class="card-body">
+			    <h5 class="card-title">{{ question.name }}</h5>
+				{% if question.description %}
+			    <p class="card-text">{{ question.description }}</p>
+			    {% endif %}
+			    {% if question.combined_approval_voting %}
+			    <p class="card-text">Score: {{ question.score() }}</p>
+			    {% else %}
+			    <p class="card-text">Meeste stemmen: {{ question.score() }}</p>
+			    {% endif %}
+			</div>
+			{% if question.options.all() | count %}
+			<ul class="list-group list-group-flush">
+				{% for option in question.options %}
+				<li class="list-group-item d-flex justify-content-between align-items-center">
+					{{ option.name }}
+					<span class="badge badge-primary badge-pill">{{ option.votes.all() | count }}</span>
+				</li>
+				{% endfor %}
+			</ul>
+			{% else %}
+			<hr>
+			{% endif %}
+			{% if not question.combined_approval_voting %}
+			<div class="card-body">
+				<form class="row" method="post">
+					<input type="hidden" name="question_id" value="{{ question.id }}">
+					<div class="col">
+				    	<input type="text" class="form-control" name="name" placeholder="Option" required>
+					</div>
+					<div class="col-auto">
+					    <button type="submit" class="btn btn-primary">Add</button>
+					</div>
+				</form>
+			</div>
+			{% endif %}
+		</div>
+	</div>
+	{% endfor %}
+</div>
+{% endblock %}

+ 0 - 49
app/modules/backend/templates/layout/backend.j2

@@ -1,49 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <title>Backend</title>
-    <meta name="viewport" content="width=device-width, initial-scale=1">
-</head>
-<body>
-    <nav class="navbar navbar-expand-md navbar-dark bg-dark">
-        <a class="navbar-brand" href="/backend"><img src="/static/uploads/logo.png" style="height: 27px"></a>
-        <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarsDefault" aria-controls="navbarsDefault" aria-expanded="false" aria-label="Toggle navigation">
-            <span class="navbar-toggler-icon"></span>
-        </button>
-        <div class="collapse navbar-collapse" id="navbarsDefault">
-            <ul class="navbar-nav mr-auto">
-                {%- for item in current_menu.children recursive -%}
-                <li class="nav-item {{ 'active' if item.active }}" data-toggle="tooltip" data-placement="right" title="{{ item.text }}">
-                    <a class="nav-link" href="{{ item.url}}">
-                        <span class="nav-link-text">{{ item.text }}</span>
-                    </a>
-                </li>
-                {%- endfor -%}
-            </ul>
-            <ul class="navbar-nav">
-                <li class="nav-item text-nowrap">
-                      <a class="nav-link" href="/">Public</a>
-                </li>
-                <li class="nav-item text-nowrap">
-                      <a class="nav-link" href="/logout">Logout</a>
-                </li>
-            </ul>
-        </div>
-    </nav>
-    <main class="container mt-3">
-        {% with messages = get_flashed_messages(with_categories=true) %}
-        {% if messages %}
-        {% for category, message in messages %}
-        <div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
-            {{ message }}
-            <button type="button" class="close" data-dismiss="alert" aria-label="Close">
-                <span aria-hidden="true">&times;</span>
-            </button>
-        </div>
-        {% endfor %}
-        {% endif %}
-        {% endwith %}
-        {% block content %}{% endblock %}
-    </main>
-    <script src="/static/js/main.js"></script>
-</body>

+ 0 - 21
app/modules/backend/templates/layout/public.j2

@@ -1,21 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <title>{{ page.title }} - ssg</title>
-    <meta name="viewport" content="width=device-width, initial-scale=1">
-    <script src="/static/js/main.js"></script>
-</head>
-<body>
-    <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
-        <a class="navbar-brand" href="/"><img src="/static/uploads/logo.png" style="height: 27px"></a>
-        <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
-            <span class="navbar-toggler-icon"></span>
-        </button>
-        <div class="collapse navbar-collapse" id="navbarSupportedContent">
-            {% block nav %}{% endblock %}
-        </div>
-    </nav>
-    <div class="container mt-3">
-        {{ page.content() }}
-    </div>
-</body>

+ 1 - 1
app/modules/backend/templates/public/private.j2

@@ -1,4 +1,4 @@
-{% extends "layout/public.j2" %}
+{% extends "layout/page.j2" %}
 {% block nav %}
 <ul class="navbar-nav mr-auto">
     {% for menu_item in menu %}

+ 1 - 1
app/modules/backend/templates/public/public.j2

@@ -1,4 +1,4 @@
-{% extends "layout/public.j2" %}
+{% extends "layout/page.j2" %}
 {% block nav %}
 <ul class="navbar-nav mr-auto">
     {% for menu_item in menu %}

+ 6 - 0
app/modules/vote/__init__.py

@@ -0,0 +1,6 @@
+
+"""
+Website login page
+"""
+
+from .app import BLUEPRINT as Vote

+ 66 - 0
app/modules/vote/app.py

@@ -0,0 +1,66 @@
+
+"""
+Authentication module
+"""
+
+from flask import render_template, request, flash, Blueprint, redirect, url_for
+from app.models import User, Ballot, Question, Option, Vote, Code
+from app import db
+
+
+BLUEPRINT = Blueprint(
+    'vote',
+    __name__,
+    template_folder='templates'
+)
+
+
+@BLUEPRINT.route('/')
+def index():
+    """View list of votes"""
+    ballots = Ballot.query.all()
+
+    return render_template(
+        'vote/index.j2',
+        ballots=ballots,
+    )
+
+
+@BLUEPRINT.route('/<int:ballot_id>', methods=["GET", "POST"])
+def view(ballot_id):
+    """Vote and view results of ballot"""
+    ballot = Ballot.query.get(ballot_id)
+
+    if request.method == 'POST':
+        security_code = request.form['security_code']
+        code = Code.query.order_by(Code.expire_date.desc()).first()
+        user_id = None
+        for user in User.query.all():
+            if security_code == code.get_digest(user.id):
+                user_id = user.id
+
+        if user_id is not None:
+            for question_id, option_id in request.form.items():
+                if question_id == 'security_code':
+                    continue
+                question = Question.query.get(question_id)
+                if question.has_voten(user_id):
+                    flash('Je hebt al gestemd.', 'warning')
+                    return redirect(url_for('vote.view', ballot_id=ballot.id))
+
+                option = question.options.filter(Option.id == option_id).first()
+
+                vote = Vote()
+                vote.option_id = option.id
+                vote.user_id = user_id
+                db.session.add(vote)
+
+            db.session.commit()
+            flash('Succesvol gestemd.', 'success')
+        else:
+            flash('Fout in veiligheids code.', 'warning')
+
+    return render_template(
+        'vote/view.j2',
+        ballot=ballot,
+    )

+ 52 - 0
app/modules/vote/templates/vote/index.j2

@@ -0,0 +1,52 @@
+{% extends "layout/public.j2" %}
+{% block head %}
+<title>Votes - Democratic Assembly</title>
+{% endblock %}
+{% block nav %}
+<ul class="navbar-nav mr-auto">
+    <li class="nav-item" data-toggle="tooltip" data-placement="right" title="Votes">
+        <a class="nav-link" href="{{ url_for('vote.index') }}">Votes</a>
+    </li>
+</ul>
+{% endblock %}
+{% block content %}
+{% with messages = get_flashed_messages(with_categories=true) %}
+{% if messages %}
+{% for category, message in messages %}
+<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
+    {{ message }}
+    <button type="button" class="close" data-dismiss="alert" aria-label="Close">
+        <span aria-hidden="true">&times;</span>
+    </button>
+</div>
+{% endfor %}
+{% endif %}
+{% endwith %}
+<h1>Votes</h1>
+<div class="table-responsive">
+    <table class="table table-striped table-sm">
+        <thead>
+            <tr>
+                <th></th>
+                <th>Naam</th>
+                <th>Start</th>
+                <th>Eindigt</th>
+                <th>Aangemaakt door</th>
+            </tr>
+        </thead>
+        <tbody>
+            {% for ballot in ballots %}
+            <tr>
+                <td>
+                    <a href="{{ url_for('vote.view', ballot_id=ballot.id) }}"><button class="btn btn-secondary btn-sm">View</button></a>
+                </td>
+                <td>{{ ballot.name }}</td>
+                <td>{{ ballot.start_at }}</td>
+                <td>{{ ballot.end_at }}</td>
+                <td>{{ ballot.user.name }}</td>
+            </tr>
+            {%- endfor -%}
+        </tbody>
+    </table>
+</div>
+{% endblock %}

+ 113 - 0
app/modules/vote/templates/vote/view.j2

@@ -0,0 +1,113 @@
+{% extends "layout/public.j2" %}
+{% block head %}
+<title>{{ ballot.name }} - Democratic Assembly</title>
+{% endblock %}
+{% block nav %}
+<ul class="navbar-nav mr-auto">
+    <li class="nav-item" data-toggle="tooltip" data-placement="right" title="Votes">
+        <a class="nav-link" href="{{ url_for('vote.index') }}">Votes</a>
+    </li>
+</ul>
+{% endblock %}
+{% block content %}
+{% with messages = get_flashed_messages(with_categories=true) %}
+{% if messages %}
+{% for category, message in messages %}
+<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
+    {{ message }}
+    <button type="button" class="close" data-dismiss="alert" aria-label="Close">
+        <span aria-hidden="true">&times;</span>
+    </button>
+</div>
+{% endfor %}
+{% endif %}
+{% endwith %}
+<h1>{{ ballot.name }}</h1>
+<table class="table table-sm">
+    {% if ballot.description %}
+    <tr>
+        <th scope="row">Description</th>
+        <td>{{ ballot.description }}</td>
+    </tr>
+    {% endif %}
+    <tr>
+        <th scope="row">Start</th>
+        <td>{{ ballot.start_at }}</td>
+    </tr>
+    <tr>
+        <th scope="row">Eindigt</th>
+        <td>{{ ballot.end_at }}</td>
+    </tr>
+    {% if ballot.active() %}
+    <tr>
+        <th scope="row">Te gaan</th>
+        <td class="countdown" date="{{ ballot.end_at }}"><span class="hours">00</span>:<span class="minutes">00</span>:<span class="seconds">00</span></td>
+    </tr>
+    {% endif %}
+    <tr>
+        <th scope="row">Aangemaakt door</th>
+        <td>{{ ballot.user.name }}</td>
+    </tr>
+    {% if ballot.priority %}
+    <tr>
+        <th scope="row">Priority</th>
+        <td>{{ ballot.priority.name }}</td>
+    </tr>
+    {% endif %}
+</table>
+{% if ballot.active() %}
+<form method="post">
+    <div class="card mb-4">
+        <div class="card-body">
+            <h5 class="card-title">Controle code</h5>
+            <input type="text" class="form-control" name="security_code" placeholder="code" required>
+        </div>
+    </div>
+    {% for question in ballot.questions %}
+    <div class="card mb-4">
+        <div class="card-body">
+            <h5 class="card-title">{{ question.name }}</h5>
+            {% if question.description %}
+            <p class="card-text">{{ question.description }}</p>
+            {% endif %}
+            {% for option in question.options %}
+            <div class="form-check">
+                <input class="form-check-input" type="radio" name="{{ question.id }}" id="{{ "option_%s" % option.id }}" value="{{ option.id }}">
+                <label class="form-check-label" for="{{ "option_%s" % option.id }}">
+                    {{ option.name }}
+                </label>
+            </div>
+            {% endfor %}
+        </div>
+    </div>
+    {% endfor %}
+    <div class="form-group pull-right">
+        <button class="btn btn-primary" type="submit">Opslaan</button>
+    </div>
+</form>
+{% else %}
+{% for question in ballot.questions %}
+<div class="card mb-4">
+    <div class="card-body">
+        <h5 class="card-title">{{ question.name }}</h5>
+        {% if question.description %}
+        <p class="card-text">{{ question.description }}</p>
+        {% endif %}
+        {% if question.combined_approval_voting %}
+        <p class="card-text">Score: {{ question.score() }}</p>
+        {% else %}
+        <p class="card-text">Meeste stemmen: {{ question.score() }}</p>
+        {% endif %}
+    </div>
+    <ul class="list-group list-group-flush">
+        {% for option in question.options %}
+        <li class="list-group-item d-flex justify-content-between align-items-center">
+            {{ option.name }}
+            <span class="badge badge-primary badge-pill">{{ option.votes.all() | count }}</span>
+        </li>
+        {% endfor %}
+    </ul>
+</div>
+{% endfor %}
+{% endif %}
+{% endblock %}

+ 9 - 0
app/static/css/index.css

@@ -0,0 +1,9 @@
+.min-vh-75
+{
+	min-height: 75vh !important;
+}
+
+.background
+{
+	background: url('/static/uploads/background.jpg') no-repeat center center fixed; background-size: cover;
+}

+ 55 - 5
app/static/js/index.js

@@ -1,9 +1,59 @@
-// import 'bootstrap';
-
+// Import Bootstrap
 import 'bootstrap'
 import 'bootstrap/dist/css/bootstrap.min.css'
 
-import SimpleMDE from 'simplemde';
-import 'simplemde/dist/simplemde.min.css';
+// Import own css
+import '../css/index.css'
+
+// Import SimpleMDE
+import SimpleMDE from 'simplemde'
+import 'simplemde/dist/simplemde.min.css'
+
+if (document.querySelector('textarea') != null) {
+  new SimpleMDE()
+}
+
+// Javascript
+function countdown() {
+  let element = document.querySelector(".countdown")
+  let date = element.getAttribute('date');
+  let days, hours, minutes, seconds;
+  
+  let endDate = new Date(date).getTime();
+  if (isNaN(endDate)) {
+    return;
+  }
+
+  let startDate = new Date().getTime();
+  if (endDate > startDate) {
+    calculate()
+    setInterval(calculate, 1000);
+  }
+
+  function calculate() {
+    let startDate = new Date().getTime();
+    let timeRemaining = parseInt((endDate - startDate) / 1000);
+    
+    if (timeRemaining >= 0) {
+      hours = parseInt(timeRemaining / 3600);
+      timeRemaining = (timeRemaining % 3600);
+      
+      minutes = parseInt(timeRemaining / 60);
+      timeRemaining = (timeRemaining % 60);
+      
+      seconds = parseInt(timeRemaining);
+      
+      document.querySelector(".countdown .hours").innerHTML = ("0" + hours).slice(-2);
+      document.querySelector(".countdown .minutes").innerHTML = ("0" + minutes).slice(-2);
+      document.querySelector(".countdown .seconds").innerHTML = ("0" + seconds).slice(-2);
+    } else {
+      window.location.reload(false); 
+    }
+  }
+}
 
-new SimpleMDE();
+window.onload = function () {
+  if (document.querySelector('.countdown') != null) {
+    countdown();
+  }
+}

+ 51 - 0
app/templates/layout/backend.j2

@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <title>Backend</title>
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+</head>
+<body>
+    <nav class="navbar navbar-expand-md navbar-dark bg-dark">
+        <div class="container">
+            <a class="navbar-brand" href="/backend"><img src="/static/uploads/logo.png" alt="DA" style="height: 27px"></a>
+            <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarsDefault" aria-controls="navbarsDefault" aria-expanded="false" aria-label="Toggle navigation">
+                <span class="navbar-toggler-icon"></span>
+            </button>
+            <div class="collapse navbar-collapse" id="navbarsDefault">
+                <ul class="navbar-nav mr-auto">
+                    {%- for item in current_menu.children recursive -%}
+                    <li class="nav-item {{ 'active' if item.active }}" data-toggle="tooltip" data-placement="right" title="{{ item.text }}">
+                        <a class="nav-link" href="{{ item.url}}">
+                            <span class="nav-link-text">{{ item.text }}</span>
+                        </a>
+                    </li>
+                    {%- endfor -%}
+                </ul>
+                <ul class="navbar-nav">
+                    <li class="nav-item text-nowrap">
+                          <a class="nav-link" href="/">Public</a>
+                    </li>
+                    <li class="nav-item text-nowrap">
+                          <a class="nav-link" href="/logout">Logout</a>
+                    </li>
+                </ul>
+            </div>
+        </div>
+    </nav>
+    <main class="container mt-3">
+        {% with messages = get_flashed_messages(with_categories=true) %}
+        {% if messages %}
+        {% for category, message in messages %}
+        <div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
+            {{ message }}
+            <button type="button" class="close" data-dismiss="alert" aria-label="Close">
+                <span aria-hidden="true">&times;</span>
+            </button>
+        </div>
+        {% endfor %}
+        {% endif %}
+        {% endwith %}
+        {% block content %}{% endblock %}
+    </main>
+    <script src="/static/js/main.js"></script>
+</body>

+ 7 - 0
app/templates/layout/page.j2

@@ -0,0 +1,7 @@
+{% extends "layout/public.j2" %}
+{% block head %}
+<title>{{ page.title }} - Democratic Assembly</title>
+{% endblock %}
+{% block content %}
+{{ page.content() }}
+{% endblock %}

+ 23 - 0
app/templates/layout/public.j2

@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<html class="vh-100" lang="nl">
+<head>
+    {% block head %}{% endblock %}
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+    <script src="/static/js/main.js"></script>
+</head>
+<body class="vh-100 background">
+    <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
+        <div class="container">
+            <a class="navbar-brand" href="/"><img src="/static/uploads/logo.png" alt="DA" style="height: 27px"></a>
+            <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
+                <span class="navbar-toggler-icon"></span>
+            </button>
+            <div class="collapse navbar-collapse" id="navbarSupportedContent">
+                {% block nav %}{% endblock %}
+            </div>
+        </div>
+    </nav>
+    <div class="container p-3 bg-white min-vh-75">
+        {% block content %}{% endblock %}
+    </div>
+</body>

+ 34 - 0
migrations/versions/3d9ba6086a23_improvin_voting_models.py

@@ -0,0 +1,34 @@
+"""improvin_voting_models
+
+Revision ID: 3d9ba6086a23
+Revises: c98e920c06ae
+Create Date: 2019-03-30 13:52:32.070847
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = '3d9ba6086a23'
+down_revision = 'c98e920c06ae'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.add_column('option', sa.Column('description', sa.String(), nullable=True))
+    op.add_column('option', sa.Column('name', sa.String(), nullable=True))
+    op.drop_column('option', 'motivation')
+    op.add_column('question', sa.Column('combined_approval_voting', sa.Boolean(), server_default='f', nullable=True))
+    # ### end Alembic commands ###
+
+
+def downgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.drop_column('question', 'combined_approval_voting')
+    op.add_column('option', sa.Column('motivation', sa.VARCHAR(), autoincrement=False, nullable=True))
+    op.drop_column('option', 'name')
+    op.drop_column('option', 'description')
+    # ### end Alembic commands ###

+ 30 - 0
migrations/versions/c1d7ddb101a1_add_code.py

@@ -0,0 +1,30 @@
+"""add_code
+
+Revision ID: c1d7ddb101a1
+Revises: 3d9ba6086a23
+Create Date: 2019-04-01 17:23:32.312946
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = 'c1d7ddb101a1'
+down_revision = '3d9ba6086a23'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+    op.create_table('code',
+    sa.Column('id', sa.Integer(), nullable=False),
+    sa.Column('expire_date', sa.DateTime(), nullable=True),
+    sa.Column('secret', sa.String(length=255), nullable=True),
+    sa.PrimaryKeyConstraint('id', name=op.f('pk_code')),
+    sa.UniqueConstraint('secret', name=op.f('uq_code_secret'))
+    )
+
+
+def downgrade():
+    op.drop_table('code')

+ 101 - 0
migrations/versions/c98e920c06ae_add_ballots_remove_page_file_relation.py

@@ -0,0 +1,101 @@
+"""add_ballots_remove_page_file_relation
+
+Revision ID: c98e920c06ae
+Revises: d692d6e1a31d
+Create Date: 2019-03-29 17:49:34.434786
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = 'c98e920c06ae'
+down_revision = 'd692d6e1a31d'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+    op.create_table('priority',
+    sa.Column('id', sa.Integer(), nullable=False),
+    sa.Column('name', sa.String(), nullable=False),
+    sa.Column('description', sa.String(), nullable=False),
+    sa.PrimaryKeyConstraint('id', name=op.f('pk_priority'))
+    )
+    op.create_table('ballot',
+    sa.Column('id', sa.Integer(), nullable=False),
+    sa.Column('name', sa.String(), nullable=False),
+    sa.Column('description', sa.String(), nullable=True),
+    sa.Column('start_at', sa.DateTime(), nullable=True),
+    sa.Column('end_at', sa.DateTime(), nullable=False),
+    sa.Column('user_id', sa.Integer(), nullable=True),
+    sa.Column('priority_id', sa.Integer(), nullable=True),
+    sa.ForeignKeyConstraint(['priority_id'], ['priority.id'], name=op.f('fk_ballot_priority_id_priority')),
+    sa.ForeignKeyConstraint(['user_id'], ['user.id'], name=op.f('fk_ballot_user_id_user')),
+    sa.PrimaryKeyConstraint('id', name=op.f('pk_ballot'))
+    )
+    op.create_table('question',
+    sa.Column('id', sa.Integer(), nullable=False),
+    sa.Column('name', sa.String(), nullable=False),
+    sa.Column('description', sa.String(), nullable=True),
+    sa.Column('ballot_id', sa.Integer(), nullable=True),
+    sa.ForeignKeyConstraint(['ballot_id'], ['ballot.id'], name=op.f('fk_question_ballot_id_ballot')),
+    sa.PrimaryKeyConstraint('id', name=op.f('pk_question'))
+    )
+    op.create_table('option',
+    sa.Column('id', sa.Integer(), nullable=False),
+    sa.Column('motivation', sa.String(), nullable=True),
+    sa.Column('user_id', sa.Integer(), nullable=True),
+    sa.Column('question_id', sa.Integer(), nullable=True),
+    sa.ForeignKeyConstraint(['question_id'], ['question.id'], name=op.f('fk_option_question_id_question')),
+    sa.ForeignKeyConstraint(['user_id'], ['user.id'], name=op.f('fk_option_user_id_user')),
+    sa.PrimaryKeyConstraint('id', name=op.f('pk_option'))
+    )
+    op.create_table('vote',
+    sa.Column('id', sa.Integer(), nullable=False),
+    sa.Column('datetime', sa.DateTime(), nullable=True),
+    sa.Column('option_id', sa.Integer(), nullable=True),
+    sa.Column('user_id', sa.Integer(), nullable=True),
+    sa.ForeignKeyConstraint(['option_id'], ['option.id'], name=op.f('fk_vote_option_id_option')),
+    sa.ForeignKeyConstraint(['user_id'], ['user.id'], name=op.f('fk_vote_user_id_user')),
+    sa.PrimaryKeyConstraint('id', name=op.f('pk_vote'))
+    )
+    op.drop_table('page_file')
+    op.add_column('user', sa.Column('alt', sa.Boolean(), server_default='f', nullable=True))
+    op.add_column('user', sa.Column('discord', sa.String(length=255), nullable=True))
+    op.add_column('user', sa.Column('game_id', sa.BigInteger(), nullable=True))
+    op.add_column('user', sa.Column('party_member', sa.Boolean(), server_default='f', nullable=True))
+    op.add_column('user', sa.Column('question_id', sa.Integer(), nullable=True))
+    op.create_unique_constraint(op.f('uq_user_discord'), 'user', ['discord'])
+    op.create_unique_constraint(op.f('uq_user_email'), 'user', ['email'])
+    op.create_unique_constraint(op.f('uq_user_game_id'), 'user', ['game_id'])
+    op.create_unique_constraint(op.f('uq_user_name'), 'user', ['name'])
+    op.drop_constraint('user_name_key', 'user', type_='unique')
+    op.create_foreign_key(op.f('fk_user_question_id_question'), 'user', 'question', ['question_id'], ['id'])
+
+
+def downgrade():
+    op.drop_constraint(op.f('fk_user_question_id_question'), 'user', type_='foreignkey')
+    op.create_unique_constraint('user_name_key', 'user', ['name'])
+    op.drop_constraint(op.f('uq_user_name'), 'user', type_='unique')
+    op.drop_constraint(op.f('uq_user_game_id'), 'user', type_='unique')
+    op.drop_constraint(op.f('uq_user_email'), 'user', type_='unique')
+    op.drop_constraint(op.f('uq_user_discord'), 'user', type_='unique')
+    op.drop_column('user', 'question_id')
+    op.drop_column('user', 'party_member')
+    op.drop_column('user', 'game_id')
+    op.drop_column('user', 'discord')
+    op.drop_column('user', 'alt')
+    op.create_table('page_file',
+    sa.Column('page_id', sa.INTEGER(), autoincrement=False, nullable=False),
+    sa.Column('file_id', sa.INTEGER(), autoincrement=False, nullable=False),
+    sa.ForeignKeyConstraint(['file_id'], ['file.id'], name='page_file_file_id_fkey'),
+    sa.ForeignKeyConstraint(['page_id'], ['page.id'], name='page_file_page_id_fkey'),
+    sa.PrimaryKeyConstraint('page_id', 'file_id', name='page_file_pkey')
+    )
+    op.drop_table('vote')
+    op.drop_table('option')
+    op.drop_table('question')
+    op.drop_table('ballot')
+    op.drop_table('priority')

+ 19 - 19
webpack.config.js

@@ -3,11 +3,11 @@ const webpack = require('webpack');
 
 module.exports = {
     mode: 'development',
-	entry: './app/static/js/index.js',
+    entry: './app/static/js/index.js',
     output: {
         filename: 'main.js',
         path: path.resolve(__dirname, 'app/static/js'),
-		publicPath: 'static/js'
+        publicPath: 'static/js'
     },
     plugins: [
         new webpack.ProvidePlugin({
@@ -21,25 +21,25 @@ module.exports = {
         }
     },
     module: {
-      rules: [
-        {
-          test: /\.js$/,
-          exclude: /node_modules/,
-          use: {
-            loader: 'babel-loader',
-            options: {
-              presets: ['@babel/preset-env']
+        rules: [
+            {
+                test: /\.js$/,
+                exclude: /node_modules/,
+                use: {
+                    loader: 'babel-loader',
+                    options: {
+                        presets: ['@babel/preset-env']
+                    }
+                }
             }
-          }
-        }
-      ]
+        ]
     },
     module: {
-      rules: [
-        {
-          test: /\.css$/,
-          use: ['style-loader', 'css-loader']
-        }
-      ]
+        rules: [
+            {
+                test: /\.css$/,
+                use: ['style-loader', 'css-loader']
+            }
+        ]
     }
 };