From 0d2f27f7718a1a31a0dd49cb336526fa3ab6f2cc Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Wed, 8 Jan 2025 22:31:20 +0100 Subject: [PATCH] import from kontor-flask --- flask/config.py | 47 ++ flask/docs/Makefile | 20 + flask/docs/conf.py | 200 +++++++++ flask/docs/index.rst | 20 + flask/kontor/__init__.py | 74 ++++ flask/kontor/admin/__init__.py | 4 + flask/kontor/admin/forms.py | 17 + flask/kontor/admin/views.py | 63 +++ flask/kontor/auth/__init__.py | 4 + flask/kontor/auth/forms.py | 50 +++ flask/kontor/auth/views.py | 79 ++++ flask/kontor/comics/__init__.py | 3 + flask/kontor/comics/forms.py | 31 ++ flask/kontor/comics/models.py | 51 +++ flask/kontor/comics/views.py | 222 ++++++++++ flask/kontor/home/__init__.py | 3 + flask/kontor/home/views.py | 39 ++ flask/kontor/library/__init__.py | 3 + flask/kontor/library/forms.py | 35 ++ flask/kontor/library/models.py | 55 +++ flask/kontor/library/views.py | 228 ++++++++++ flask/kontor/models.py | 66 +++ flask/kontor/office/__init__.py | 3 + flask/kontor/static/css/style.css | 126 ++++++ flask/kontor/templates/admin/users/user.html | 21 + flask/kontor/templates/admin/users/users.html | 58 +++ flask/kontor/templates/auth/login.html | 18 + flask/kontor/templates/auth/register.html | 14 + flask/kontor/templates/base.html | 107 +++++ flask/kontor/templates/comics/artists.html | 58 +++ flask/kontor/templates/comics/comics.html | 66 +++ flask/kontor/templates/comics/publishers.html | 58 +++ .../templates/home/admin_dashboard.html | 31 ++ flask/kontor/templates/home/dashboard.html | 20 + flask/kontor/templates/home/index.html | 20 + flask/kontor/templates/library/authors.html | 58 +++ flask/kontor/templates/library/books.html | 74 ++++ .../kontor/templates/library/publishers.html | 58 +++ flask/kontor/templates/simpleform.html | 21 + .../kontor/templates/tysc/manufacturers.html | 58 +++ flask/kontor/templates/tysc/players.html | 60 +++ flask/kontor/templates/tysc/positions.html | 62 +++ flask/kontor/templates/tysc/sports.html | 58 +++ flask/kontor/templates/tysc/teams.html | 60 +++ flask/kontor/tysc/__init__.py | 377 ++++++++++++++++ flask/kontor/tysc/forms.py | 76 ++++ flask/kontor/tysc/models.py | 155 +++++++ flask/kontor/tysc/views.py | 405 ++++++++++++++++++ flask/kontor/version.py | 2 + flask/pylint.cfg | 382 +++++++++++++++++ flask/requirements.txt | 11 + flask/tests/__init__.py | 38 ++ flask/tests/test_comics_model.py | 51 +++ flask/tests/test_comics_view.py | 44 ++ flask/tests/test_config.txt | 4 + flask/tests/test_kontor_model.py | 14 + flask/tests/test_kontor_view.py | 58 +++ flask/tests/test_library_model.py | 49 +++ flask/tests/test_library_view.py | 44 ++ flask/tests/test_tysc_model.py | 53 +++ flask/tests/test_tysc_view.py | 110 +++++ 61 files changed, 4296 insertions(+) create mode 100644 flask/config.py create mode 100644 flask/docs/Makefile create mode 100644 flask/docs/conf.py create mode 100644 flask/docs/index.rst create mode 100644 flask/kontor/__init__.py create mode 100644 flask/kontor/admin/__init__.py create mode 100644 flask/kontor/admin/forms.py create mode 100644 flask/kontor/admin/views.py create mode 100644 flask/kontor/auth/__init__.py create mode 100644 flask/kontor/auth/forms.py create mode 100644 flask/kontor/auth/views.py create mode 100644 flask/kontor/comics/__init__.py create mode 100644 flask/kontor/comics/forms.py create mode 100644 flask/kontor/comics/models.py create mode 100644 flask/kontor/comics/views.py create mode 100644 flask/kontor/home/__init__.py create mode 100644 flask/kontor/home/views.py create mode 100644 flask/kontor/library/__init__.py create mode 100644 flask/kontor/library/forms.py create mode 100644 flask/kontor/library/models.py create mode 100644 flask/kontor/library/views.py create mode 100644 flask/kontor/models.py create mode 100644 flask/kontor/office/__init__.py create mode 100644 flask/kontor/static/css/style.css create mode 100644 flask/kontor/templates/admin/users/user.html create mode 100644 flask/kontor/templates/admin/users/users.html create mode 100644 flask/kontor/templates/auth/login.html create mode 100644 flask/kontor/templates/auth/register.html create mode 100644 flask/kontor/templates/base.html create mode 100644 flask/kontor/templates/comics/artists.html create mode 100644 flask/kontor/templates/comics/comics.html create mode 100644 flask/kontor/templates/comics/publishers.html create mode 100644 flask/kontor/templates/home/admin_dashboard.html create mode 100644 flask/kontor/templates/home/dashboard.html create mode 100644 flask/kontor/templates/home/index.html create mode 100644 flask/kontor/templates/library/authors.html create mode 100644 flask/kontor/templates/library/books.html create mode 100644 flask/kontor/templates/library/publishers.html create mode 100644 flask/kontor/templates/simpleform.html create mode 100644 flask/kontor/templates/tysc/manufacturers.html create mode 100644 flask/kontor/templates/tysc/players.html create mode 100644 flask/kontor/templates/tysc/positions.html create mode 100644 flask/kontor/templates/tysc/sports.html create mode 100644 flask/kontor/templates/tysc/teams.html create mode 100644 flask/kontor/tysc/__init__.py create mode 100644 flask/kontor/tysc/forms.py create mode 100644 flask/kontor/tysc/models.py create mode 100644 flask/kontor/tysc/views.py create mode 100644 flask/kontor/version.py create mode 100644 flask/pylint.cfg create mode 100644 flask/requirements.txt create mode 100644 flask/tests/__init__.py create mode 100644 flask/tests/test_comics_model.py create mode 100644 flask/tests/test_comics_view.py create mode 100644 flask/tests/test_config.txt create mode 100644 flask/tests/test_kontor_model.py create mode 100644 flask/tests/test_kontor_view.py create mode 100644 flask/tests/test_library_model.py create mode 100644 flask/tests/test_library_view.py create mode 100644 flask/tests/test_tysc_model.py create mode 100644 flask/tests/test_tysc_view.py diff --git a/flask/config.py b/flask/config.py new file mode 100644 index 0000000..4ea5e78 --- /dev/null +++ b/flask/config.py @@ -0,0 +1,47 @@ +# config.py + + +class Config(object): + """ + Common configurations + """ + + # Put any configurations here that are common across all environments + host = '127.0.0.1' + port = 8500 + database = 'kontor' + + +class DevelopmentConfig(Config): + """ + Development configurations + """ + + DEBUG = True + host = '0.0.0.0' + database = 'kontor_dev' + + +class ProductionConfig(Config): + """ + Production configurations + """ + + DEBUG = False + +class TestingConfig(Config): + """ + Testing configurations + """ + + TESTING = True + DEBUG = True + port = 8600 + database = 'kontor_test' + +app_config = { + 'development': DevelopmentConfig, + 'production': ProductionConfig, + 'testing': TestingConfig +} + diff --git a/flask/docs/Makefile b/flask/docs/Makefile new file mode 100644 index 0000000..d7218b4 --- /dev/null +++ b/flask/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +SPHINXPROJ = KontorFlask +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file diff --git a/flask/docs/conf.py b/flask/docs/conf.py new file mode 100644 index 0000000..7278f2d --- /dev/null +++ b/flask/docs/conf.py @@ -0,0 +1,200 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Kontor Flask documentation build configuration file, created by +# sphinx-quickstart on Mon Nov 6 08:53:04 2017. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +# import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) + + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = ['sphinx.ext.autodoc', + 'sphinx.ext.doctest', + 'sphinx.ext.intersphinx', + 'sphinx.ext.todo', + 'sphinx.ext.coverage', + 'sphinx.ext.mathjax', + 'sphinx.ext.ifconfig', + 'sphinx.ext.viewcode'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = 'Kontor Flask' +copyright = '2017, Thomas Peetz' +author = 'Thomas Peetz' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = '0.1' +# The full version, including alpha/beta/rc tags. +release = '0.0.7' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = 'de' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This patterns also effect to html_static_path and html_extra_path +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = True + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'alabaster' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Custom sidebar templates, must be a dictionary that maps document names +# to template names. +# +# This is required for the alabaster theme +# refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars +html_sidebars = { + '**': [ + 'relations.html', # needs 'show_related': True theme option to display + 'searchbox.html', + ] +} + + +# -- Options for HTMLHelp output ------------------------------------------ + +# Output file base name for HTML help builder. +htmlhelp_basename = 'KontorFlaskdoc' + + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'KontorFlask.tex', 'Kontor Flask Documentation', + 'Thomas Peetz', 'manual'), +] + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'kontorflask', 'Kontor Flask Documentation', + [author], 1) +] + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'KontorFlask', 'Kontor Flask Documentation', + author, 'KontorFlask', 'One line description of project.', + 'Miscellaneous'), +] + + + +# -- Options for Epub output ---------------------------------------------- + +# Bibliographic Dublin Core info. +epub_title = project +epub_author = author +epub_publisher = author +epub_copyright = copyright + +# The unique identifier of the text. This can be a ISBN number +# or the project homepage. +# +# epub_identifier = '' + +# A unique identification for the text. +# +# epub_uid = '' + +# A list of files that should not be packed into the epub file. +epub_exclude_files = ['search.html'] + + + +# Example configuration for intersphinx: refer to the Python standard library. +intersphinx_mapping = {'https://docs.python.org/': None} diff --git a/flask/docs/index.rst b/flask/docs/index.rst new file mode 100644 index 0000000..ca4df98 --- /dev/null +++ b/flask/docs/index.rst @@ -0,0 +1,20 @@ +.. Kontor Flask documentation master file, created by + sphinx-quickstart on Mon Nov 6 08:53:04 2017. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to Kontor Flask's documentation! +======================================== + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/flask/kontor/__init__.py b/flask/kontor/__init__.py new file mode 100644 index 0000000..961e132 --- /dev/null +++ b/flask/kontor/__init__.py @@ -0,0 +1,74 @@ +""" +Module kontor implements Kontor application. +""" + +from flask import Flask +from flask_bootstrap import Bootstrap +from flask_login import LoginManager +from pymodm import connect + +# local imports +from config import app_config +from .version import __version__ + + +_LOGIN_MANAGER_ = LoginManager() + + +def get_host(config_name): + """ + Returns host address from configuration. + :param config_name: + :return: host address + """ + host = app_config[config_name].host + return host + + +def get_port(config_name): + """ + Returns port number from configuration. + :param config_name: + :return: port number + """ + port = app_config[config_name].port + return port + + +def create_app(config_name): + """ + Returns Flask application. + :param config_name: + :return: Flask application + """ + app = Flask(__name__, instance_relative_config=True) + app.config.from_object(app_config[config_name]) + app.config.from_pyfile('config.py') + + database = "mongodb://localhost:27017/{}".format(app_config[config_name].database) + connect(database, alias="kontor") + + Bootstrap(app) + _LOGIN_MANAGER_.init_app(app) + _LOGIN_MANAGER_.login_message = "You must be logged in to access this page." + _LOGIN_MANAGER_.login_view = "auth.login" + + from .admin.views import ADMIN as ADMIN_BLUEPRINT + app.register_blueprint(ADMIN_BLUEPRINT, url_prefix='/admin') + + from .auth.views import AUTH as AUTH_BLUEPRINT + app.register_blueprint(AUTH_BLUEPRINT) + + from .home.views import HOME as HOME_BLUEPRINT + app.register_blueprint(HOME_BLUEPRINT) + + from .comics.views import COMIC as COMIC_BLUEPRINT + app.register_blueprint(COMIC_BLUEPRINT, url_prefix='/comics') + + from .library.views import LIBRARY as LIBRARY_BLUEPRINT + app.register_blueprint(LIBRARY_BLUEPRINT, url_prefix='/library') + + from .tysc.views import TYSC as TYSC_BLUEPRINT + app.register_blueprint(TYSC_BLUEPRINT, url_prefix='/tysc') + + return app diff --git a/flask/kontor/admin/__init__.py b/flask/kontor/admin/__init__.py new file mode 100644 index 0000000..ca00a4b --- /dev/null +++ b/flask/kontor/admin/__init__.py @@ -0,0 +1,4 @@ +""" +Module admin implements administration functions. +""" +from . import views diff --git a/flask/kontor/admin/forms.py b/flask/kontor/admin/forms.py new file mode 100644 index 0000000..d05cfaf --- /dev/null +++ b/flask/kontor/admin/forms.py @@ -0,0 +1,17 @@ +""" +Define form to edit users +""" +from flask_wtf import FlaskForm +from wtforms import StringField, SubmitField +from wtforms.validators import DataRequired + + +class UserEditForm(FlaskForm): + """ + Form for admin to edit users + """ + email = StringField('Email', validators=[DataRequired()]) + username = StringField('Username', validators=[DataRequired()]) + first_name = StringField('First Name', validators=[DataRequired()]) + last_name = StringField('Last Name', validators=[DataRequired()]) + submit = SubmitField('Submit') diff --git a/flask/kontor/admin/views.py b/flask/kontor/admin/views.py new file mode 100644 index 0000000..e96d8f0 --- /dev/null +++ b/flask/kontor/admin/views.py @@ -0,0 +1,63 @@ +""" +Define routing rules for managing users +""" +from flask import abort, Blueprint, flash, redirect, render_template, url_for +from flask_login import current_user, login_required + +from .forms import UserEditForm +from ..models import User + +ADMIN = Blueprint('admin', __name__) + + +def check_admin(): + """ + Prevent non-admins from accessing the page + """ + if not current_user.is_admin: + abort(403) + + +# User Views +@ADMIN.route('/users') +@login_required +def list_users(): + """ + List all users + """ + check_admin() + + users = User.objects.all() + return render_template('admin/users/users.html', + users=users, title='Users') + + +@ADMIN.route('/users/edit/', methods=['GET', 'POST']) +@login_required +def edit_user(user_id): + """ + Edit an user. + """ + check_admin() + + user = User.objects.get({'username': user_id}) + + # prevent admin from being assigned a department or role + if user.is_admin: + abort(403) + + form = UserEditForm(obj=user) + if form.validate_on_submit(): + user.email = form.email.data + user.username = form.username.data + user.first_name = form.first_name.data + user.last_name = form.last_name.data + user.save() + flash('You have successfully edited an user.') + + # redirect to the roles page + return redirect(url_for('admin.list_users')) + + return render_template('admin/users/user.html', + user=user, form=form, + title='Edit User') diff --git a/flask/kontor/auth/__init__.py b/flask/kontor/auth/__init__.py new file mode 100644 index 0000000..2d9b1ca --- /dev/null +++ b/flask/kontor/auth/__init__.py @@ -0,0 +1,4 @@ +""" +Module auth implements authentication functions. +""" +from . import views diff --git a/flask/kontor/auth/forms.py b/flask/kontor/auth/forms.py new file mode 100644 index 0000000..6e4d6dd --- /dev/null +++ b/flask/kontor/auth/forms.py @@ -0,0 +1,50 @@ +""" +Contains forms for authentication. +""" +from flask_wtf import FlaskForm +from wtforms import PasswordField, StringField, SubmitField, ValidationError +from wtforms.validators import DataRequired, Email, EqualTo + +from ..models import User + + +class RegistrationForm(FlaskForm): + """ + Form for users to create new account + """ + email = StringField('Email', validators=[DataRequired(), Email()]) + username = StringField('Username', validators=[DataRequired()]) + first_name = StringField('First Name', validators=[DataRequired()]) + last_name = StringField('Last Name', validators=[DataRequired()]) + password = PasswordField('Password', validators=[DataRequired(), + EqualTo('confirm_password') + ]) + confirm_password = PasswordField('Confirm Password') + submit = SubmitField('Register') + + def validate_email(self, field): + """ + Check if email is already in use. + :param field: + :return: + """ + if User.objects.raw({'email': field.data}).count() > 0: + raise ValidationError('Email is already in use.') + + def validate_username(self, field): + """ + check if username is already in use. + :param field: + :return: + """ + if User.objects.raw({'username': field.data}).count() > 0: + raise ValidationError('Username is already in use.') + + +class LoginForm(FlaskForm): + """ + Form for users to login + """ + email = StringField('Email', validators=[DataRequired(), Email()]) + password = PasswordField('Password', validators=[DataRequired()]) + submit = SubmitField('Login') diff --git a/flask/kontor/auth/views.py b/flask/kontor/auth/views.py new file mode 100644 index 0000000..217ae18 --- /dev/null +++ b/flask/kontor/auth/views.py @@ -0,0 +1,79 @@ +""" +Define routing rules for registering users and login and logout. +""" +from flask import Blueprint, flash, redirect, render_template, url_for +from flask_login import login_required, login_user, logout_user +from .forms import LoginForm, RegistrationForm +from ..models import User + +AUTH = Blueprint('auth', __name__) + + +@AUTH.route('/register', methods=['GET', 'POST']) +def register(): + """ + Handle requests to the /register route + Add an employee to the database through the registration form + """ + form = RegistrationForm() + if form.validate_on_submit(): + user = User() + user.email = form.email.data + user.username = form.username.data + user.first_name = form.first_name.data + user.last_name = form.last_name.data + user.password = form.password.data + # add employee to the database + user.save() + flash('You have successfully registered! You may now login.') + + # redirect to the login page + return redirect(url_for('auth.login')) + + # load registration template + return render_template('auth/register.html', form=form, title='Register') + + +@AUTH.route('/login', methods=['GET', 'POST']) +def login(): + """ + Handle requests to the /login route + Validate given email with matching password + :return: + """ + form = LoginForm() + if form.validate_on_submit(): + + # check whether employee exists in the database and whether + # the password entered matches the password in the database + user = User.objects.get({'email': form.email.data}) + if user is not None and user.verify_password( + form.password.data): + # log employee in + login_user(user) + + # redirect to the appropriate dashboard page + if user.is_admin: + return redirect(url_for('home.admin_dashboard')) + else: + return redirect(url_for('home.dashboard')) + + # when login details are incorrect + else: + flash('Invalid email or password.') + + # load login template + return render_template('auth/login.html', form=form, title='Login') + +@AUTH.route('/logout') +@login_required +def logout(): + """ + Handle requests to the /logout route + Log an employee out through the logout link + """ + logout_user() + flash('You have successfully been logged out.') + + # redirect to the login page + return redirect(url_for('auth.login')) diff --git a/flask/kontor/comics/__init__.py b/flask/kontor/comics/__init__.py new file mode 100644 index 0000000..e947d82 --- /dev/null +++ b/flask/kontor/comics/__init__.py @@ -0,0 +1,3 @@ +""" +Define routing rules for comic related information +""" diff --git a/flask/kontor/comics/forms.py b/flask/kontor/comics/forms.py new file mode 100644 index 0000000..0bae45f --- /dev/null +++ b/flask/kontor/comics/forms.py @@ -0,0 +1,31 @@ +"""Define form to edit publisher, artists and comics""" +from flask_wtf import FlaskForm +from wtforms import StringField, SubmitField, BooleanField, SelectField +from wtforms.validators import DataRequired +from bson import ObjectId + + +class PublisherForm(FlaskForm): + """ + Form to add and edit a Comic publisher + """ + name = StringField('Name', validators=[DataRequired()]) + submit = SubmitField('Submit') + + +class ArtistForm(FlaskForm): + """ + Form to add and edit a Comic publisher + """ + name = StringField('Name', validators=[DataRequired()]) + submit = SubmitField('Submit') + + +class ComicForm(FlaskForm): + """ + Form to add and edit Comics + """ + title = StringField('Title', validators=[DataRequired()]) + publisher = SelectField('Publisher', coerce=ObjectId) + current_order = BooleanField('Current Order') + submit = SubmitField('Submit') diff --git a/flask/kontor/comics/models.py b/flask/kontor/comics/models.py new file mode 100644 index 0000000..e39e744 --- /dev/null +++ b/flask/kontor/comics/models.py @@ -0,0 +1,51 @@ +"""This modules declares the model for Comic related information.""" + +from flask import current_app +from pymongo.write_concern import WriteConcern +from pymodm import MongoModel, fields + + +class Publisher(MongoModel): + """Class Publisher represents a publisher of a comic.""" + name = fields.CharField() + + def __str__(self): + return "Publisher({})".format(self.name) + + @property + def comics(self): + """ + Return list of comics which has reference to this publisher + :return: + """ + comics = Comic.objects.raw({'publisher': self.pk}) + current_app.logger.debug(comics) + return comics + + class Meta: + """Sets the connection and connections details.""" + connection_alias = 'kontor' + write_concern = WriteConcern(j=True) + + +class Artist(MongoModel): + """Class Artist represents a comic artist.""" + name = fields.CharField() + + class Meta: + """Sets the connection and connections details.""" + connection_alias = 'kontor' + write_concern = WriteConcern(j=True) + + +class Comic(MongoModel): + """Class Comic represents a comic.""" + title = fields.CharField() + publisher = fields.ReferenceField(Publisher) + current_order = fields.BooleanField(default=False) + completed = fields.BooleanField(default=False) + + class Meta: + """Sets the connection and connections details.""" + connection_alias = 'kontor' + write_concern = WriteConcern(j=True) diff --git a/flask/kontor/comics/views.py b/flask/kontor/comics/views.py new file mode 100644 index 0000000..090b032 --- /dev/null +++ b/flask/kontor/comics/views.py @@ -0,0 +1,222 @@ +""" +Define routing rules for comics, publisher and artists +""" +from flask import Blueprint, flash, redirect, render_template, url_for +from flask_login import login_required +from bson import ObjectId +from pymongo.errors import PyMongoError +from .forms import ComicForm, PublisherForm, ArtistForm +from .models import Comic, Publisher, Artist + + +COMIC = Blueprint('comic', __name__) + + +@COMIC.route('/artists') +@login_required +def list_artists(): + """ + List all artists + :return: + """ + artists = Artist.objects.all() + return render_template('comics/artists.html', + artists=artists, title="Artists") + + +@COMIC.route('/artists/edit/', methods=['GET', 'POST']) +@login_required +def edit_artist(artist_id): + """ + Edit a comic artist + """ + artist = Artist.objects.get({'_id': ObjectId(artist_id)}) + form = ArtistForm(obj=artist) + if form.validate_on_submit(): + artist.name = form.name.data + artist.save() + flash('You have successfully edited the artist.') + return redirect(url_for('comic.list_artists')) + form.name.data = artist.name + return render_template('simpleform.html', action="Edit", + form=form, title="Edit Artist") + + +@COMIC.route('/artists/add', methods=['GET', 'POST']) +@login_required +def add_artist(): + """ + Add a artist + :return: + """ + form = ArtistForm() + if form.validate_on_submit(): + artist = Artist() + artist.name = form.name.data + try: + # add publisher to the database + artist.save() + flash('You have successfully added a new artist.') + except PyMongoError: + # in case publisher name already exists + flash('Error: artist name already exists.') + return redirect(url_for('comic.list_artists')) + return render_template('simpleform.html', action="Add", + form=form, title="Add Artist") + + +@COMIC.route('/artists/delete/', methods=['GET', 'POST']) +@login_required +def delete_artist(artist_id): + """ + Delete a comic artist + :param artist_id: + :return: + """ + artist = Artist.objects.raw({'_id': ObjectId(artist_id)}) + if artist: + artist.delete() + flash('You have successfully deleted the comic artist.') + return redirect(url_for('comic.list_artists')) + + +@COMIC.route('/publishers') +@login_required +def list_publishers(): + """ + List all publishers + :return: + """ + publishers = Publisher.objects.all() + return render_template('comics/publishers.html', + publishers=publishers, title="Publishers") + + +@COMIC.route('/publishers/add', methods=['GET', 'POST']) +@login_required +def add_publisher(): + """ + Add a publisher to the database + :return: + """ + form = PublisherForm() + if form.validate_on_submit(): + publisher = Publisher() + publisher.name = form.name.data + try: + # add publisher to the database + publisher.save() + flash('You have successfully added a new publisher.') + except PyMongoError: + # in case publisher name already exists + flash('Error: publisher name already exists.') + return redirect(url_for('comic.list_publishers')) + return render_template('simpleform.html', action="Add", + form=form, title="Add Publisher") + + +@COMIC.route('/publishers/edit/', methods=['GET', 'POST']) +@login_required +def edit_publisher(publisher_id): + """ + Edit a publisher + """ + publisher = Publisher.objects.get({'_id': ObjectId(publisher_id)}) + form = PublisherForm(obj=publisher) + if form.validate_on_submit(): + publisher.name = form.name.data + publisher.save() + flash('You have successfully edited the publisher.') + return redirect(url_for('comic.list_publishers')) + form.name.data = publisher.name + return render_template('simpleform.html', action="Edit", + form=form, title="Edit Publisher") + + +@COMIC.route('/publishers/delete/', methods=['GET', 'POST']) +@login_required +def delete_publisher(publisher_id): + """ + Delete a publisher + :param publisher_id: ObjectId of publisher + :return: + """ + publisher = Publisher.objects.raw({'_id': ObjectId(publisher_id)}) + if publisher: + publisher.delete() + flash('You have successfully deleted the publisher.') + return redirect(url_for('comic.list_publishers')) + + +@COMIC.route('/comics') +@login_required +def list_comics(): + """ + List all comics + :return: + """ + comics = Comic.objects.all() + return render_template('comics/comics.html', + comics=comics, title="Comics") + + +@COMIC.route('/comics/edit/', methods=['GET', 'POST']) +@login_required +def edit_comic(comic_id): + """ + Edit a comic + """ + comic = Comic.objects.get({'_id': ObjectId(comic_id)}) + form = ComicForm(obj=comic) + form.publisher.choices = [(p.pk, p.name) for p in Publisher.objects.all()] + form.publisher.default = comic.publisher.pk + form.publisher.process_data(comic.publisher.pk) + if form.validate_on_submit(): + comic.title = form.title.data + comic.current_order = form.current_order.data + comic.publisher = form.publisher.data + comic.save() + flash('You have successfully edited the comic.') + return redirect(url_for('comic.list_comics')) + form.title.data = comic.title + form.current_order.data = comic.current_order + return render_template('simpleform.html', action="Edit", + form=form, title="Edit Comic") + + +@COMIC.route('/comics/add', methods=['GET', 'POST']) +@login_required +def add_comic(): + """ + Add a comic + :return: + """ + form = ComicForm() + form.publisher.choices = [(p.pk, p.name) for p in Publisher.objects.all()] + if form.validate_on_submit(): + comic = Comic() + comic.title = form.title.data + comic.publisher = form.publisher.data + try: + comic.save() + flash('You have successfully added a new comic.') + except PyMongoError: + flash('Error: comic title already exists.') + return redirect(url_for('comic.list_comics')) + return render_template('simpleform.html', action="Add", + form=form, title="Add Comic") + + +@COMIC.route('/comics/delete/', methods=['GET', 'POST']) +@login_required +def delete_comic(comic_id): + """ + Delete a comic + :param comic_id: + :return: + """ + comic = Comic.objects.raw({'_id': ObjectId(comic_id)}) + if comic: + comic.delete() + flash('You have successfully deleted the comic.') + return redirect(url_for('comic.list_comics')) diff --git a/flask/kontor/home/__init__.py b/flask/kontor/home/__init__.py new file mode 100644 index 0000000..d4468a9 --- /dev/null +++ b/flask/kontor/home/__init__.py @@ -0,0 +1,3 @@ +""" +Module for the Kontor homepage. +""" diff --git a/flask/kontor/home/views.py b/flask/kontor/home/views.py new file mode 100644 index 0000000..4e4d539 --- /dev/null +++ b/flask/kontor/home/views.py @@ -0,0 +1,39 @@ +""" +Define routing rules for homepage and dashboards +""" + +from flask import Blueprint, render_template, abort +from flask_login import login_required, current_user + +HOME = Blueprint('home', __name__) + + +@HOME.route('/') +def homepage(): + """ + Render the homepage template on the / route + """ + return render_template('home/index.html', title="Welcome") + + +@HOME.route('/dashboard') +@login_required +def dashboard(): + """ + Render the dashboard template on the /dashboard route + """ + return render_template('home/dashboard.html', title="Dashboard") + + +@HOME.route('/admin/dashboard') +@login_required +def admin_dashboard(): + """ + Render the admin_dashboard template on the /admin/dashboard route + :return: + """ + # prevent non-admins from accessing the page + if not current_user.is_admin: + abort(403) + + return render_template('home/admin_dashboard.html', title="Dashboard") diff --git a/flask/kontor/library/__init__.py b/flask/kontor/library/__init__.py new file mode 100644 index 0000000..31eb0bc --- /dev/null +++ b/flask/kontor/library/__init__.py @@ -0,0 +1,3 @@ +""" +Define routing rules for library related information +""" diff --git a/flask/kontor/library/forms.py b/flask/kontor/library/forms.py new file mode 100644 index 0000000..a080781 --- /dev/null +++ b/flask/kontor/library/forms.py @@ -0,0 +1,35 @@ +""" +Define form to edit publisher, artists and books +""" +from flask_wtf import FlaskForm +from wtforms import StringField, SubmitField, SelectField, IntegerField +from wtforms.validators import DataRequired, Optional, NumberRange +from bson import ObjectId + + +class PublisherForm(FlaskForm): + """ + Form to add and edit a Comic publisher + """ + name = StringField('Name', validators=[DataRequired()]) + submit = SubmitField('Submit') + + +class AuthorForm(FlaskForm): + """ + Form to add and edit a Comic publisher + """ + name = StringField('Name', validators=[DataRequired()]) + submit = SubmitField('Submit') + + +class BookForm(FlaskForm): + """ + Form to add and edit Comics + """ + title = StringField('Title', validators=[DataRequired()]) + author = SelectField('Author', coerce=ObjectId) + publisher = SelectField('Publisher', coerce=ObjectId) + isbn = StringField('ISBN', validators=[Optional()]) + year = IntegerField('Year', validators=[NumberRange(min=1970, max=2050)]) + submit = SubmitField('Submit') diff --git a/flask/kontor/library/models.py b/flask/kontor/library/models.py new file mode 100644 index 0000000..7ca5c58 --- /dev/null +++ b/flask/kontor/library/models.py @@ -0,0 +1,55 @@ +""" +This module declares the model for the library related information. +""" +from pymongo.write_concern import WriteConcern +from pymodm import MongoModel, fields + + +class Publisher(MongoModel): + """ + Class Publisher represents a publisher of a book. + """ + name = fields.CharField() + + def __str__(self): + return "Publisher({})".format(self.name) + + class Meta: + """Sets the connection and connections details.""" + connection_alias = 'kontor' + write_concern = WriteConcern(j=True) + + +class Author(MongoModel): + """ + Class Author represents an author of a book. + """ + name = fields.CharField() + + def __str__(self): + return "Author({})".format(self.name) + + class Meta: + """Sets the connection and connections details.""" + connection_alias = 'kontor' + write_concern = WriteConcern(j=True) + + +class Book(MongoModel): + """ + Class book represents a book. + """ + title = fields.CharField() + author = fields.ReferenceField(Author) + publisher = fields.ReferenceField(Publisher) + isbn = fields.CharField(blank=True) + year = fields.IntegerField(blank=True) + edition = fields.CharField() + + def __str__(self): + return "Book({})".format(self.title) + + class Meta: + """Sets the connection and connections details.""" + connection_alias = 'kontor' + write_concern = WriteConcern(j=True) diff --git a/flask/kontor/library/views.py b/flask/kontor/library/views.py new file mode 100644 index 0000000..1b53eee --- /dev/null +++ b/flask/kontor/library/views.py @@ -0,0 +1,228 @@ +"""Define routing rules for book publishers""" +from flask import Blueprint, flash, redirect, render_template, url_for +from flask_login import login_required +from bson import ObjectId +from pymongo.errors import PyMongoError +from .forms import PublisherForm, AuthorForm, BookForm +from .models import Publisher, Author, Book + + +LIBRARY = Blueprint('library', __name__) + + +@LIBRARY.route('/publishers') +@login_required +def list_publishers(): + """ + List all publishers + :return: + """ + publishers = Publisher.objects.all() + return render_template('library/publishers.html', + publishers=publishers, title="Publishers") + + +@LIBRARY.route('/publishers/add', methods=['GET', 'POST']) +@login_required +def add_publisher(): + """ + Add a publisher to the database + :return: + """ + form = PublisherForm() + if form.validate_on_submit(): + publisher = Publisher() + publisher.name = form.name.data + try: + publisher.save() + flash('You have successfully added a new publisher.') + except PyMongoError: + flash('Error: publisher name already exists.') + return redirect(url_for('library.list_publishers')) + return render_template('simpleform.html', action="Add", + form=form, title="Add Publisher") + + +@LIBRARY.route('/publishers/edit/', methods=['GET', 'POST']) +@login_required +def edit_publisher(publisher_id): + """ + Edit a publisher + """ + publisher = Publisher.objects.get({'_id': ObjectId(publisher_id)}) + form = PublisherForm(obj=publisher) + if form.validate_on_submit(): + publisher.name = form.name.data + publisher.save() + flash('You have successfully edited the publisher.') + return redirect(url_for('library.list_publishers')) + form.name.data = publisher.name + return render_template('simpleform.html', action="Edit", + form=form, publisher=publisher, title="Edit Publisher") + + +@LIBRARY.route('/publishers/delete/', methods=['GET', 'POST']) +@login_required +def delete_publisher(publisher_id): + """ + Delete a publisher + :param publisher_id: ObjectId of publisher + :return: + """ + publisher = Publisher.objects.raw({'_id': ObjectId(publisher_id)}) + if publisher: + publisher.delete() + flash('You have successfully deleted the publisher.') + return redirect(url_for('library.list_publishers')) + + +@LIBRARY.route('/authors') +@login_required +def list_authors(): + """ + List all authors + :return: + """ + authors = Author.objects.all() + return render_template('library/authors.html', + authors=authors, title="Authors") + + +@LIBRARY.route('/authors/edit/', methods=['GET', 'POST']) +@login_required +def edit_author(author_id): + """ + Edit a book artist + """ + author = Author.objects.get({'_id': ObjectId(author_id)}) + form = AuthorForm(obj=author) + if form.validate_on_submit(): + author.name = form.name.data + author.save() + flash('You have successfully edited the author.') + return redirect(url_for('library.list_authors')) + form.name.data = author.name + return render_template('simpleform.html', action="Edit", + form=form, author=author, title="Edit Author") + + +@LIBRARY.route('/authors/add', methods=['GET', 'POST']) +@login_required +def add_author(): + """ + Add a author + :return: + """ + form = AuthorForm() + if form.validate_on_submit(): + author = Author() + author.name = form.name.data + try: + author.save() + flash('You have successfully added a new author.') + except PyMongoError: + flash('Error: author name already exists.') + return redirect(url_for('library.list_authors')) + return render_template('simpleform.html', action="Add", + form=form, title="Add Author") + + +@LIBRARY.route('/authors/delete/', methods=['GET', 'POST']) +@login_required +def delete_author(author_id): + """ + Delete a author + :param author_id: + :return: + """ + author = Author.objects.raw({'_id': ObjectId(author_id)}) + if author: + author.delete() + flash('You have successfully deleted the author.') + return redirect(url_for('library.list_authors')) + + +@LIBRARY.route('/books') +@login_required +def list_books(): + """ + List all comics + :return: + """ + books = Book.objects.all() + return render_template('library/books.html', + books=books, title="Books") + + +@LIBRARY.route('/books/edit/', methods=['GET', 'POST']) +@login_required +def edit_book(book_id): + """ + Edit a book + """ + book = Book.objects.get({'_id': ObjectId(book_id)}) + form = BookForm(obj=book) + form.publisher.choices = [(p.pk, p.name) for p in Publisher.objects.all()] + if book.publisher: + form.publisher.default = book.publisher.pk + form.publisher.process_data(book.publisher.pk) + form.author.choices = [(a.pk, a.name) for a in Author.objects.all()] + if book.author: + form.author.default = book.author.pk + form.author.process_data(book.author.pk) + if form.validate_on_submit(): + book.title = form.title.data + book.publisher = form.publisher.data + book.author = form.author.data + if form.isbn.data: + book.isbn = form.isbn.data + else: + book.isbn = None + book.year = form.year.data + book.save() + flash('You have successfully edited the book.') + return redirect(url_for('library.list_books')) + form.title.data = book.title + return render_template('simpleform.html', action="Edit", + form=form, book=book, title="Edit Book") + + +@LIBRARY.route('/books/add', methods=['GET', 'POST']) +@login_required +def add_book(): + """ + Add a book + :return: + """ + form = BookForm() + form.publisher.choices = [(p.pk, p.name) for p in Publisher.objects.all()] + form.author.choices = [(a.pk, a.name) for a in Author.objects.all()] + if form.validate_on_submit(): + book = Book() + book.title = form.title.data + book.publisher = form.publisher.data + book.author = form.author.data + book.year = form.year.data + try: + book.save() + flash('You have successfully added a new book.') + except PyMongoError: + flash('Error: book title already exists.') + return redirect(url_for('library.list_books')) + return render_template('simpleform.html', action="Add", + form=form, title="Add Book") + + +@LIBRARY.route('/books/delete/', methods=['GET', 'POST']) +@login_required +def delete_book(book_id): + """ + Delete a book + :param book_id: + :return: + """ + book = Book.objects.raw({'_id': ObjectId(book_id)}) + if book: + book.delete() + flash('You have successfully deleted the book.') + return redirect(url_for('library.list_books')) diff --git a/flask/kontor/models.py b/flask/kontor/models.py new file mode 100644 index 0000000..7150364 --- /dev/null +++ b/flask/kontor/models.py @@ -0,0 +1,66 @@ +""" +This modules declares the model for Kontor users. +""" +from pymodm import MongoModel, fields +from pymongo.write_concern import WriteConcern +from flask_login import UserMixin +from werkzeug.security import generate_password_hash, check_password_hash +from kontor import _LOGIN_MANAGER_ + + +class User(UserMixin, MongoModel): + """ + This class represents an user for the Kontor application. + """ + email = fields.EmailField() + username = fields.CharField(max_length=60) + first_name = fields.CharField(max_length=60) + last_name = fields.CharField(max_length=60) + password_hash = fields.CharField(max_length=128) + is_admin = fields.BooleanField(default=False) + + @property + def password(self): + """ + Prevent pasword from being accessed + """ + raise AttributeError('password is not a readable attribute.') + + @password.setter + def password(self, password): + """ + Set password to a hashed password + """ + self.password_hash = generate_password_hash(password) + + def verify_password(self, password): + """ + Check if hashed password matches actual password + """ + return check_password_hash(self.password_hash, password) + + def get_id(self): + """ + Get username as id + :return: + """ + return self.username + + def __repr__(self): + return ''.format(self.username) + + class Meta: + """Sets the connection and connections details.""" + connection_alias = 'kontor' + write_concern = WriteConcern(j=True) + + +# Set up user_loader +@_LOGIN_MANAGER_.user_loader +def load_user(user_name): + """ + Get list of users from database + :param user_name: + :return: + """ + return User.objects.get({'username': user_name}) diff --git a/flask/kontor/office/__init__.py b/flask/kontor/office/__init__.py new file mode 100644 index 0000000..d830d41 --- /dev/null +++ b/flask/kontor/office/__init__.py @@ -0,0 +1,3 @@ +""" +Define routing rules for office related information +""" diff --git a/flask/kontor/static/css/style.css b/flask/kontor/static/css/style.css new file mode 100644 index 0000000..35fe053 --- /dev/null +++ b/flask/kontor/static/css/style.css @@ -0,0 +1,126 @@ +/* app/static/css/style.css */ + +body, html { + width: 100%; + height: 100%; +} + +body, h1, h2, h3 { + font-family: "Lato", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 700; +} + +a, .navbar-default .navbar-brand, .navbar-default .navbar-nav>li>a { + color: #aec251; +} + +a:hover, .navbar-default .navbar-brand:hover, .navbar-default .navbar-nav>li>a:hover { + color: #687430; +} + +footer { + padding: 50px 0; + background-color: #f8f8f8; +} + +p.copyright { + margin: 15px 0 0; +} + +.alert-info { + width: 50%; + margin: auto; + color: #687430; + background-color: #e6ecca; + border-color: #aec251; +} + +.btn-default { + border-color: #aec251; + color: #aec251; +} + +.btn-default:hover { + background-color: #aec251; +} + +.center { + margin: auto; + width: 70%; + padding: 10px; +} + +.content-section { + padding: 50px 0; + border-top: 1px solid #e7e7e7; +} + +.footer, .push { + clear: both; + height: 4em; +} + +.intro-divider { + width: 400px; + border-top: 1px solid #f8f8f8; + border-bottom: 1px solid rgba(0,0,0,0.2); +} + +.intro-header { + padding-top: 50px; + padding-bottom: 50px; + text-align: center; + color: #f8f8f8; + background: url(../img/intro-bg.jpg) no-repeat center center; + background-size: cover; + height: 100%; +} + +.intro-message { + position: relative; + padding-top: 20%; + padding-bottom: 20%; +} + +.intro-message > h1 { + margin: 0; + text-shadow: 2px 2px 3px rgba(0,0,0,0.6); + font-size: 5em; +} + +.intro-message > h3 { + text-shadow: 2px 2px 3px rgba(0,0,0,0.6); +} + +.lead { + font-size: 18px; + font-weight: 400; +} + +.topnav { + font-size: 14px; +} + +.wrapper { + min-height: 100%; + height: auto !important; + height: 100%; + margin: 0 auto -4em; +} + +.outer { + display: table; + position: absolute; + height: 70%; + width: 100%; +} + +.middle { + display: table-cell; + vertical-align: middle; +} + +.inner { + margin-left: auto; + margin-right: auto; +} diff --git a/flask/kontor/templates/admin/users/user.html b/flask/kontor/templates/admin/users/user.html new file mode 100644 index 0000000..50df2c4 --- /dev/null +++ b/flask/kontor/templates/admin/users/user.html @@ -0,0 +1,21 @@ + + +{% import "bootstrap/wtf.html" as wtf %} +{% extends "base.html" %} +{% block title %}Edit User{% endblock %} +{% block body %} +
+
+
+
+
+

Edit User

+
+
+ {{ wtf.quick_form(form) }} +
+
+
+
+
+{% endblock %} diff --git a/flask/kontor/templates/admin/users/users.html b/flask/kontor/templates/admin/users/users.html new file mode 100644 index 0000000..b23f67c --- /dev/null +++ b/flask/kontor/templates/admin/users/users.html @@ -0,0 +1,58 @@ + + +{% import "bootstrap/utils.html" as utils %} +{% extends "base.html" %} +{% block title %}Users{% endblock %} +{% block body %} +
+
+
+
+
+ {{ utils.flashed_messages() }} +
+

Users

+ {% if users %} +
+
+ + + + + + + + + + + {% for user in users %} + {% if user.is_admin %} + + + + + + + {% else %} + + + + + + + {% endif %} + {% endfor %} + +
Name Email Username Edit
Admin N/A N/A N/A
{{ user.first_name }} {{ user.last_name }} {{ user.email }} {{ user.username }} + + Edit + +
+
+ {% endif %} +
+
+
+
+ +{% endblock %} diff --git a/flask/kontor/templates/auth/login.html b/flask/kontor/templates/auth/login.html new file mode 100644 index 0000000..8b70e01 --- /dev/null +++ b/flask/kontor/templates/auth/login.html @@ -0,0 +1,18 @@ + + +{% import "bootstrap/utils.html" as utils %} +{% import "bootstrap/wtf.html" as wtf %} +{% extends "base.html" %} +{% block title %}Login{% endblock %} +{% block body %} +
+
+ {{ utils.flashed_messages() }} +
+
+

Login to your account

+
+ {{ wtf.quick_form(form) }} +
+
+{% endblock %} diff --git a/flask/kontor/templates/auth/register.html b/flask/kontor/templates/auth/register.html new file mode 100644 index 0000000..55d4918 --- /dev/null +++ b/flask/kontor/templates/auth/register.html @@ -0,0 +1,14 @@ + + +{% import "bootstrap/wtf.html" as wtf %} +{% extends "base.html" %} +{% block title %}Register{% endblock %} +{% block body %} +
+
+

Register for an account

+
+ {{ wtf.quick_form(form) }} +
+
+{% endblock %} diff --git a/flask/kontor/templates/base.html b/flask/kontor/templates/base.html new file mode 100644 index 0000000..80bdc27 --- /dev/null +++ b/flask/kontor/templates/base.html @@ -0,0 +1,107 @@ + + + + + {{ title }} | Kontor + + + + + + + + +
+ {% block body %} + {% endblock %} +
+
+
+
+
+

+

+ +
+
+ +
+ + diff --git a/flask/kontor/templates/comics/artists.html b/flask/kontor/templates/comics/artists.html new file mode 100644 index 0000000..a92415e --- /dev/null +++ b/flask/kontor/templates/comics/artists.html @@ -0,0 +1,58 @@ +{% import "bootstrap/utils.html" as utils %} +{% extends "base.html" %} +{% block title %}Comic Artists{% endblock %} +{% block body %} +
+
+
+
+
+ {{ utils.flashed_messages() }} +
+

Comic Artists

+ {% if artists %} +
+
+ + + + + + + + + + {% for artist in artists %} + + + + + + {% endfor %} + +
Name Edit Delete
{{ artist.name }} + + Edit + + + + Delete + +
+
+
+ {% else %} +
+

No artists have been added.

+
+ {% endif %} + + + Add Artist + +
+
+
+
+
+{% endblock %} diff --git a/flask/kontor/templates/comics/comics.html b/flask/kontor/templates/comics/comics.html new file mode 100644 index 0000000..c596ad5 --- /dev/null +++ b/flask/kontor/templates/comics/comics.html @@ -0,0 +1,66 @@ +{% import "bootstrap/utils.html" as utils %} +{% extends "base.html" %} +{% block title %}Comics{% endblock %} +{% block body %} +
+
+
+
+
+ {{ utils.flashed_messages() }} +
+

Comics

+ {% if comics %} +
+
+ + + + + + + + + + + + {% for comic in comics %} + + + + + + + + {% endfor %} + +
Title Publisher Current Order Edit Delete
{{ comic.title }} + + {{ comic.publisher.name }} + + {{ comic.current_order }} + + Edit + + + + Delete + +
+
+
+ {% else %} +
+

No comics have been added.

+
+ {% endif %} + + + Add Comic + +
+
+
+
+
+{% endblock %} diff --git a/flask/kontor/templates/comics/publishers.html b/flask/kontor/templates/comics/publishers.html new file mode 100644 index 0000000..aa09f7b --- /dev/null +++ b/flask/kontor/templates/comics/publishers.html @@ -0,0 +1,58 @@ +{% import "bootstrap/utils.html" as utils %} +{% extends "base.html" %} +{% block title %}Comic Publishers{% endblock %} +{% block body %} +
+
+
+
+
+ {{ utils.flashed_messages() }} +
+

Comic Publishers

+ {% if publishers %} +
+
+ + + + + + + + + + {% for publisher in publishers %} + + + + + + {% endfor %} + +
Name Edit Delete
{{ publisher.name }} + + Edit + + + + Delete + +
+
+
+ {% else %} +
+

No comics have been added.

+
+ {% endif %} + + + Add Publisher + +
+
+
+
+
+{% endblock %} diff --git a/flask/kontor/templates/home/admin_dashboard.html b/flask/kontor/templates/home/admin_dashboard.html new file mode 100644 index 0000000..77a0c99 --- /dev/null +++ b/flask/kontor/templates/home/admin_dashboard.html @@ -0,0 +1,31 @@ + + +{% extends "base.html" %} +{% block title %}Admin Dashboard{% endblock %} +{% block body %} +
+
+
+
+
+

Admin Dashboard

+
+ +
+
+
+
+
+{% endblock %} diff --git a/flask/kontor/templates/home/dashboard.html b/flask/kontor/templates/home/dashboard.html new file mode 100644 index 0000000..6764375 --- /dev/null +++ b/flask/kontor/templates/home/dashboard.html @@ -0,0 +1,20 @@ + + +{% extends "base.html" %} +{% block title %}Dashboard{% endblock %} +{% block body %} +
+
+
+
+
+

The Dashboard

+

We made it here!

+
+ +
+
+
+
+
+{% endblock %} diff --git a/flask/kontor/templates/home/index.html b/flask/kontor/templates/home/index.html new file mode 100644 index 0000000..5b01225 --- /dev/null +++ b/flask/kontor/templates/home/index.html @@ -0,0 +1,20 @@ + + +{% extends "base.html" %} +{% block title %}Home{% endblock %} +{% block body %} +
+
+
+
+
+

Project Kontor

+

Store everything!

+
+ +
+
+
+
+
+{% endblock %} diff --git a/flask/kontor/templates/library/authors.html b/flask/kontor/templates/library/authors.html new file mode 100644 index 0000000..8c24f7b --- /dev/null +++ b/flask/kontor/templates/library/authors.html @@ -0,0 +1,58 @@ +{% import "bootstrap/utils.html" as utils %} +{% extends "base.html" %} +{% block title %}Library Authors{% endblock %} +{% block body %} +
+
+
+
+
+ {{ utils.flashed_messages() }} +
+

Library Authors

+ {% if authors %} +
+
+ + + + + + + + + + {% for author in authors %} + + + + + + {% endfor %} + +
Name Edit Delete
{{ author.name }} + + Edit + + + + Delete + +
+
+
+ {% else %} +
+

No artists have been added.

+
+ {% endif %} + + + Add Author + +
+
+
+
+
+{% endblock %} diff --git a/flask/kontor/templates/library/books.html b/flask/kontor/templates/library/books.html new file mode 100644 index 0000000..6a6ef76 --- /dev/null +++ b/flask/kontor/templates/library/books.html @@ -0,0 +1,74 @@ +{% import "bootstrap/utils.html" as utils %} +{% extends "base.html" %} +{% block title %}Books{% endblock %} +{% block body %} +
+
+
+
+
+ {{ utils.flashed_messages() }} +
+

Books

+ {% if books %} +
+
+ + + + + + + + + + + + + + {% for book in books %} + + + + + + + + + + {% endfor %} + +
Title Author Publisher ISBN Year Edit Delete
{{ book.title }} + + {{ book.author.name }} + + + + {{ book.publisher.name }} + + {{ book.isbn }} {{ book.year }} + + Edit + + + + Delete + +
+
+
+ {% else %} +
+

No books have been added.

+
+ {% endif %} + + + Add Book + +
+
+
+
+
+{% endblock %} diff --git a/flask/kontor/templates/library/publishers.html b/flask/kontor/templates/library/publishers.html new file mode 100644 index 0000000..d0d363c --- /dev/null +++ b/flask/kontor/templates/library/publishers.html @@ -0,0 +1,58 @@ +{% import "bootstrap/utils.html" as utils %} +{% extends "base.html" %} +{% block title %}Book Publishers{% endblock %} +{% block body %} +
+
+
+
+
+ {{ utils.flashed_messages() }} +
+

Book Publishers

+ {% if publishers %} +
+
+ + + + + + + + + + {% for publisher in publishers %} + + + + + + {% endfor %} + +
Name Edit Delete
{{ publisher.name }} + + Edit + + + + Delete + +
+
+
+ {% else %} +
+

No publishers have been added.

+
+ {% endif %} + + + Add Publisher + +
+
+
+
+
+{% endblock %} diff --git a/flask/kontor/templates/simpleform.html b/flask/kontor/templates/simpleform.html new file mode 100644 index 0000000..6ce9d3b --- /dev/null +++ b/flask/kontor/templates/simpleform.html @@ -0,0 +1,21 @@ +{% import "bootstrap/wtf.html" as wtf %} +{% extends "base.html" %} +{% block title %}{{ title }}{% endblock %} +{% block body %} +
+
+
+
+
+

{{ title }}

+
+
+ {{ wtf.quick_form(form) }} +
+
+
+
+
+
+
+{% endblock %} diff --git a/flask/kontor/templates/tysc/manufacturers.html b/flask/kontor/templates/tysc/manufacturers.html new file mode 100644 index 0000000..f71ed75 --- /dev/null +++ b/flask/kontor/templates/tysc/manufacturers.html @@ -0,0 +1,58 @@ +{% import "bootstrap/utils.html" as utils %} +{% extends "base.html" %} +{% block title %}{{ title }}{% endblock %} +{% block body %} +
+
+
+
+
+ {{ utils.flashed_messages() }} +
+

{{ title }}

+ {% if manufacturers %} +
+
+ + + + + + + + + + {% for manufacturer in manufacturers %} + + + + + + {% endfor %} + +
Name Edit Delete
{{ manufacturer.name }} + + Edit + + + + Delete + +
+
+
+ {% else %} +
+

No manufacturers have been added.

+
+ {% endif %} + + + Add Manufacturer + +
+
+
+
+
+{% endblock %} diff --git a/flask/kontor/templates/tysc/players.html b/flask/kontor/templates/tysc/players.html new file mode 100644 index 0000000..db25067 --- /dev/null +++ b/flask/kontor/templates/tysc/players.html @@ -0,0 +1,60 @@ +{% import "bootstrap/utils.html" as utils %} +{% extends "base.html" %} +{% block title %}{{ title }}{% endblock %} +{% block body %} +
+
+
+
+
+ {{ utils.flashed_messages() }} +
+

{{ title }}

+ {% if players %} +
+
+ + + + + + + + + + + {% for player in players %} + + + + + + + {% endfor %} + +
First Name Last Name Edit Delete
{{ player.first_name }} {{ player.last_name }} + + Edit + + + + Delete + +
+
+
+ {% else %} +
+

No players have been added.

+
+ {% endif %} + + + Add Player + +
+
+
+
+
+{% endblock %} diff --git a/flask/kontor/templates/tysc/positions.html b/flask/kontor/templates/tysc/positions.html new file mode 100644 index 0000000..3b49e6e --- /dev/null +++ b/flask/kontor/templates/tysc/positions.html @@ -0,0 +1,62 @@ +{% import "bootstrap/utils.html" as utils %} +{% extends "base.html" %} +{% block title %}{{ title }}{% endblock %} +{% block body %} +
+
+
+
+
+ {{ utils.flashed_messages() }} +
+

{{ title }}

+ {% if positions %} +
+
+ + + + + + + + + + + + {% for position in positions %} + + + + + + + + {% endfor %} + +
Name Description Sport Edit Delete
{{ position.name }} {{ position.description }} {{ position.sport.name }} + + Edit + + + + Delete + +
+
+
+ {% else %} +
+

No positions have been added.

+
+ {% endif %} + + + Add Position + +
+
+
+
+
+{% endblock %} diff --git a/flask/kontor/templates/tysc/sports.html b/flask/kontor/templates/tysc/sports.html new file mode 100644 index 0000000..1efe09a --- /dev/null +++ b/flask/kontor/templates/tysc/sports.html @@ -0,0 +1,58 @@ +{% import "bootstrap/utils.html" as utils %} +{% extends "base.html" %} +{% block title %}Sports{% endblock %} +{% block body %} +
+
+
+
+
+ {{ utils.flashed_messages() }} +
+

Sports

+ {% if sports %} +
+
+ + + + + + + + + + {% for sport in sports %} + + + + + + {% endfor %} + +
Name Edit Delete
{{ sport.name }} + + Edit + + + + Delete + +
+
+
+ {% else %} +
+

No sports have been added.

+
+ {% endif %} + + + Add Sport + +
+
+
+
+
+{% endblock %} diff --git a/flask/kontor/templates/tysc/teams.html b/flask/kontor/templates/tysc/teams.html new file mode 100644 index 0000000..c51e9bf --- /dev/null +++ b/flask/kontor/templates/tysc/teams.html @@ -0,0 +1,60 @@ +{% import "bootstrap/utils.html" as utils %} +{% extends "base.html" %} +{% block title %}Teams{% endblock %} +{% block body %} +
+
+
+
+
+ {{ utils.flashed_messages() }} +
+

Teams

+ {% if teams %} +
+
+ + + + + + + + + + + {% for team in teams %} + + + + + + + {% endfor %} + +
Name Shortname Edit Delete
{{ team.name }} {{ team.shortname }} + + Edit + + + + Delete + +
+
+
+ {% else %} +
+

No teams have been added.

+
+ {% endif %} + + + Add Team + +
+
+
+
+
+{% endblock %} diff --git a/flask/kontor/tysc/__init__.py b/flask/kontor/tysc/__init__.py new file mode 100644 index 0000000..9f06f83 --- /dev/null +++ b/flask/kontor/tysc/__init__.py @@ -0,0 +1,377 @@ +""" +Define routing rules for TradeYourSportsCards related information +""" +from .models import Sport, Team, Position, Player +from .models import Manufacturer, CardSet, ParallelSet, InsertSet, Card + + +def initialize_model(): + """ + Initialize collections for module TradeYourSportsCards. + """ + initialize_sport() + initialize_position() + initialize_teams() + initialize_players() + initialize_manufacturers() + initialize_card_sets() + initialize_parallel_sets() + initialize_insert_sets() + initialize_cards() + + +def initialize_sport(): + """ + Initialize collection Sport with american sports. + """ + for sport in Sport.objects.all(): + sport.delete() + sports = ["Baseball", "Basketball", "Football", "Hockey"] + for sport_name in sports: + sport = Sport() + sport.name = sport_name + sport.save() + + +def initialize_position(): + """ + Initialize collection Position with data. + """ + for position in Position.objects.all(): + position.delete() + positions = { + 'Quarterback': {'short': 'QB', 'sport': 'Football'}, + 'Wide Receiver': {'short': 'WR', 'sport': 'Football'}, + 'Runningback': {'short': 'RB', 'sport': 'Football'}, + 'Linebacker': {'short': 'LB', 'sport': 'Football'}, + 'Tight End': {'short': 'TE', 'sport': 'Football'}, + 'Fullback': {'short': 'FB', 'sport': 'Football'}, + 'Strong Safety': {'short': 'SS', 'sport': 'Football'}, + 'Defensive End': {'short': 'DE', 'sport': 'Football'}, + 'Kicker': {'short': 'K', 'sport': 'Football'}, + 'Punter': {'short': 'P', 'sport': 'Football'}, + 'Left Guard': {'short': 'LG', 'sport': 'Football'}, + 'Right Guard': {'short': 'RG', 'sport': 'Football'}, + 'Offensive Tackle': {'short': 'OT', 'sport': 'Football'}, + 'Defensive Back': {'short': 'DB', 'sport': 'Football'}, + 'Cornerback': {'short': 'CB', 'sport': 'Football'}, + 'Catcher': {'short': 'C', 'sport': 'Baseball'}, + 'First Base': {'short': '1B', 'sport': 'Baseball'}, + 'Second Base': {'short': '2B', 'sport': 'Baseball'}, + 'Third Base': {'short': '3B', 'sport': 'Baseball'}, + 'Shortstop': {'short': 'SS', 'sport': 'Baseball'}, + 'Left Field': {'short': 'LF', 'sport': 'Baseball'}, + 'Center Field': {'short': 'CF', 'sport': 'Baseball'}, + 'Right Field': {'short': 'RF', 'sport': 'Baseball'}, + 'Designated Hitter': {'short': 'DH', 'sport': 'Baseball'}, + 'Pitcher': {'short': 'P', 'sport': 'Baseball'} + } + for key in positions: + sport = Sport.objects.get({'name': positions.get(key)['sport']}) + position = Position() + position.description = key + position.name = positions.get(key)['short'] + position.sport = sport + position.save() + + +def initialize_teams(): + """ + Initialize collection Team with data. + """ + for team in Team.objects.all(): + team.delete() + teams = {'Buffalo Bills': {'short': 'Bills', 'sport': 'Football'}, + 'Indianapolis Colts': {'short': 'Colts', 'sport': 'Football'}, + 'Miami Dolphins': {'short': 'Dolphins', 'sport': 'Football'}, + 'New England Patriots': {'short': 'Patriots', 'sport': 'Football'}, + 'New York Jets': {'short': 'Jets', 'sport': 'Football'}, + 'Baltimore Ravens': {'short': 'Ravens', 'sport': 'Football'}, + 'Cincinnati Bengals': {'short': 'Bengals', 'sport': 'Football'}, + 'Cleveland Browns': {'short': 'Browns', 'sport': 'Football'}, + 'Jacksonville Jaguars': {'short': 'Jaguars', 'sport': 'Football'}, + 'Pittsburgh Steelers': {'short': 'Steelers', 'sport': 'Football'}, + 'Tennessee Titans': {'short': 'Titans', 'sport': 'Football'}, + 'Denver Broncos': {'short': 'Broncos', 'sport': 'Football'}, + 'Kansas City Chiefs': {'short': 'Chiefs', 'sport': 'Football'}, + 'Oakland Raiders': {'short': 'Raiders', 'sport': 'Football'}, + 'San Diego Chargers': {'short': 'Chargers', 'sport': 'Football'}, + 'Seattle Seahawks': {'short': 'Seahawks', 'sport': 'Football'}, + 'Arizona Cardinals': {'short': 'Cardinals', 'sport': 'Football'}, + 'Dallas Cowboys': {'short': 'Cowboys', 'sport': 'Football'}, + 'New York Giants': {'short': 'Giants', 'sport': 'Football'}, + 'Philadelphia Eagles': {'short': 'Eagles', 'sport': 'Football'}, + 'Washington Redskins': {'short': 'Redskins', 'sport': 'Football'}, + 'Chicago Bears': {'short': 'Bears', 'sport': 'Football'}, + 'Detroit Lions': {'short': 'Lions', 'sport': 'Football'}, + 'Green Bay Packers': {'short': 'Packers', 'sport': 'Football'}, + 'Minnesota Vikings': {'short': 'Vikings', 'sport': 'Football'}, + 'Tampa Bay Buccaneers': {'short': 'Buccaneers', 'sport': 'Football'}, + 'Atlanta Falcons': {'short': 'Falcons', 'sport': 'Football'}, + 'Carolina Panthers': {'short': 'Panthers', 'sport': 'Football'}, + 'New Orleans Saints': {'short': 'Saints', 'sport': 'Football'}, + 'St.Louis Rams': {'short': 'Rams', 'sport': 'Football'}, + 'San Francisco 49ers': {'short': '49ers', 'sport': 'Football'}, + 'Baltimore Orioles': {'short': 'Orioles', 'sport': 'Baseball'}, + 'Boston Red Sox': {'short': 'Red Sox', 'sport': 'Baseball'}, + 'New York Yankees': {'short': 'Yankees', 'sport': 'Baseball'}, + 'Tampa Bay Devil Rays': {'short': 'Devil Rays', 'sport': 'Baseball'}, + 'Toronto Blue Jays': {'short': 'Blue Jays', 'sport': 'Baseball'}, + 'Chicago White Sox': {'short': 'White Sox', 'sport': 'Baseball'}, + 'Cleveland Indians': {'short': 'Indians', 'sport': 'Baseball'}, + 'Detroit Tigers': {'short': 'Tigers', 'sport': 'Baseball'}, + 'Kansas City Royals': {'short': 'Royals', 'sport': 'Baseball'}, + 'Minnesota Twins': {'short': 'Twins', 'sport': 'Baseball'}, + 'Anaheim Angels': {'short': 'Angels', 'sport': 'Baseball'}, + 'Oakland Athletics': {'short': 'Athletics', 'sport': 'Baseball'}, + 'Seattle Mariners': {'short': 'Mariners', 'sport': 'Baseball'}, + 'Texas Rangers': {'short': 'Rangers', 'sport': 'Baseball'}, + 'Atlanta Braves': {'short': 'Braves', 'sport': 'Baseball'}, + 'Florida Marlins': {'short': 'Marlins', 'sport': 'Baseball'}, + 'Montreal Expos': {'short': 'Expos', 'sport': 'Baseball'}, + 'New York Mets': {'short': 'Mets', 'sport': 'Baseball'}, + 'Philadelphia Phillies': {'short': 'Phillies', 'sport': 'Baseball'}, + 'Chicago Cubs': {'short': 'Cubs', 'sport': 'Baseball'}, + 'Cincinnati Reds': {'short': 'Reds', 'sport': 'Baseball'}, + 'Houston Astros': {'short': 'Astros', 'sport': 'Baseball'}, + 'Milwaukee Brewers': {'short': 'Brewers', 'sport': 'Baseball'}, + 'Pittsburgh Pirates': {'short': 'Pirates', 'sport': 'Baseball'}, + 'St.Louis Cardinals': {'short': 'Cardinals', 'sport': 'Baseball'}, + 'Arizona Diamondbacks': {'short': 'Diamondbacks', 'sport': 'Baseball'}, + 'Colorado Rockies': {'short': 'Rockies', 'sport': 'Baseball'}, + 'Los Angeles Dodgers': {'short': 'Dodgers', 'sport': 'Baseball'}, + 'San Diego Padres': {'short': 'Padres', 'sport': 'Baseball'}, + 'San Francisco Giants': {'short': 'Giants', 'sport': 'Baseball'}, + 'Boston Celtics': {'short': 'Celtics', 'sport': 'Basketball'}, + 'Miami Heat': {'short': 'Heat', 'sport': 'Basketball'}, + 'New Jersey Nets': {'short': 'Mets', 'sport': 'Basketball'}, + 'New York Knicks': {'short': 'Knicks', 'sport': 'Basketball'}, + 'Orlando Magic': {'short': 'Magic', 'sport': 'Basketball'}, + 'Philadelphia 76ers': {'short': '76ers', 'sport': 'Basketball'}, + 'Washington Wizards': {'short': 'Wizards', 'sport': 'Basketball'}, + 'Atlanta Hawks': {'short': 'Hawks', 'sport': 'Basketball'}, + 'Charlotte Hornets': {'short': 'Hornets', 'sport': 'Basketball'}, + 'Chicago Bulls': {'short': 'Bulls', 'sport': 'Basketball'}, + 'Cleveland Cavaliers': {'short': 'Cavaliers', 'sport': 'Basketball'}, + 'Detroit Pistons': {'short': 'Pistons', 'sport': 'Basketball'}, + 'Indiana Pacers': {'short': 'Pacers', 'sport': 'Basketball'}, + 'Milwaukee Bucks': {'short': 'Bucks', 'sport': 'Basketball'}, + 'Toronto Raptors': {'short': 'Raptors', 'sport': 'Basketball'}, + 'Dallas Mavericks': {'short': 'Mavericks', 'sport': 'Basketball'}, + 'Denver Nuggets': {'short': 'Nuggets', 'sport': 'Basketball'}, + 'Houston Rockets': {'short': 'Rockets', 'sport': 'Basketball'}, + 'Minnesota Timberwolves': {'short': 'Timberwolves', 'sport': 'Basketball'}, + 'San Antonio Spurs': {'short': 'Spurs', 'sport': 'Basketball'}, + 'Utah Jazz': {'short': 'Jazz', 'sport': 'Basketball'}, + 'Vancouver Grizzlies': {'short': 'Grizzlies', 'sport': 'Basketball'}, + 'Golden State Warriors': {'short': 'Warriors', 'sport': 'Basketball'}, + 'Los Angeles Clippers': {'short': 'Clippers', 'sport': 'Basketball'}, + 'Los Angeles Lakers': {'short': 'Lakers', 'sport': 'Basketball'}, + 'Phoenix Suns': {'short': 'Suns', 'sport': 'Basketball'}, + 'Portland Trail Blazers': {'short': 'Blazers', 'sport': 'Basketball'}, + 'Sacramento Kings': {'short': 'Kings', 'sport': 'Basketball'}, + 'Seattle SuperSonics': {'short': 'SuperSonics', 'sport': 'Basketball'}, + 'Boston Bruins': {'short': 'Bruins', 'sport': 'Hockey'}, + 'Buffalo Sabres': {'short': 'Sabres', 'sport': 'Hockey'}, + 'Montreal Canadiens': {'short': 'Canadiens', 'sport': 'Hockey'}, + 'Ottawa Senators': {'short': 'Senators', 'sport': 'Hockey'}, + 'Toronto Maple Leafs': {'short': 'Maple Leafs', 'sport': 'Hockey'}, + 'New Jersey Devils': {'short': 'Devils', 'sport': 'Hockey'}, + 'New York Islander': {'short': 'Islander', 'sport': 'Hockey'}, + 'New York Rangers': {'short': 'Rangers', 'sport': 'Hockey'}, + 'Philadelphia Flyers': {'short': 'Flyers', 'sport': 'Hockey'}, + 'Pittsburgh Penguins': {'short': 'Penguins', 'sport': 'Hockey'}, + 'Atlanta Trashers': {'short': 'Trashers', 'sport': 'Hockey'}, + 'Carolina Hurricanes': {'short': 'Hurricanes', 'sport': 'Hockey'}, + 'Florida Panthers': {'short': 'Panthers', 'sport': 'Hockey'}, + 'Tampa Bay Lightnings': {'short': 'Lightnings', 'sport': 'Hockey'}, + 'Washington Capitals': {'short': 'Capitals', 'sport': 'Hockey'}, + 'Chicago Blackhawks': {'short': 'Blackhawks', 'sport': 'Hockey'}, + 'Columbo Blue Jackets': {'short': 'Blue Jackets', 'sport': 'Hockey'}, + 'Detroit Red Wings': {'short': 'Red Wings', 'sport': 'Hockey'}, + 'Nashville Predators': {'short': 'Predators', 'sport': 'Hockey'}, + 'St.Louis Blues': {'short': 'Blues', 'sport': 'Hockey'}, + 'Calgary Flames': {'short': 'Flames', 'sport': 'Hockey'}, + 'Colorado Avalanche': {'short': 'Avalanche', 'sport': 'Hockey'}, + 'Edmonton Oilers': {'short': 'Oilers', 'sport': 'Hockey'}, + 'Minnesota Wild': {'short': 'Wild', 'sport': 'Hockey'}, + 'Vancouver Canucks': {'short': 'Canucks', 'sport': 'Hockey'}, + 'Anaheim Mighty Ducks': {'short': 'Mighty Ducks', 'sport': 'Hockey'}, + 'Dallas Stars': {'short': 'Stars', 'sport': 'Hockey'}, + 'Los Angeles Kings': {'short': 'Kings', 'sport': 'Hockey'}, + 'Phoenix Coyotes': {'short': 'Coyotes', 'sport': 'Hockey'}, + 'San Jose Sharks': {'short': 'Sharks', 'sport': 'Hockey'}, + 'Houston Texans': {'short': 'Texans', 'sport': 'Football'}, + 'Houston Oilers': {'short': 'Oilers', 'sport': 'Football'} + } + for key in teams: + sport = Sport.objects.get({'name': teams.get(key)['sport']}) + team = Team() + team.name = key + team.shortname = teams.get(key)['short'] + team.sport = sport + team.save() + + +def initialize_players(): + """ + Initialize collection Manufacturer with data. + """ + for player in Player.objects.all(): + player.delete() + players = ['Pathon, Jerome', 'Bruschi, Tedy', 'Couch, Tim', 'Shea, Aaron', + 'Lewis, Jamal', 'Lewis, Jermaine', 'Banks, Tony', 'Fuamatu-Ma\'Afala, Chris', + 'Bettis, Jerome', 'Stewart, Kordell', 'Moon, Warren', 'Lockett, Kevin', + 'Gannon, Rich', 'Jett, James', 'Strong, Mack', 'Huard, Brock', + 'Watters, Ricky', 'Aikman, Troy', 'LaFleur, David', 'Brazzell, Chris', + 'Dayne, Ron', 'Brown, Na', 'Small, Torrance', 'Lewis, Chad', 'Murrell, Adrian', + 'Smith, Maurice', 'Chandler, Chris', 'Kanell, Danny', 'Williams, Ricky', + 'Garcia, Jeff', 'Streets, Tai', 'Garner, Charlie', 'Rice, Jerry', + 'Owens, Terrell', 'Bruce, Isaac', 'Canidate, Trung'] + for player_name in players: + player = Player() + (last_name, first_name) = player_name.split(',') + player.last_name = last_name.strip() + player.first_name = first_name.strip() + player.save() + + +def initialize_manufacturers(): + """ + Initialize collection Manufacturer with data. + """ + for manufacturer in Manufacturer.objects.all(): + manufacturer.delete() + manufacturers = ['Pacific', 'Fleer', 'Bowman', 'Topps', 'Donruss', + 'Score', 'Flair', 'Upper Deck'] + for manufacturer_name in manufacturers: + manufacturer = Manufacturer() + manufacturer.name = manufacturer_name + manufacturer.save() + + +def initialize_card_sets(): + """ + Initialize collection CardSet with data. + """ + for card_set in CardSet.objects.all(): + card_set.delete() + card_sets = {'Pacific': 'Pacific', + 'Fleer': 'Fleer', + 'Bowman': 'Bowman', + 'Leaf': 'Topps', + 'Ultra': 'Fleer', + 'Mystique': 'Fleer', + 'Finest Hour': 'Pacific', + 'SP': 'Upper Deck', + 'SPx': 'Upper Deck', + 'SP Authentic': 'Upper Deck', + 'Black Diamond': 'Upper Deck' + } + for set_name in card_sets: + card_set = CardSet() + card_set.name = set_name + card_set.manufacturer = Manufacturer.objects.get({'name': card_sets.get(set_name)}) + card_set.save() + + +def initialize_parallel_sets(): + """ + Initialize collection ParallelSet with data. + """ + for parallel_set in ParallelSet.objects.all(): + parallel_set.delete() + parallel_sets = {'Mystique Gold': 'Fleer', + 'Pacific Copper': 'Pacific', + 'Pacific Gold': 'Pacific' + } + for key in parallel_sets: + manufacturer = Manufacturer.objects.get({'name': parallel_sets.get(key)}) + parallel_set = ParallelSet() + parallel_set.name = key + parallel_set.manufacturer = manufacturer + parallel_set.save() + + +def initialize_insert_sets(): + """ + Initialize collection InsertSet with data. + """ + for insert_set in InsertSet.objects.all(): + insert_set.delete() + manufacturer = Manufacturer.objects.get({'name': 'Fleer'}) + insert_set = InsertSet() + insert_set.name = 'Mystique Big Buzz' + insert_set.manufacturer = manufacturer + insert_set.save() + + +def initialize_cards(): + """ + Initialize collection Card with data. + """ + for card in Card.objects.all(): + card.delete() + players = ['Pathon, Jerome', 'Bruschi, Tedy', 'Couch, Tim', 'Shea, Aaron', + 'Lewis, Jamal', 'Lewis, Jermaine', 'Banks, Tony', 'Fuamatu-Ma\'Afala, Chris', + 'Bettis, Jerome', 'Stewart, Kordell', 'Moon, Warren', 'Lockett, Kevin', + 'Gannon, Rich', 'Jett, James', 'Strong, Mack', 'Huard, Brock', + 'Watters, Ricky', 'Aikman, Troy', 'LaFleur, David', 'Brazzell, Chris', + 'Dayne, Ron', 'Brown, Na', 'Small, Torrance', 'Lewis, Chad', 'Murrell, Adrian', + 'Smith, Maurice', 'Chandler, Chris', 'Kanell, Danny', 'Williams, Ricky', + 'Garcia, Jeff', 'Streets, Tai', 'Garner, Charlie', 'Rice, Jerry', + 'Owens, Terrell', 'Bruce, Isaac', 'Canidate, Trung'] + cards = [ + [0, 'Indianapolis Colts', 'Pacific', 'Pacific', None, None, False, 2001, 185], + [1, 'Indianapolis Colts', 'Pacific', 'Pacific', None, None, False, 2001, 250], + [2, 'Cleveland Browns', 'Pacific', 'Pacific', None, None, False, 2001, 103], + [3, 'Cleveland Browns', 'Pacific', 'Pacific', None, None, False, 2001, 112], + [4, 'Baltimore Ravens', 'Pacific', 'Pacific', None, None, False, 2001, 37], + [5, 'Baltimore Ravens', 'Pacific', 'Pacific', None, None, False, 2001, 38], + [6, 'Baltimore Ravens', 'Pacific', 'Pacific', None, None, False, 2001, 31], + [7, 'Pittsburgh Steelers', 'Pacific', 'Pacific', None, None, False, 2001, 338], + [8, 'Pittsburgh Steelers', 'Pacific', 'Pacific', None, None, False, 2001, 335], + [9, 'Pittsburgh Steelers', 'Pacific', 'Pacific', None, None, False, 2001, 345], + [10, 'Kansas City Chiefs', 'Pacific', 'Pacific', None, None, False, 2001, 213], + [11, 'Kansas City Chiefs', 'Pacific', 'Pacific', None, None, False, 2001, 212], + [12, 'Oakland Raiders', 'Pacific', 'Pacific', None, None, False, 2001, 311], + [13, 'Oakland Raiders', 'Pacific', 'Pacific', None, None, False, 2001, 312], + [14, 'Seattle Seahawks', 'Pacific', 'Pacific', None, None, False, 2001, 403], + [15, 'Seattle Seahawks', 'Pacific', 'Pacific', None, None, False, 2001, 397], + [16, 'Seattle Seahawks', 'Pacific', 'Pacific', None, None, False, 2001, 404], + [17, 'Dallas Cowboys', 'Pacific', 'Pacific', None, None, False, 2001, 116], + [18, 'Dallas Cowboys', 'Pacific', 'Pacific', None, None, False, 2001, 122], + [19, 'Dallas Cowboys', 'Pacific', 'Pacific', None, None, False, 2001, 117], + [20, 'New York Giants', 'Pacific', 'Pacific', None, None, False, 2001, 281], + [21, 'New York Giants', 'Pacific', 'Pacific', None, None, False, 2001, 321], + [22, 'Philadelphia Eagles', 'Pacific', 'Pacific', None, None, False, 2001, 331], + [23, 'Philadelphia Eagles', 'Pacific', 'Pacific', None, None, False, 2001, 324], + [24, 'Washington Redskins', 'Pacific', 'Pacific', None, None, False, 2001, 445], + [25, 'Atlanta Falcons', 'Pacific', 'Pacific', None, None, False, 2001, 28], + [26, 'Atlanta Falcons', 'Pacific', 'Pacific', None, None, False, 2001, 17], + [27, 'Atlanta Falcons', 'Pacific', 'Pacific', None, None, False, 2001, 23], + [28, 'New Orleans Saints', 'Pacific', 'Pacific', None, None, False, 2001, 273], + [29, 'San Francisco 49ers', 'Pacific', 'Pacific', None, None, False, 2001, 380], + [30, 'San Francisco 49ers', 'Pacific', 'Pacific', None, None, False, 2001, 390], + [31, 'San Francisco 49ers', 'Pacific', 'Pacific', None, None, False, 2001, 381], + [32, 'San Francisco 49ers', 'Pacific', 'Pacific', None, None, False, 2001, 387], + [33, 'San Francisco 49ers', 'Pacific', 'Pacific', None, None, False, 2001, 386], + [34, 'St.Louis Rams', 'Pacific', 'Pacific', None, None, False, 2001, 349], + [35, 'St.Louis Rams', 'Pacific', 'Pacific', None, None, False, 2001, 350], + ] + for card_data in cards: + card = Card() + player_name = players[card_data[0]] + (last_name, first_name) = player_name.split(',') + card.player = Player.objects.get( + {'last_name': last_name.strip(), 'first_name': first_name.strip()} + ) + card.team = Team.objects.get({'name': card_data[1]}) + card.manufacturer = Manufacturer.objects.get({'name': card_data[2]}) + card.card_set = CardSet.objects.get({'name': card_data[3]}) + card.parallel_set = card_data[4] + card.insert_set = card_data[5] + card.rookie = card_data[6] + card.year = card_data[7] + card.number = card_data[8] + card.save() diff --git a/flask/kontor/tysc/forms.py b/flask/kontor/tysc/forms.py new file mode 100644 index 0000000..9cb5c3c --- /dev/null +++ b/flask/kontor/tysc/forms.py @@ -0,0 +1,76 @@ +""" +Define form to edit sport, teams, player, manufacturers and card types +""" +from flask_wtf import FlaskForm +from wtforms import StringField, SubmitField, SelectField, IntegerField, BooleanField +from wtforms.validators import DataRequired, Optional, NumberRange +from bson import ObjectId + + +class SportForm(FlaskForm): + """ + Form to add and edit a Sport + """ + name = StringField('Name', validators=[DataRequired()]) + submit = SubmitField('Submit') + + +class TeamForm(FlaskForm): + """ + Form to add and edit a team + """ + name = StringField('Name', validators=[DataRequired()]) + shortname = StringField('Shortname', validators=[DataRequired()]) + sport = SelectField('Sport', coerce=ObjectId) + submit = SubmitField('Submit') + + +class PlayerForm(FlaskForm): + """ + Form to add and edit a player. + """ + first_name = StringField('First name', validators=[DataRequired()]) + last_name = StringField('Last name', validators=[DataRequired()]) + submit = SubmitField('Submit') + + +class PositionForm(FlaskForm): + """ + Form to add and edit field positions for sports + """ + name = StringField('Name', validators=[DataRequired()]) + description = StringField('Description', validators=[DataRequired()]) + sport = SelectField('Sport', coerce=ObjectId) + submit = SubmitField('Submit') + + +class ManufacturerForm(FlaskForm): + """ + Form to add and edit a card manufacturer. + """ + name = StringField('Name', validators=[DataRequired()]) + submit = SubmitField('Submit') + + +class CardSetForm(FlaskForm): + """ + Form to add and edit a regular card set + """ + name = StringField('Name', validators=[DataRequired()]) + manufacturer = SelectField('Manufacturer', coerce=ObjectId) + submit = SubmitField('Submit') + + +class CardForm(FlaskForm): + """ + Form to add and edit a trading card + """ + player = SelectField('Player', coerce=ObjectId) + team = SelectField('Team', coerce=ObjectId) + manufacturer = SelectField('Manufacturer', coerce=ObjectId) + card_set = SelectField('Card Set', coerce=ObjectId) + parallel_set = SelectField('Parallel Set', coerce=ObjectId, validators=[Optional()]) + insert_set = SelectField('Inserts', coerce=ObjectId, validators=[Optional()]) + rookie = BooleanField('Rookie', validators=[DataRequired()]) + year = IntegerField('Year', validators=[NumberRange(min=1956, max=2020)]) + number = IntegerField('Number', validators=[DataRequired()]) diff --git a/flask/kontor/tysc/models.py b/flask/kontor/tysc/models.py new file mode 100644 index 0000000..02dc341 --- /dev/null +++ b/flask/kontor/tysc/models.py @@ -0,0 +1,155 @@ +"""This modules declares the model for TradingCards related information.""" +from pymongo.write_concern import WriteConcern +from pymodm import MongoModel, fields + + +class Sport(MongoModel): + """Class Sport represents a sport.""" + name = fields.CharField() + + def __str__(self): + """Returns printable version of Sport object.""" + return self.name + + def __repr__(self): + """Returns printable version of Sport object.""" + return "Sport({})".format(self.name) + + class Meta: + """Sets the connection and connections details.""" + connection_alias = 'kontor' + write_concern = WriteConcern(j=True) + + +class Position(MongoModel): + """Class Position represents the position of a player for a sport.""" + name = fields.CharField(max_length=4) + description = fields.CharField(max_length=30) + sport = fields.ReferenceField(Sport) + + def __str__(self): + """Returns printable version of Position object.""" + return "{0}({1})".format(self.name, self.description) + + def __repr__(self): + """Returns printable version of Position object.""" + return "Position({0}, {1})".format(self.name, self.description) + + class Meta: + """Sets the connection and connections details.""" + connection_alias = 'kontor' + write_concern = WriteConcern(j=True) + + +class Team(MongoModel): + """Class Team represents a team for a sport.""" + name = fields.CharField(max_length=60) + shortname = fields.CharField(max_length=30) + sport = fields.ReferenceField(Sport, blank=True) + + def __str__(self): + """Returns printable version of Team object.""" + return self.name + + def __repr__(self): + """Returns printable version of Team object.""" + return "Team({0}{1})".format(self.name, self.shortname) + + class Meta: + """Sets the connection and connections details.""" + connection_alias = 'kontor' + write_concern = WriteConcern(j=True) + + +class Player(MongoModel): + """ + Class Player represents a player. + """ + first_name = fields.CharField(max_length=60) + last_name = fields.CharField(max_length=60) + + def __str__(self): + """Returns printable version of Team object.""" + return "{0} {1}".format(self.first_name, self.last_name) + + def __repr__(self): + """Returns printable version of Team object.""" + return "Player({0} {1})".format(self.first_name, self.last_name) + + class Meta: + """Sets the connection and connections details.""" + connection_alias = 'kontor' + write_concern = WriteConcern(j=True) + + +class Manufacturer(MongoModel): + """ + Class Manufacturer represents a manufacturer of trading cards. + """ + name = fields.CharField() + + class Meta: + """Sets the connection and connections details.""" + connection_alias = 'kontor' + write_concern = WriteConcern(j=True) + + +class CardSet(MongoModel): + """ + Class CardSet represents the regular card set. + """ + name = fields.CharField() + manufacturer = fields.ReferenceField(Manufacturer) + + class Meta: + """Sets the connection and connections details.""" + connection_alias = 'kontor' + write_concern = WriteConcern(j=True) + + +class ParallelSet(MongoModel): + """ + Class CardSet represents the parallel card set. + """ + name = fields.CharField() + manufacturer = fields.ReferenceField(Manufacturer) + + class Meta: + """Sets the connection and connections details.""" + connection_alias = 'kontor' + write_concern = WriteConcern(j=True) + + +class InsertSet(MongoModel): + """ + Class CardSet represents the inserts card set. + """ + name = fields.CharField() + manufacturer = fields.ReferenceField(Manufacturer) + + class Meta: + """Sets the connection and connections details.""" + connection_alias = 'kontor' + write_concern = WriteConcern(j=True) + + +class Card(MongoModel): + """ + Class CardSet represents the regular card set. + """ + # pylint: disable=too-many-instance-attributes + # Nine is reasonable in this case. + player = fields.ReferenceField(Player) + team = fields.ReferenceField(Team) + manufacturer = fields.ReferenceField(Manufacturer) + card_set = fields.ReferenceField(CardSet, blank=True) + parallel_set = fields.ReferenceField(ParallelSet, blank=True) + insert_set = fields.ReferenceField(InsertSet, blank=True) + rookie = fields.BooleanField(default=False) + year = fields.IntegerField(min_value=1956, max_value=2020) + number = fields.IntegerField() + + class Meta: + """Sets the connection and connections details.""" + connection_alias = 'kontor' + write_concern = WriteConcern(j=True) diff --git a/flask/kontor/tysc/views.py b/flask/kontor/tysc/views.py new file mode 100644 index 0000000..836b295 --- /dev/null +++ b/flask/kontor/tysc/views.py @@ -0,0 +1,405 @@ +""" +Define BLueprint and Views for TradeYourSportsCards +""" +from flask import Blueprint, url_for, render_template, redirect, flash +from flask_login import login_required +from bson import ObjectId +from pymongo.errors import PyMongoError +from .forms import SportForm, TeamForm, PlayerForm, PositionForm +from .forms import ManufacturerForm +from .models import Sport, Team, Player, Position +from .models import Manufacturer, CardSet, ParallelSet, InsertSet, Card + + +TYSC = Blueprint('tysc', __name__) + + +@TYSC.route('/sport') +@login_required +def list_sports(): + """ + List sports. + :return: + """ + sports = Sport.objects.all() + return render_template('tysc/sports.html', sports=sports, title="Sports") + + +@TYSC.route('/sport/edit/', methods=['GET', 'POST']) +@login_required +def edit_sport(sport_id): + """ + Edit a sport + """ + sport = Sport.objects.get({'_id': ObjectId(sport_id)}) + form = SportForm(obj=sport) + if form.validate_on_submit(): + sport.name = form.name.data + sport.save() + flash('You have successfully edited the sport.') + return redirect(url_for('tysc.list_sports')) + form.name.data = sport.name + return render_template('simpleform.html', action="Edit", + form=form, sport=sport, title="Edit Sport") + + +@TYSC.route('/sport/add', methods=['GET', 'POST']) +@login_required +def add_sport(): + """ + Add a sport + :return: + """ + form = SportForm() + if form.validate_on_submit(): + sport = Sport() + sport.name = form.name.data + try: + sport.save() + flash('You have successfully added a new sport.') + except PyMongoError: + flash('Error: sport name already exists.') + return redirect(url_for('tysc.list_sports')) + return render_template('simpleform.html', action="Add", + form=form, title="Add Sport") + + +@TYSC.route('/sport/delete/', methods=['GET', 'POST']) +@login_required +def delete_sport(sport_id): + """ + Delete a sport + :param sport_id: + :return: + """ + sport = Sport.objects.raw({'_id': ObjectId(sport_id)}) + if sport: + sport.delete() + flash('You have successfully deleted the sport.') + return redirect(url_for('tysc.list_sports')) + + +@TYSC.route('/team') +@login_required +def list_teams(): + """ + List teams. + :return: + """ + teams = Team.objects.all() + return render_template('tysc/teams.html', teams=teams, title="Teams") + + +@TYSC.route('/team/edit/', methods=['GET', 'POST']) +@login_required +def edit_team(team_id): + """ + Edit a team + """ + team = Team.objects.get({'_id': ObjectId(team_id)}) + form = TeamForm(obj=team) + form.sport.choices = [(s.pk, s.name) for s in Sport.objects.all()] + if team.sport: + form.sport.default = team.sport.pk + form.sport.process_data(team.sport.pk) + if form.validate_on_submit(): + team.name = form.name.data + team.shortname = form.shortname.data + team.sport = form.sport.data + team.save() + flash('You have successfully edited the team.') + return redirect(url_for('tysc.list_teams')) + form.name.data = team.name + return render_template('simpleform.html', action="Edit", + form=form, team=team, title="Edit Team") + + +@TYSC.route('/team/add', methods=['GET', 'POST']) +@login_required +def add_team(): + """ + Add a team + :return: + """ + form = TeamForm() + form.sport.choices = [(s.pk, s.name) for s in Sport.objects.all()] + if form.validate_on_submit(): + team = Team() + team.name = form.name.data + team.shortname = form.shortname.data + team.sport = form.sport.data + try: + team.save() + flash('You have successfully added a new team.') + except PyMongoError: + flash('Error: team name already exists.') + return redirect(url_for('tysc.list_teams')) + return render_template('simpleform.html', action="Add", + form=form, title="Add Team") + + +@TYSC.route('/team/delete/', methods=['GET', 'POST']) +@login_required +def delete_team(team_id): + """ + Delete a team + :param team_id: + :return: + """ + team = Team.objects.raw({'_id': ObjectId(team_id)}) + if team: + team.delete() + return redirect(url_for('tysc.list_teams')) + + +@TYSC.route('/position') +@login_required +def list_positions(): + """ + List positions. + :return: + """ + positions = Position.objects.all() + return render_template('tysc/positions.html', positions=positions, title="Positions") + + +@TYSC.route('/position/edit/', methods=['GET', 'POST']) +@login_required +def edit_position(position_id): + """ + Edit a position + """ + position = Position.objects.get({'_id': ObjectId(position_id)}) + form = PositionForm(obj=position) + form.sport.choices = [(s.pk, s.name) for s in Sport.objects.all()] + if position.sport: + form.sport.default = position.sport.pk + form.sport.process_data(position.sport.pk) + if form.validate_on_submit(): + position.name = form.name.data + position.description = form.description.data + position.sport = form.sport.data + position.save() + flash('You have successfully edited the position.') + return redirect(url_for('tysc.list_positions')) + form.name.data = position.name + form.description.data = position.description + return render_template('simpleform.html', action="Edit", + form=form, position=position, title="Edit position") + + +@TYSC.route('/position/add', methods=['GET', 'POST']) +@login_required +def add_position(): + """ + Add a position + :return: + """ + form = PositionForm() + form.sport.choices = [(s.pk, s.name) for s in Sport.objects.all()] + if form.validate_on_submit(): + position = Position() + position.name = form.name.data + position.description = form.description.data + position.sport = form.sport.data + try: + position.save() + flash('You have successfully added a new position.') + except PyMongoError: + flash('Error: team position already exists.') + return redirect(url_for('tysc.list_positions')) + return render_template('simpleform.html', action="Add", + form=form, title="Add Position") + + +@TYSC.route('/position/delete/', methods=['GET', 'POST']) +@login_required +def delete_position(position_id): + """ + Delete a position + :param position_id: + :return: + """ + position = Position.objects.raw({'_id': ObjectId(position_id)}) + if position: + position.delete() + return redirect(url_for('tysc.list_positions')) + + +@TYSC.route('/player') +@login_required +def list_players(): + """ + List players. + :return: + """ + players = Player.objects.all() + return render_template('tysc/players.html', players=players, title="Players") + + +@TYSC.route('/player/edit/', methods=['GET', 'POST']) +@login_required +def edit_player(player_id): + """ + Edit a player + """ + player = Player.objects.get({'_id': ObjectId(player_id)}) + form = PlayerForm(obj=player) + if form.validate_on_submit(): + player.first_name = form.first_name.data + player.last_name = form.last_name.data + player.save() + flash('You have successfully edited the player.') + return redirect(url_for('tysc.list_players')) + form.first_name.data = player.first_name + form.last_name.data = player.last_name + return render_template('simpleform.html', action="Edit", + form=form, player=player, title="Edit player") + + +@TYSC.route('/player/add', methods=['GET', 'POST']) +@login_required +def add_player(): + """ + Add a player + :return: + """ + form = PlayerForm() + if form.validate_on_submit(): + player = Player() + player.first_name = form.first_name.data + player.last_name = form.last_name.data + try: + player.save() + flash('You have successfully added a new player.') + except PyMongoError: + flash('Error: player name already exists.') + return redirect(url_for('tysc.list_players')) + return render_template('simpleform.html', action="Add", + form=form, title="Add player") + + +@TYSC.route('/player/delete/', methods=['GET', 'POST']) +@login_required +def delete_player(player_id): + """ + Delete a player + :param player_id: + :return: + """ + player = Player.objects.raw({'_id': ObjectId(player_id)}) + if player: + player.delete() + return redirect(url_for('tysc.list_players')) + + +@TYSC.route('/manufacturer') +@login_required +def list_manufacturers(): + """ + List manufacturers. + :return: + """ + manufacturers = Manufacturer.objects.all() + return render_template('tysc/manufacturers.html', + manufacturers=manufacturers, + title="Manufacturers") + + +@TYSC.route('/manufacturer/edit/', methods=['GET', 'POST']) +@login_required +def edit_manufacturer(manufacturer_id): + """ + Edit a manufacturer + """ + manufacturer = Manufacturer.objects.get({'_id': ObjectId(manufacturer_id)}) + form = ManufacturerForm(obj=manufacturer) + if form.validate_on_submit(): + manufacturer.name = form.name.data + manufacturer.save() + flash('You have successfully edited the manufacturer.') + return redirect(url_for('tysc.list_manufacturers')) + form.name.data = manufacturer.name + return render_template('simpleform.html', action="Edit", + form=form, manufacturer=manufacturer, title="Edit Manufacturer") + + +@TYSC.route('/manufacturer/add', methods=['GET', 'POST']) +@login_required +def add_manufacturer(): + """ + Add a manufacturer + :return: + """ + form = ManufacturerForm() + if form.validate_on_submit(): + manufacturer = Manufacturer() + manufacturer.name = form.name.data + try: + manufacturer.save() + flash('You have successfully added a new manufacturer.') + except PyMongoError: + flash('Error: manufacturer name already exists.') + return redirect(url_for('tysc.list_manufacturers')) + return render_template('simpleform.html', action="Add", + form=form, title="Add Manufacturer") + + +@TYSC.route('/manufacturer/delete/', methods=['GET', 'POST']) +@login_required +def delete_manufacturer(manufacturer_id): + """ + Delete a manufacturer + :param manufacturer_id: + :return: + """ + manufacturer = Manufacturer.objects.raw({'_id': ObjectId(manufacturer_id)}) + if manufacturer: + manufacturer.delete() + flash('You have successfully deleted the manufacturer.') + return redirect(url_for('tysc.list_manufacturers')) + + +@TYSC.route('/cardsets') +@login_required +def list_card_sets(): + """ + List card sets. + :return: + """ + card_sets = CardSet.objects.all() + return render_template('tysc/cardsets.html', card_sets=card_sets, title="Card Sets") + + +@TYSC.route('/parallelsets') +@login_required +def list_parallel_sets(): + """ + List card sets. + :return: + """ + parallel_sets = ParallelSet.objects.all() + return render_template('tysc/parallelsets.html', + parallel_sets=parallel_sets, title="Parallel Sets") + + +@TYSC.route('/insertsets') +@login_required +def list_insert_sets(): + """ + List card sets. + :return: + """ + insert_sets = InsertSet.objects.all() + return render_template('tysc/insertsets.html', insert_sets=insert_sets, title="Inserts") + + +@TYSC.route('/cards') +@login_required +def list_cards(): + """ + List card sets. + :return: + """ + cards = Card.objects.all() + return render_template('tysc/cards.html', cards=cards, title="Cards") diff --git a/flask/kontor/version.py b/flask/kontor/version.py new file mode 100644 index 0000000..044c225 --- /dev/null +++ b/flask/kontor/version.py @@ -0,0 +1,2 @@ +"""This module declares the version of the Kontor application.""" +__version__ = '0.0.7' diff --git a/flask/pylint.cfg b/flask/pylint.cfg new file mode 100644 index 0000000..e08fe3a --- /dev/null +++ b/flask/pylint.cfg @@ -0,0 +1,382 @@ +[MASTER] + +# Specify a configuration file. +#rcfile= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +init-hook='sys.path = list(); sys.path.append("./lib/python3.4/site-packages/")' + +# Profiled execution. +profile=no + +# Add files or directories to the blacklist. They should be base names, not +# paths. +ignore=CVS + +# Pickle collected data for later comparisons. +persistent=yes + +# List of plugins (as comma separated values of python modules names) to load, +# usually to register additional checkers. +load-plugins= + +# Use multiple processes to speed up Pylint. +jobs=1 + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code +extension-pkg-whitelist= + +# Allow optimization of some AST trees. This will activate a peephole AST +# optimizer, which will apply various small optimizations. For instance, it can +# be used to obtain the result of joining multiple strings with the addition +# operator. Joining a lot of strings can lead to a maximum recursion error in +# Pylint and this flag can prevent that. It has one side effect, the resulting +# AST will be different than the one from reality. +optimize-ast=no + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED +confidence= + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time. See also the "--disable" option for examples. +#enable= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once).You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use"--disable=all --enable=classes +# --disable=W" +disable=W1601,W1606,W1604,W1630,E1605,E1604,W1636,W1603,W1633,W1610,W1615,W1626,E1606,W0704,I0020,W1622,E1608,W1612,W1638,W1614,W1616,W1605,W1613,W1634,E1603,I0021,W1640,W1621,W1625,E1602,W1639,W1602,W1620,W1617,W1624,W1609,W1618,E1601,E1607,W1637,W1632,W1629,W1635,W1611,W1623,W1608,W1627,W1628,W1607,W1619,E1101,R0201,R0903,R0801,locally-disabled + + +[REPORTS] + +# Set the output format. Available formats are text, parseable, colorized, msvs +# (visual studio) and html. You can also give a reporter class, eg +# mypackage.mymodule.MyReporterClass. +output-format=parseable + +# Put messages in a separate file for each module / package specified on the +# command line instead of printing them on stdout. Reports (if any) will be +# written in a file name "pylint_global.[txt|html]". +files-output=no + +# Tells whether to display a full report or only the messages +reports=yes + +# Python expression which should return a note less than 10 (10 is the highest +# note). You have access to the variables errors warning, statement which +# respectively contain the number of errors / warnings messages and the total +# number of statements analyzed. This is used by the global evaluation report +# (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Add a comment according to your evaluation note. This is used by the global +# evaluation report (RP0004). +comment=no + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details +#msg-template= + + +[SPELLING] + +# Spelling dictionary name. Available dictionaries: none. To make it working +# install python-enchant package. +spelling-dict= + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to indicated private dictionary in +# --spelling-private-dict-file option instead of raising a message. +spelling-store-unknown-words=no + + +[TYPECHECK] + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis +ignored-modules= + +# List of classes names for which member attributes should not be checked +# (useful for classes with attributes dynamically set). +ignored-classes=SQLObject + +# When zope mode is activated, add a predefined set of Zope acquired attributes +# to generated-members. +zope=no + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E0201 when accessed. Python regular +# expressions are accepted. +generated-members=REQUEST,acl_users,aq_parent + + +[LOGGING] + +# Logging modules to check that the string format arguments are in logging +# function parameter format +logging-modules=logging + + +[VARIABLES] + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# A regular expression matching the name of dummy variables (i.e. expectedly +# not used). +dummy-variables-rgx=_$|dummy + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid to define new builtins when possible. +additional-builtins= + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_,_cb + + +[FORMAT] + +# Maximum number of characters on a single line. +max-line-length=100 + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + +# List of optional constructs for which whitespace checking is disabled +no-space-check=trailing-comma,dict-separator + +# Maximum number of lines in a module +max-module-lines=1000 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + + +[BASIC] + +# Required attributes for module, separated by a comma +#required-attributes= + +# List of builtins function names that should not be used, separated by a comma +bad-functions=map,filter + +# Good variable names which should always be accepted, separated by a comma +good-names=i,j,k,ex,Run,_ + +# Bad variable names which should always be refused, separated by a comma +bad-names=foo,bar,baz,toto,tutu,tata + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Include a hint for the correct naming format with invalid-name +include-naming-hint=yes + +# Regular expression matching correct class attribute names +class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ + +# Naming hint for class attribute names +class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ + +# Regular expression matching correct function names +function-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for function names +function-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression matching correct attribute names +attr-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for attribute names +attr-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression matching correct method names +method-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for method names +method-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression matching correct argument names +argument-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for argument names +argument-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression matching correct inline iteration names +inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ + +# Naming hint for inline iteration names +inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ + +# Regular expression matching correct module names +module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ + +# Naming hint for module names +module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ + +# Regular expression matching correct constant names +const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ + +# Naming hint for constant names +const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ + +# Regular expression matching correct class names +class-rgx=[A-Z_][a-zA-Z0-9]+$ + +# Naming hint for class names +class-name-hint=[A-Z_][a-zA-Z0-9]+$ + +# Regular expression matching correct variable names +variable-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for variable names +variable-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=__.*__ + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME,XXX,TODO + + +[SIMILARITIES] + +# Minimum lines number of a similarity. +min-similarity-lines=4 + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + + +[CLASSES] + +# List of interface methods to ignore, separated by a comma. This is used for +# instance to not check methods defines in Zope's Interface base class. +#ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__,__new__,setUp + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=mcs + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict,_fields,_replace,_source,_make + + +[DESIGN] + +# Maximum number of arguments for function / method +max-args=5 + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore +ignored-argument-names=_.* + +# Maximum number of locals for function / method body +max-locals=15 + +# Maximum number of return / yield for function / method body +max-returns=6 + +# Maximum number of branch for function / method body +max-branches=12 + +# Maximum number of statements in function / method body +max-statements=50 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + + +[IMPORTS] + +# Deprecated modules which should not be used, separated by a comma +deprecated-modules=stringprep,optparse + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled) +import-graph= + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled) +ext-import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled) +int-import-graph= + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "Exception" +overgeneral-exceptions=Exception diff --git a/flask/requirements.txt b/flask/requirements.txt new file mode 100644 index 0000000..f596076 --- /dev/null +++ b/flask/requirements.txt @@ -0,0 +1,11 @@ +flask +flask-login +flask-bootstrap +Flask-WTF +flask-testing +pymodm +nose +nose-htmloutput +coverage +pylint + diff --git a/flask/tests/__init__.py b/flask/tests/__init__.py new file mode 100644 index 0000000..8506c57 --- /dev/null +++ b/flask/tests/__init__.py @@ -0,0 +1,38 @@ +from flask_testing import TestCase +from kontor import create_app +from kontor.models import User + + +class TestBase(TestCase): + + def create_app(self): + + # pass in test configuration + config_name = 'testing' + app = create_app(config_name) + return app + + def setUp(self): + """ + Will be called before every test + """ + # create test admin user + admin = User() + admin.username="admin" + admin.password="admin2016" + admin.is_admin=True + admin.save() + + # create test non-admin user + employee = User() + employee.username="test_user" + employee.password="test2016" + employee.save() + + def tearDown(self): + """ + Will be called after every test + """ + users = User.objects.all() + for user in users: + user.delete() diff --git a/flask/tests/test_comics_model.py b/flask/tests/test_comics_model.py new file mode 100644 index 0000000..2a5dcc5 --- /dev/null +++ b/flask/tests/test_comics_model.py @@ -0,0 +1,51 @@ +""" +This module cantains tests for the comic data model +""" +import unittest +from . import TestBase +from kontor.comics.models import Artist, Comic, Publisher + + +class TestComicsModel(TestBase): + """This TestCase contains tests for comic data model.""" + + def setUp(self): + publisher = Publisher() + publisher.name = "Publisher1" + publisher.save() + artist = Artist() + artist.name = "Artist1" + artist.save() + comic = Comic() + comic.title = "Title1" + comic.save() + + def tearDown(self): + for comic in Comic.objects.all(): + comic.delete() + for artist in Artist.objects.all(): + artist.delete() + for publisher in Publisher.objects.all(): + publisher.delete() + + def test_comic_model(self): + comic_list = Comic.objects.all() + self.assertEqual(comic_list.count(), 1) + + def test_publisher_model(self): + self.assertEqual(Publisher.objects.all().count(), 1) + + def test_artist_model(self): + self.assertEqual(Artist.objects.all().count(), 1) + + def test_assign_publisher(self): + comic = Comic.objects.first() + publisher = Publisher.objects.first() + comic.publisher = publisher + comic.save() + self.assertEqual(publisher.name, comic.publisher.name) + self.assertEqual(publisher.comics.count(), 1) + + +if __name__ == '__main__': + unittest.main() diff --git a/flask/tests/test_comics_view.py b/flask/tests/test_comics_view.py new file mode 100644 index 0000000..e33fd79 --- /dev/null +++ b/flask/tests/test_comics_view.py @@ -0,0 +1,44 @@ +"""This module contains tests for Comic related urls.""" +import unittest +from flask import url_for +from . import TestBase + + +class TestComicsViews(TestBase): + + def test_comics_index(self): + """ + Test that comics page is inaccessible without login + and redirects to login page then to comics page + """ + target_url = url_for('comic.list_comics') + redirect_url = url_for('auth.login', next=target_url) + response = self.client.get(target_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, redirect_url) + + def test_comics_artist(self): + """ + Test that comics page is inaccessible without login + and redirects to login page then to comics page + """ + target_url = url_for('comic.list_artists') + redirect_url = url_for('auth.login', next=target_url) + response = self.client.get(target_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, redirect_url) + + def test_comics_publisher(self): + """ + Test that comics page is inaccessible without login + and redirects to login page then to comics page + """ + target_url = url_for('comic.list_publishers') + redirect_url = url_for('auth.login', next=target_url) + response = self.client.get(target_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, redirect_url) + + +if __name__ == '__main__': + unittest.main() diff --git a/flask/tests/test_config.txt b/flask/tests/test_config.txt new file mode 100644 index 0000000..2b847b0 --- /dev/null +++ b/flask/tests/test_config.txt @@ -0,0 +1,4 @@ +# instance/config.py + +SECRET_KEY = 'p9Bv<3Eid9%$i01' + diff --git a/flask/tests/test_kontor_model.py b/flask/tests/test_kontor_model.py new file mode 100644 index 0000000..0120475 --- /dev/null +++ b/flask/tests/test_kontor_model.py @@ -0,0 +1,14 @@ +""" +This module cantains tests for the general Kontor data model. +""" +from . import TestBase +from kontor.models import User + + +class TestKontorModel(TestBase): + """This TestCase contains tests for users.""" + def test_user_model(self): + """ + Test number of records in User collection + """ + self.assertEqual(User.objects.all().count(), 2) diff --git a/flask/tests/test_kontor_view.py b/flask/tests/test_kontor_view.py new file mode 100644 index 0000000..101ae2f --- /dev/null +++ b/flask/tests/test_kontor_view.py @@ -0,0 +1,58 @@ +"""This module contains tests for Comic related urls.""" +import unittest +from flask import url_for +from . import TestBase + + +class TestKontorViews(TestBase): + + def test_homepage_view(self): + """ + Test that homepage is accessible without login + """ + response = self.client.get(url_for('home.homepage')) + self.assertEqual(response.status_code, 200) + + def test_login_view(self): + """ + Test that login page is accessible without login + """ + response = self.client.get(url_for('auth.login')) + self.assertEqual(response.status_code, 200) + + def test_logout_view(self): + """ + Test that logout link is inaccessible without login + and redirects to login page then to logout + """ + target_url = url_for('auth.logout') + redirect_url = url_for('auth.login', next=target_url) + response = self.client.get(target_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, redirect_url) + + def test_dashboard_view(self): + """ + Test that dashboard is inaccessible without login + and redirects to login page then to dashboard + """ + target_url = url_for('home.dashboard') + redirect_url = url_for('auth.login', next=target_url) + response = self.client.get(target_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, redirect_url) + + def test_admin_dashboard_view(self): + """ + Test that dashboard is inaccessible without login + and redirects to login page then to dashboard + """ + target_url = url_for('home.admin_dashboard') + redirect_url = url_for('auth.login', next=target_url) + response = self.client.get(target_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, redirect_url) + + +if __name__ == '__main__': + unittest.main() diff --git a/flask/tests/test_library_model.py b/flask/tests/test_library_model.py new file mode 100644 index 0000000..635bb58 --- /dev/null +++ b/flask/tests/test_library_model.py @@ -0,0 +1,49 @@ +""" +This module cantains tests for the comic data model +""" +import unittest +from . import TestBase +from kontor.library.models import Author, Publisher, Book + + +class TestComicsModel(TestBase): + """This TestCase contains tests for comic data model.""" + + def setUp(self): + publisher = Publisher() + publisher.name = "Publisher1" + publisher.save() + author = Author() + author.name = "Autor1" + author.save() + book = Book() + book.title = "Title1" + book.save() + + def tearDown(self): + for book in Book.objects.all(): + book.delete() + for author in Author.objects.all(): + author.delete() + for publisher in Publisher.objects.all(): + publisher.delete() + + def test_comic_model(self): + book_list = Book.objects.all() + self.assertEqual(book_list.count(), 1) + + def test_publisher_model(self): + self.assertEqual(Publisher.objects.all().count(), 1) + + def test_artist_model(self): + self.assertEqual(Author.objects.all().count(), 1) + + def test_assign_publisher(self): + book = Book.objects.first() + publisher = Publisher.objects.first() + book.publisher = publisher + book.save() + self.assertEqual(publisher.name, book.publisher.name) + +if __name__ == '__main__': + unittest.main() diff --git a/flask/tests/test_library_view.py b/flask/tests/test_library_view.py new file mode 100644 index 0000000..51b2c30 --- /dev/null +++ b/flask/tests/test_library_view.py @@ -0,0 +1,44 @@ +"""This module contains tests for Comic related urls.""" +import unittest +from flask import url_for +from . import TestBase + + +class TestLibraryViews(TestBase): + + def test_library_index(self): + """ + Test that comics page is inaccessible without login + and redirects to login page then to comics page + """ + target_url = url_for('library.list_books') + redirect_url = url_for('auth.login', next=target_url) + response = self.client.get(target_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, redirect_url) + + def test_library_author(self): + """ + Test that comics page is inaccessible without login + and redirects to login page then to comics page + """ + target_url = url_for('library.list_authors') + redirect_url = url_for('auth.login', next=target_url) + response = self.client.get(target_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, redirect_url) + + def test_library_publisher(self): + """ + Test that comics page is inaccessible without login + and redirects to login page then to comics page + """ + target_url = url_for('library.list_publishers') + redirect_url = url_for('auth.login', next=target_url) + response = self.client.get(target_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, redirect_url) + + +if __name__ == '__main__': + unittest.main() diff --git a/flask/tests/test_tysc_model.py b/flask/tests/test_tysc_model.py new file mode 100644 index 0000000..228cc49 --- /dev/null +++ b/flask/tests/test_tysc_model.py @@ -0,0 +1,53 @@ +""" +This module cantains tests for the TYSC data model +""" +import unittest +from . import TestBase +from kontor.tysc import initialize_model +from kontor.tysc.models import Sport, Position, Team, Player +from kontor.tysc.models import Manufacturer, CardSet, ParallelSet, InsertSet, Card + + +class TestTYSCModel(TestBase): + """ + This TestCase contains tests for TYSC data model. + """ + _initialize_db_ = True + + def setUp(self): + if self._initialize_db_: + initialize_model() + self._initialize_db_ = False + + def tearDown(self): + pass + + def test_sport_model(self): + self.assertEqual(Sport.objects.all().count(), 4) + + def test_position_model(self): + self.assertEqual(Position.objects.all().count(), 25) + + def test_team_model(self): + self.assertEqual(Team.objects.all().count(), 122) + + def test_manufacturer_model(self): + self.assertEqual(Manufacturer.objects.all().count(), 8) + + def test_cardset_model(self): + self.assertEqual(CardSet.objects.all().count(), 11) + + def test_parallelset_model(self): + self.assertEqual(ParallelSet.objects.all().count(), 3) + + def test_insertset_model(self): + self.assertEqual(InsertSet.objects.all().count(), 1) + + def test_playerset_model(self): + self.assertEqual(Player.objects.all().count(), 36) + + def test_card_model(self): + self.assertEqual(Card.objects.all().count(), 36) + +if __name__ == '__main__': + unittest.main() diff --git a/flask/tests/test_tysc_view.py b/flask/tests/test_tysc_view.py new file mode 100644 index 0000000..11fc222 --- /dev/null +++ b/flask/tests/test_tysc_view.py @@ -0,0 +1,110 @@ +"""This module contains tests for TradeYourSportsCards related urls.""" +import unittest +from flask import url_for +from . import TestBase + + +class TestTYSCViews(TestBase): + + def test_tysc_index(self): + """ + Test that TYSC page is inaccessible without login + and redirects to login page then to TYSC page + """ + target_url = url_for('tysc.list_cards') + redirect_url = url_for('auth.login', next=target_url) + response = self.client.get(target_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, redirect_url) + + def test_tysc_sport(self): + """ + Test that TYSC page is inaccessible without login + and redirects to login page then to TYSC page + """ + target_url = url_for('tysc.list_sports') + redirect_url = url_for('auth.login', next=target_url) + response = self.client.get(target_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, redirect_url) + + def test_tysc_team(self): + """ + Test that TYSC page is inaccessible without login + and redirects to login page then to TYSC page + """ + target_url = url_for('tysc.list_teams') + redirect_url = url_for('auth.login', next=target_url) + response = self.client.get(target_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, redirect_url) + + def test_tysc_player(self): + """ + Test that TYSC page is inaccessible without login + and redirects to login page then to TYSC page + """ + target_url = url_for('tysc.list_players') + redirect_url = url_for('auth.login', next=target_url) + response = self.client.get(target_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, redirect_url) + + def test_tysc_manufacturer(self): + """ + Test that TYSC page is inaccessible without login + and redirects to login page then to TYSC page + """ + target_url = url_for('tysc.list_manufacturers') + redirect_url = url_for('auth.login', next=target_url) + response = self.client.get(target_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, redirect_url) + + def test_tysc_card_set(self): + """ + Test that TYSC page is inaccessible without login + and redirects to login page then to TYSC page + """ + target_url = url_for('tysc.list_card_sets') + redirect_url = url_for('auth.login', next=target_url) + response = self.client.get(target_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, redirect_url) + + def test_tysc_parallel_set(self): + """ + Test that TYSC page is inaccessible without login + and redirects to login page then to TYSC page + """ + target_url = url_for('tysc.list_parallel_sets') + redirect_url = url_for('auth.login', next=target_url) + response = self.client.get(target_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, redirect_url) + + def test_tysc_insert_set(self): + """ + Test that TYSC page is inaccessible without login + and redirects to login page then to TYSC page + """ + target_url = url_for('tysc.list_insert_sets') + redirect_url = url_for('auth.login', next=target_url) + response = self.client.get(target_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, redirect_url) + + def test_tysc_card(self): + """ + Test that TYSC page is inaccessible without login + and redirects to login page then to TYSC page + """ + target_url = url_for('tysc.list_cards') + redirect_url = url_for('auth.login', next=target_url) + response = self.client.get(target_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, redirect_url) + + +if __name__ == '__main__': + unittest.main()