djsilcock 8 лет назад
Родитель
Сommit
3a0aa72def

+ 5 - 2
.travis.yml

@@ -2,14 +2,17 @@ language: python
 sudo: false
 sudo: false
 python:
 python:
   - "2.7"
   - "2.7"
-  - "3.3"
+  - "3.4"
+  - "3.5"
+  - "3.6"
 # command to install dependencies
 # command to install dependencies
 install:
 install:
   - "pip install -r requirements-dev.txt"
   - "pip install -r requirements-dev.txt"
   - "pip install coveralls"
   - "pip install coveralls"
+  - "pip install -e ."
 # command to run tests
 # command to run tests
 script:
 script:
-  - python manage.py compile_translations
+  - flaskbb translations compile
   - py.test --cov=flaskbb --cov-report=term-missing tests
   - py.test --cov=flaskbb --cov-report=term-missing tests
 after_success:
 after_success:
   - coveralls
   - coveralls

+ 9 - 0
MANIFEST.in

@@ -0,0 +1,9 @@
+include LICENSE AUTHORS CHANGES README.md celery_worker.py babel.cfg pytest.ini
+graft flaskbb
+graft tests
+graft migrations
+prune flaskbb/themes/*/node_modules
+prune flaskbb/themes/*/.sass-cache
+prune flaskbb/themes/*/src/*
+global-exclude __pycache__
+global-exclude *.py[co]

+ 5 - 4
Makefile

@@ -2,9 +2,9 @@
 
 
 help:
 help:
 	@echo "  clean      remove unwanted stuff"
 	@echo "  clean      remove unwanted stuff"
-	@echo "  install    install flaskbb and setup"
+	@echo "  install    install dependencies and flaskbb"
 	@echo "  test       run the testsuite"
 	@echo "  test       run the testsuite"
-	@echo "  run        run the development server"
+	@echo "  run        run the development server with the development config"
 	@echo "  docs       build the documentation"
 	@echo "  docs       build the documentation"
 
 
 dependencies:requirements.txt
 dependencies:requirements.txt
@@ -20,11 +20,12 @@ test:
 	py.test
 	py.test
 
 
 run:
 run:
-	python manage.py runserver -dr
+	flaskbb --config flaskbb.configs.development.DevelopmentConfig run
 
 
 install:dependencies
 install:dependencies
 	clear
 	clear
-	python manage.py install
+	pip install -e .
+	flaskbb install
 
 
 docs:
 docs:
 	$(MAKE) -C docs html
 	$(MAKE) -C docs html

+ 9 - 13
README.md

@@ -3,32 +3,28 @@
 [![Code Health](https://landscape.io/github/sh4nks/flaskbb/master/landscape.svg?style=flat)](https://landscape.io/github/sh4nks/flaskbb/master)
 [![Code Health](https://landscape.io/github/sh4nks/flaskbb/master/landscape.svg?style=flat)](https://landscape.io/github/sh4nks/flaskbb/master)
 [![License](https://img.shields.io/badge/license-BSD-blue.svg)](https://flaskbb.org)
 [![License](https://img.shields.io/badge/license-BSD-blue.svg)](https://flaskbb.org)
 
 
-# INTRODUCTION
+# Introduction
 
 
 [FlaskBB](http://flaskbb.org) is a forum software written in python
 [FlaskBB](http://flaskbb.org) is a forum software written in python
 using the micro framework Flask.
 using the micro framework Flask.
 
 
 
 
-## FEATURES
+## Features
 
 
 * A Bulletin Board like FluxBB or DjangoBB in Flask
 * A Bulletin Board like FluxBB or DjangoBB in Flask
 * Private Messages
 * Private Messages
 * Admin Interface
 * Admin Interface
 * Group based permissions
 * Group based permissions
-* BBCode Support
+* Markdown Support
 * Topic Tracker
 * Topic Tracker
 * Unread Topics/Forums
 * Unread Topics/Forums
 * i18n Support
 * i18n Support
 * Completely Themeable
 * Completely Themeable
 * Plugin System
 * Plugin System
+* Command Line Interface
 
 
 
 
-## TODO
-
-* See the github [issues](https://github.com/sh4nks/flaskbb/issues?state=open)
-
-
-## INSTALLATION
+## Quickstart
 
 
 For a complete installation guide please visit the installation documentation
 For a complete installation guide please visit the installation documentation
 [here](https://flaskbb.readthedocs.org/en/latest/installation.html).
 [here](https://flaskbb.readthedocs.org/en/latest/installation.html).
@@ -39,20 +35,20 @@ For a complete installation guide please visit the installation documentation
 * Install dependencies and FlaskBB
 * Install dependencies and FlaskBB
     * `make install`
     * `make install`
 * Run the development server
 * Run the development server
-    * `make runserver`
+    * `make run`
 * Visit [localhost:8080](http://localhost:8080)
 * Visit [localhost:8080](http://localhost:8080)
 
 
 
 
-## DOCUMENTATION
+## Documentation
 
 
 The documentation is located [here](http://flaskbb.readthedocs.org/en/latest/).
 The documentation is located [here](http://flaskbb.readthedocs.org/en/latest/).
 
 
 
 
-## LICENSE
+## License
 
 
 [BSD LICENSE](http://flask.pocoo.org/docs/license/#flask-license)
 [BSD LICENSE](http://flask.pocoo.org/docs/license/#flask-license)
 
 
 
 
-## ACKNOWLEDGEMENTS
+## Acknowledgements
 
 
 [/r/flask](http://reddit.com/r/flask), [Flask](http://flask.pocoo.org), it's [extensions](http://flask.pocoo.org/extensions/) and everyone who has helped me!
 [/r/flask](http://reddit.com/r/flask), [Flask](http://flask.pocoo.org), it's [extensions](http://flask.pocoo.org/extensions/) and everyone who has helped me!

+ 4 - 1
babel.cfg

@@ -1,4 +1,7 @@
+[ignore: .tox/**]
+[ignore: .venv/**]
 [ignore: **/plugins/**]
 [ignore: **/plugins/**]
-[python: **.py]
+[ignore: **/node_modules/**]
+[python: **/flaskbb/**.py]
 [jinja2: **/templates/**.html]
 [jinja2: **/templates/**.html]
 extensions=jinja2.ext.autoescape,jinja2.ext.with_
 extensions=jinja2.ext.autoescape,jinja2.ext.with_

+ 341 - 0
docs/cli.rst

@@ -0,0 +1,341 @@
+.. _commandline:
+
+Command Line Interface
+======================
+
+Here you can find the documentation about FlaskBB's Command Line Interface.
+
+To get help for a commands, just type ``flaskbb COMMAND --help``.
+If no command options or arguments are used it will display all available
+commands.
+
+.. sourcecode:: text
+
+    Usage: flaskbb [OPTIONS] COMMAND [ARGS]...
+
+      This is the commandline interface for flaskbb.
+
+    Options:
+      --version      Show the FlaskBB version.
+      --config TEXT  Specify the config to use in dotted module notation e.g.
+                     flaskbb.configs.default.DefaultConfig
+      --help         Show this message and exit.
+
+    Commands:
+      celery           Preconfigured wrapper around the 'celery' command.
+      db               Perform database migrations.
+      download-emojis  Downloads emojis from emoji-cheat-sheet.com.
+      install          Installs flaskbb.
+      plugins          Plugins command sub group.
+      populate         Creates the necessary tables and groups for FlaskBB.
+      reindex          Reindexes the search index.
+      run              Runs a development server.
+      shell            Runs a shell in the app context.
+      start            Starts a production ready wsgi server.
+      themes           Themes command sub group.
+      translations     Translations command sub group.
+      upgrade          Updates the migrations and fixtures.
+      urls             Show routes for the app.
+      users            Create, update or delete users.
+
+
+Commands
+--------
+
+Here you will find a detailed description of every command including all
+of their options and arguments.
+
+.. I am cheating here as i don't know how else to get rid of the warnings
+
+.. describe:: flaskbb install
+
+    Installs flaskbb. If no arguments are used, an interactive setup
+    will be run.
+
+    .. describe:: --welcome, -w
+
+        Disables the generation of the welcome forum.
+
+    .. describe:: --force, -f
+
+        Doesn't ask for confirmation if the database should be deleted or not.
+
+    .. describe:: --username USERNAME, -u USERNAME
+
+        The username of the user.
+
+    .. describe:: --email EMAIL, -e EMAIL
+
+        The email address of the user.
+
+    .. describe:: --password PASSWORD, -p PASSWORD
+
+        The password of the user.
+
+    .. describe:: --group GROUP, -g GROUP
+
+        The primary group of the user. The group ``GROUP`` has to be
+        one of ``admin``, ``super_mod``, ``mod`` or ``member``.
+
+.. describe:: flaskbb upgrade
+
+    Updates the migrations and fixtures.
+
+    .. describe:: --all, -a
+
+        Upgrades migrations AND fixtures to the latest version.
+
+    .. describe:: --fixture FIXTURE, -f FIXTURE
+
+        The fixture which should be upgraded or installed.
+        All fixtures have to be places inside flaskbb/fixtures/
+
+    .. describe:: --force-fixture, -ff
+
+        Forcefully upgrades the fixtures. WARNING: This will also overwrite
+        any settings.
+
+.. describe:: flaskbb populate
+
+    Creates the necessary tables and groups for FlaskBB.
+
+    .. describe:: --test-data, -t
+
+        Adds some test data.
+
+    .. describe:: --bulk-data, -b
+
+        Adds a lot of test data. Has to be used in combination with
+        ``--posts`` and ``--topics``.
+
+    .. describe:: --posts
+
+        Number of posts to create in each topic (default: 100).
+
+    .. describe:: --topics
+
+        Number of topics to create (default: 100).
+
+    .. describe:: --force, -f
+
+        Will delete the database without asking before populating it.
+
+    .. describe:: --initdb, -i
+
+        Initializes the database before populating it.
+
+.. describe:: flaskbb runserver
+
+    Starts the development server
+
+.. describe:: flaskbb start
+
+    Starts a production ready wsgi server.
+    Other versions of starting FlaskBB are still supported!
+
+    .. describe:: --server SERVER, -s SERVER
+
+        Defaults to ``gunicorn``. The following WSGI Servers are supported:
+            - gunicorn (default)
+            - gevent
+
+    .. describe:: --host HOST, -h HOST
+
+        The interface to bind FlaskBB to. Defaults to ``127.0.0.1``.
+
+    .. describe:: --port PORT, -p PORT
+
+        The port to bind FlaskBB to. Defaults to ``8000``.
+
+    .. describe:: --workers WORKERS, -w WORKERS
+
+        The number of worker processes for handling requests.
+        Defaults to ``4``.
+
+    .. describe:: --daemon, -d
+
+        Starts gunicorn in daemon mode.
+
+    .. describe:: --config, -c
+
+        The configuration file to use for the FlaskBB WSGI Application.
+
+.. describe:: flaskbb celery CELERY_ARGS
+
+    Starts celery. This is just a preconfigured wrapper around the ``celery``
+    command. Additional arguments are directly passed to celery.
+
+    .. describe:: --help-celery
+
+        Shows the celery help message.
+
+.. describe:: flaskbb shell
+
+    Creates a python shell with an app context.
+
+.. describe:: flaskbb urls
+
+    Lists all available routes.
+
+    .. describe:: --route, -r
+
+        Order by route.
+
+    .. describe:: --endpoint, -e
+
+        Order by endpoint
+
+    .. describe:: --methods, m
+
+        Order by methods
+
+.. describe:: flaskbb reindex
+
+    Reindexes the search index.
+
+.. describe:: flaskbb translations
+
+    Translations command sub group.
+
+    .. describe:: new LANGUAGE_CODE
+
+        Adds a new language to FlaskBB's translations.
+        The ``LANGUAGE_CODE`` is the short identifier for the language i.e.
+        '``en``', '``de``', '``de_AT``', etc.
+
+        .. describe:: --plugin PLUGIN_NAME, --p PLUGIN_NAME
+
+            Adds a new language to a plugin.
+
+    .. describe:: update
+
+        Updates the translations.
+
+        .. describe:: --all, -a
+
+            Updates all translations, including the ones from the plugins.
+
+        .. describe:: --plugin PLUGIN_NAME, --p PLUGIN_NAME
+
+            Update the language of the given plugin.
+
+    .. describe:: compile
+
+        Compiles the translations.
+
+        .. describe:: --all, -a
+
+            Compiles all translations, including the ones from the plugins.
+
+        .. describe:: --plugin PLUGIN_NAME, --p PLUGIN_NAME
+
+            Compiles only the given plugin translation.
+
+.. describe:: flaskbb plugins
+
+    Plugins command sub group.
+
+    .. describe:: new PLUGIN_IDENTIFIER
+
+        Creates a new plugin based on the cookiecutter plugin template.
+        Defaults to this template:
+        https://github.com/sh4nks/cookiecutter-flaskbb-plugin.
+        It will either accept a valid path on the filesystem
+        or a URL to a Git repository which contains the cookiecutter template.
+
+    .. describe:: install PLUGIN_IDENTIFIER
+
+        Installs a plugin by using the plugin's identifier.
+
+    .. describe:: uninstall PLUGIN_IDENTIFIER
+
+        Uninstalls a plugin by using the plugin's identifier.
+
+    .. describe:: remove PLUGIN_IDENTIFIER
+
+        Removes a plugin from the filesystem by using the plugin's identifier.
+
+        describe:: --force, -f
+
+            Removes the plugin without asking for confirmation first.
+
+    .. describe:: list
+
+        Lists all installed plugins.
+
+.. describe:: flaskbb themes
+
+    Themes command sub group.
+
+    .. describe:: new THEME_IDENTIFIER
+
+        Creates a new theme based on the cookiecutter theme
+        template. Defaults to this template:
+        https://github.com/sh4nks/cookiecutter-flaskbb-theme.
+        It will either accept a valid path on the filesystem
+        or a URL to a Git repository which contains the cookiecutter template.
+
+    .. describe:: remove THEME_IDENTIFIER
+
+        Removes a theme from the filesystem by the theme's identifier.
+
+    .. describe:: list
+
+        Lists all installed themes.
+
+.. describe:: flaskbb users
+
+    Creates a new user. If an option is missing, you will be interactivly
+    prompted to type it.
+
+    .. describe:: new
+
+        Creates a new user.
+
+        .. describe:: --username USERNAME, -u USERNAME
+
+            The username of the user.
+
+        .. describe:: --email EMAIL, -e EMAIL
+
+            The email address of the user.
+
+        .. describe:: --password PASSWORD, -p PASSWORD
+
+            The password of the user.
+
+        .. describe:: --group GROUP, -g GROUP
+
+            The primary group of the user. The group ``GROUP`` has to be
+            one of ``admin``, ``super_mod``, ``mod`` or ``member``.
+
+    .. describe:: update
+
+        Updates an user.
+
+        .. describe:: --username USERNAME, -u USERNAME
+
+            The username of the user.
+
+        .. describe:: --email EMAIL, -e EMAIL
+
+            The email address of the user.
+
+        .. describe:: --password PASSWORD, -p PASSWORD
+
+            The password of the user.
+
+        .. describe:: --group GROUP, -g GROUP
+
+            The primary group of the user. The group ``GROUP`` has to be
+            one of ``admin``, ``super_mod``, ``mod`` or ``member``.
+
+    .. describe:: delete
+
+        .. describe:: --username USERNAME, -u USERNAME
+
+            The username of the user.
+
+        .. describe:: --force, -f
+
+            Removes the user without asking for confirmation first.

+ 2 - 1
docs/contents.rst.inc

@@ -5,10 +5,11 @@ Contents
    :maxdepth: 2
    :maxdepth: 2
 
 
    installation
    installation
+   theming
+   cli
    plugins
    plugins
    plugin_tutorial/index
    plugin_tutorial/index
    events
    events
-   theming
    settings
    settings
    permissions
    permissions
    models
    models

+ 3 - 1
docs/index.rst

@@ -3,7 +3,9 @@
 Welcome to FlaskBB
 Welcome to FlaskBB
 ==================
 ==================
 
 
-FlaskBB is a lightweight forum software in Flask.
+FlaskBB is a classic forum software in a modern and fresh look.
+It is written in Python using the web framework Flask.
+FlaskBB is being distributed under the BSD 3-Clause License.
 
 
 
 
 Links
 Links

+ 235 - 65
docs/installation.rst

@@ -4,46 +4,57 @@ Installation
 -  `Basic Setup <#basic-setup>`_
 -  `Basic Setup <#basic-setup>`_
 -  `Configuration <#configuration>`_
 -  `Configuration <#configuration>`_
 -  `Deploying <#deploying>`_
 -  `Deploying <#deploying>`_
+-  `Deploying to PythonAnywhere <#deploying-to-pythonanywhere>`_
 
 
 
 
 
 
 Basic Setup
 Basic Setup
 -----------
 -----------
 
 
+We recommend installing FlaskBB in an isolated Python environment. This can be
+achieved with `virtualenv`_. In our little guide we will use a wrapper around
+virtualenv - the `virtualenvwrapper`_. In addition to virtualenv, we will also
+use the package manager `pip`_ to install the dependencies for FlaskBB.
+
+
 Virtualenv Setup
 Virtualenv Setup
 ~~~~~~~~~~~~~~~~
 ~~~~~~~~~~~~~~~~
 
 
-Before you can start, you need to create a `virtualenv`.
-You can install the virtualenvwrapper with your package manager or via pip.
-Be sure that pip is installed. If you don't know how to install pip, have a
-look at their `documentation <http://www.pip-installer.org/en/latest/installing.html>`_.
+The easiest way to install `virtualenv`_ and
+`virtualenvwrapper`_ is, to use the package manager on your system (if you
+are running Linux) to install them.
 
 
-For example, on archlinux you can install it with
-::
+For example, on archlinux you can install them with::
 
 
     $ sudo pacman -S python2-virtualenvwrapper
     $ sudo pacman -S python2-virtualenvwrapper
 
 
-or, if you own a Mac, you can simply install it with
-::
+or, on macOS, you can install them with::
 
 
     $ sudo pip install virtualenvwrapper
     $ sudo pip install virtualenvwrapper
 
 
-For more information checkout the  `virtualenvwrapper <http://virtualenvwrapper.readthedocs.org/en/latest/install.html#basic-installation>`_ installation.
+It's sufficient to just install the virtualenvwrapper because it depends on
+virtualenv and the package manager will resolve all the dependncies for you.
 
 
-After that you can create your virtualenv with
-::
+After that, you can create your virtualenv with::
 
 
     $ mkvirtualenv -a /path/to/flaskbb -p $(which python2) flaskbb
     $ mkvirtualenv -a /path/to/flaskbb -p $(which python2) flaskbb
 
 
-and you should be switched automatically to your newly created virtualenv.
+This will create a virtualenv named ``flaskbb`` using the python interpreter in
+version 2 and it will set your project directory to ``/path/to/flaskbb``.
+This comes handy when typing ``workon flaskbb`` as it will change your
+current directory automatically to ``/path/to/flaskbb``.
 To deactivate it you just have to type ``deactivate`` and if you want to work
 To deactivate it you just have to type ``deactivate`` and if you want to work
-on it again, you need to type ``workon flaskbb``.
+on it again, just type ``workon flaskbb``.
+
+If you want to know more about those isolated python environments, checkout
+the `virtualenv`_ and `virtualenvwrapper`_ docs.
 
 
 
 
 Required Dependencies
 Required Dependencies
 ~~~~~~~~~~~~~~~~~~~~~
 ~~~~~~~~~~~~~~~~~~~~~
 
 
-Now you can install the required dependencies.
+Now that you have set up your environment, you are ready to install the
+dependencies.
 ::
 ::
 
 
     $ pip install -r requirements.txt
     $ pip install -r requirements.txt
@@ -57,16 +68,21 @@ Alternatively, you can use the `make` command to install the dependencies.
 Optional Dependencies
 Optional Dependencies
 ~~~~~~~~~~~~~~~~~~~~~~
 ~~~~~~~~~~~~~~~~~~~~~~
 
 
-We have one optional dependency, redis (the python package is installed automatically).
-If you want to use it, be sure that a redis-server is running. If you decide
-to use redis, the `online guests` and `online users` are being tracked by redis,
-else it will only track the `online users` via a simple SQL query.
-
-**On Archlinux**
+We have one optional dependency, redis (the python package is installed
+automatically).
+If you want to use it, make sure that a redis-server is running.
+Redis will be used as the default result and caching backend for
+celery (celery is a task queue which FlaskBB uses to send non blocking emails).
+The feature for tracking the `online guests` and `online users` do also
+require redis (although `online users` works without redis as well).
+To install redis, just use your distributions package manager. For Arch Linux
+this is `pacman` and for Debian/Ubuntu based systems this is `apt-get`.
 ::
 ::
 
 
-    # Install redis
+    # Installing redis using 'pacman':
     $ sudo pacman -S redis
     $ sudo pacman -S redis
+    # Installing redis using 'apt-get':
+    $ sudo apt-get install redis-server
 
 
     # Check if redis is already running.
     # Check if redis is already running.
     $ systemctl status redis
     $ systemctl status redis
@@ -74,30 +90,32 @@ else it will only track the `online users` via a simple SQL query.
     # If not, start it.
     # If not, start it.
     $ sudo systemctl start redis
     $ sudo systemctl start redis
 
 
-    # Optional: Start redis everytime you boot your machine
+    # Optional: Lets start redis everytime you boot your machine
     $ sudo systemctl enable redis
     $ sudo systemctl enable redis
 
 
-**On Debian 7.0 (Wheezy)**
-::
-
-    # Install redis
-    $ sudo apt-get install redis-server
 
 
-    # Check if redis is already running.
-    $ service redis-server status
+Configuration
+-------------
 
 
-    # If not, start it
-    $ sudo service redis-server start
+FlaskBB will no longer assume which config to use. By default, it will load
+a config with some sane defaults (i.e. debug off) but thats it.
+You can either pass the import string to a config object or
+the path to the config file, which is in turn a valid python file.
 
 
-    # Optional: Start redis everytime you boot your machine
-    # I can't remember if this is done automatically..
-    $ sudo update-rc.d redis-server defaults
+A valid import string, for example, is::
 
 
+    flaskbb.configs.development.DevelopmentConfig
 
 
-Configuration
--------------
+and if you wish to use a configuration file, you are free to place it anywhere
+your app user has read access. Please note, that if you decide to use a
+relativ path, it will start looking for the file in the 'root' directory
+of FlaskBB (this is, where the README.md, LICENSE, etc. files are in).
+Absolut paths are also supported. Use whatever you like.
+::
 
 
-Before you can start, you need to configure `FlaskBB`.
+    flaskbb --config dev_config.cfg run
+    [+] Using config from: /path/to/flaskbb/dev_config.cfg
+     * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
 
 
 
 
 Development
 Development
@@ -109,21 +127,75 @@ For development, you need to copy ``flaskbb/configs/development.py.example`` to
 
 
     cp flaskbb/configs/development.py.example flaskbb/configs/development.py
     cp flaskbb/configs/development.py.example flaskbb/configs/development.py
 
 
-The reCAPTCHA keys should work fine on localhost.
+And you should be ready to go!
+
+You can either run::
+
+    make run
+
+or::
+
+    flaskbb --config flaskbb.configs.development.DevelopmentConfig run
+
+to start the development server using the development config.
 
 
 
 
 Production
 Production
 ~~~~~~~~~~
 ~~~~~~~~~~
 
 
-If you plan, to use `FlaskBB` in a production environment (not recommended at
-the moment, because it's still in development), you need to copy
-``flaskbb/configs/production.py.example`` to ``flaskbb/configs/production.py``.
-::
+FlaskBB already sets some sane defaults, so you shouldn't have to change much.
+There are only a few things you have to do. Here we will use the provided
+`production.py.example` configuration file as a template.
+
+Let's copy the example config (production.py file is in .gitignore)::
 
 
     cp flaskbb/configs/production.py.example flaskbb/configs/production.py
     cp flaskbb/configs/production.py.example flaskbb/configs/production.py
 
 
-Now open ``flaskbb/configs/production.py`` with your favourite editor and adjust
-the config variables to your needs.
+and now you are ready to start adjusting the config.
+Open `production.py` with your favorite editor and search for the following
+configuration variables and change them accordingly to your needs:
+
+- ``SERVER_NAME = "example.org"``
+- ``PREFERRED_URL_SCHEME = "https"``
+- ``SQLALCHEMY_DATABASE_URI = 'sqlite:///path/to/flaskbb.sqlite'``
+- ``SECRET_KEY = "secret key"``
+- ``WTF_CSRF_SECRET_KEY = "reallyhardtoguess"``
+
+
+Redis
+~~~~~
+
+If you have decided to use redis as well, which we highly recommend, then
+the following services and features can be enabled and configured to use redis.
+
+Before you can start using redis, you have to enable and configure it.
+This is quite easy just set ``REDIS_ENABLE`` to ``True`` and adjust the
+``REDIS_URL`` if needed.::
+
+    REDIS_ENABLED = True
+    REDIS_URL = "redis://localhost:6379"  # or with a password: "redis://:password@localhost:6379"
+    REDIS_DATABASE = 0
+
+The other services are already configured to use the REDIS_URL configuration
+variable.
+
+**Celery**
+::
+
+    CELERY_BROKER_URL = REDIS_URL
+    CELERY_RESULT_BACKEND = REDIS_URL
+
+**Caching**
+::
+
+    CACHE_TYPE = "redis"
+    CACHE_REDIS_URL = REDIS_URL
+
+**Rate Limiting**
+::
+
+    RATELIMIT_ENABLED = True
+    RATELIMIT_STORAGE_URL = REDIS_URL
 
 
 
 
 Mail Examples
 Mail Examples
@@ -155,41 +227,35 @@ Both methods are included in the example configs.
 Installation
 Installation
 ------------
 ------------
 
 
-For a guided install, run
-::
+For a guided install, run::
 
 
     $ make install
     $ make install
 
 
-or:
+or::
 
 
-    python manage.py install
+    flaskbb install
 
 
 During the installation process you are asked about your username,
 During the installation process you are asked about your username,
-your email address and the password for your administrator user. Using the 
-`make install` command is recommended as it checks that the dependencies are also
-installed.
+your email address and the password for your administrator user. Using the
+`make install` command is recommended as it checks that the dependencies
+are also installed.
 
 
 
 
 Upgrading
 Upgrading
 ---------
 ---------
 
 
 If the database models changed after a release, you have to run the ``upgrade``
 If the database models changed after a release, you have to run the ``upgrade``
-command
-::
+command::
 
 
-    python manage.py db upgrade
+    flaskbb db upgrade
 
 
 
 
 Deploying
 Deploying
 ---------
 ---------
 
 
-I prefer to use supervisor, uWSGI and nginx to deploy my apps, but if you have
-figured out how to deploy it in another way, please let me know, so I
-(or you if you create a pull request) can add it to the documentation.
-
-**NOTE:** I have only used Debian to deploy it, if someone is using a other
-distribution, could you let me know if that works too? `Also, if you have better
-configurations for uWSGI, supervisor or nginx let me know that too.`
+This chapter will describe how to set up Supervisor + uWSGI + nginx for
+FlaskBB as well as document how to use the built-in WSGI server (gunicorn)
+that can be used in a productive environment.
 
 
 
 
 Supervisor
 Supervisor
@@ -230,9 +296,7 @@ uWSGI
 
 
 To get started with uWSGI, you need to install it first.
 To get started with uWSGI, you need to install it first.
 You'll also need the python plugin to serve python apps.
 You'll also need the python plugin to serve python apps.
-This can be done with:
-
-::
+This can be done with::
 
 
     $ sudo apt-get install uwsgi uwsgi-plugin-python
     $ sudo apt-get install uwsgi uwsgi-plugin-python
 
 
@@ -278,6 +342,28 @@ Don't forget to create a symlink to ``/etc/uwsgi/apps-enabled``.
     ln -s /etc/uwsgi/apps-available/flaskbb /etc/uwsgi/apps-enabled/flaskbb
     ln -s /etc/uwsgi/apps-available/flaskbb /etc/uwsgi/apps-enabled/flaskbb
 
 
 
 
+gunicorn
+~~~~~~~~
+
+`Gunicorn 'Green Unicorn' is a Python WSGI HTTP Server for UNIX.`
+
+It's a pre-fork worker model ported from Ruby's Unicorn project.
+The Gunicorn server is broadly compatible with various web frameworks,
+simply implemented, light on server resources, and fairly speedy.
+
+This is probably the easiest way to run a FlaskBB instance.
+Just install gunicorn via pip inside your virtualenv::
+
+    pip install gunicorn
+
+FlaskBB has an built-in command to gunicorn::
+
+    flaskbb start
+
+To see a full list of options either type ``flaskbb start --help`` or
+visit the :ref:`cli <commandline>` docs.
+
+
 nginx
 nginx
 ~~~~~
 ~~~~~
 
 
@@ -289,7 +375,7 @@ The nginx config is pretty straightforward. Again, this is how I use it for
 ``/etc/nginx/sites-available/flaskbb``.
 ``/etc/nginx/sites-available/flaskbb``.
 The only thing left is, that you need to adjust the ``server_name`` to your
 The only thing left is, that you need to adjust the ``server_name`` to your
 domain and the paths in ``access_log``, ``error_log``. Also, don't forget to
 domain and the paths in ``access_log``, ``error_log``. Also, don't forget to
-adjust the paths in the ``alias`` es, as well as the socket adress in ``uwsgi_pass``.
+adjust the paths in the ``alias`` es, as well as the socket address in ``uwsgi_pass``.
 
 
 ::
 ::
 
 
@@ -324,6 +410,90 @@ adjust the paths in the ``alias`` es, as well as the socket adress in ``uwsgi_pa
         }
         }
     }
     }
 
 
+If you wish to use gunicorn instead of uwsgi just replace the ``location @flaskbb``
+with this::
+
+    location @flaskbb {
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+        proxy_set_header X-Forwarded-Proto $scheme;
+        proxy_set_header Host $http_host;
+        #proxy_set_header SCRIPT_NAME /forums;  # This line will make flaskbb available on /forums;
+        proxy_redirect off;
+        proxy_buffering off;
+
+        proxy_pass http://127.0.0.1:8000;
+    }
+
+Don't forget to adjust the ``proxy_pass`` address to your socket address.
+
 
 
 Like in the `uWSGI <#uwsgi>`_ chapter, don't forget to create a symlink to
 Like in the `uWSGI <#uwsgi>`_ chapter, don't forget to create a symlink to
 ``/etc/nginx/sites-enabled/``.
 ``/etc/nginx/sites-enabled/``.
+
+
+User Contributed Deployment Guides
+----------------------------------
+
+We do not maintain these deployment guides. They have been submitted by users
+and we thought it is nice to include them in docs. If something is missing,
+or doesn't work - please open a new pull request on GitHub.
+
+
+Deploying to PythonAnywhere
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+`PythonAnywhere <https://www.pythonanywhere.com/>`_ is a
+platform-as-a-service, which basically means they have a bunch of servers
+pre-configured with Python, nginx and uWSGI.
+You can run a low-traffic website with them for free,
+so it's an easy way to get quickly FlaskBB running publicly.
+
+Here's what to do:
+
+* Sign up for a PythonAnywhere account at
+  `https://www.pythonanywhere.com/ <https://www.pythonanywhere.com/>`_.
+* On the "Consoles" tab, start a Bash console and install/configure
+  FlaskBB like this
+
+::
+
+    git clone https://github.com/sh4nks/flaskbb.git
+    cd flaskbb
+    pip3.5 install --user -r requirements.txt
+    pip3.5 install --user -e .
+
+* Click the PythonAnywhere logo to go back to the dashboard,
+  then go to the "Web" tab, and click the "Add a new web app" button.
+* Just click "Next" on the first page.
+* On the next page, click "Flask"
+* On the next page, click "Python 3.5"
+* On the next page, just accept the default and click next
+* Wait while the website is created.
+* Click on the "Source code" link, and in the input that appears,
+  replace the `mysite` at the end with `flaskbb`
+* Click on the "WSGI configuration file" filename,
+  and wait for an editor to load.
+* Change the line that sets `project_home` to replace `mysite` with `flaskbb`
+  again.
+* Change the line that says
+
+::
+
+    from flask_app import app as application
+
+to say
+
+::
+
+    from flaskbb import create_app
+    application = create_app("/path/to/your/configuration/file")
+
+* Click the green "Save" button near the top right.
+* Go back to the "Web" tab.
+* Click the green "Reload..." button.
+* Click the link to visit the site -- you'll have a new FlaskBB install!
+
+
+.. _virtualenv: https://virtualenv.pypa.io/en/latest/installation.html
+.. _virtualenvwrapper: http://virtualenvwrapper.readthedocs.org/en/latest/install.html#basic-installation
+.. _pip: http://www.pip-installer.org/en/latest/installing.html

+ 3 - 3
flaskbb/_compat.py

@@ -27,15 +27,15 @@ else:           # pragma: no cover
     iteritems = lambda d: d.iteritems()
     iteritems = lambda d: d.iteritems()
 
 
 
 
-def to_bytes(text):
+def to_bytes(text, encoding='utf-8'):
     """Transform string to bytes."""
     """Transform string to bytes."""
     if isinstance(text, text_type):
     if isinstance(text, text_type):
-        text = text.encode('utf-8')
+        text = text.encode(encoding)
     return text
     return text
 
 
 
 
 def to_unicode(input_bytes, encoding='utf-8'):
 def to_unicode(input_bytes, encoding='utf-8'):
     """Decodes input_bytes to text if needed."""
     """Decodes input_bytes to text if needed."""
-    if not isinstance(input_bytes, string_types):
+    if not isinstance(input_bytes, text_type):
         input_bytes = input_bytes.decode(encoding)
         input_bytes = input_bytes.decode(encoding)
     return input_bytes
     return input_bytes

+ 23 - 10
flaskbb/app.py

@@ -18,6 +18,7 @@ from sqlalchemy.engine import Engine
 from flask import Flask, request
 from flask import Flask, request
 from flask_login import current_user
 from flask_login import current_user
 
 
+from flaskbb._compat import string_types
 # views
 # views
 from flaskbb.user.views import user
 from flaskbb.user.views import user
 from flaskbb.message.views import message
 from flaskbb.message.views import message
@@ -52,19 +53,15 @@ from flaskbb.utils.settings import flaskbb_config
 def create_app(config=None):
 def create_app(config=None):
     """Creates the app.
     """Creates the app.
 
 
-    :param config: The configuration object.
+    :param config: The configuration file or object.
+                   The environment variable is weightet as the heaviest.
+                   For example, if the config is specified via an file
+                   and a ENVVAR, it will load the config via the file and
+                   later overwrite it from the ENVVAR.
     """
     """
-
-    # Initialize the app
     app = Flask("flaskbb")
     app = Flask("flaskbb")
 
 
-    # Use the default config and override it afterwards
-    app.config.from_object('flaskbb.configs.default.DefaultConfig')
-    # Update the config
-    app.config.from_object(config)
-    # try to update the config via the environment variable
-    app.config.from_envvar("FLASKBB_SETTINGS", silent=True)
-
+    configure_app(app, config)
     configure_celery_app(app, celery)
     configure_celery_app(app, celery)
     configure_blueprints(app)
     configure_blueprints(app)
     configure_extensions(app)
     configure_extensions(app)
@@ -77,6 +74,22 @@ def create_app(config=None):
     return app
     return app
 
 
 
 
+def configure_app(app, config):
+    """Configures FlaskBB."""
+    # Use the default config and override it afterwards
+    app.config.from_object('flaskbb.configs.default.DefaultConfig')
+
+    if isinstance(config, string_types) and \
+            os.path.exists(os.path.abspath(config)):
+        app.config.from_pyfile(os.path.abspath(config))
+    else:
+        # try to update the config from the object
+        app.config.from_object(config)
+
+    # try to update the config via the environment variable
+    app.config.from_envvar("FLASKBB_SETTINGS", silent=True)
+
+
 def configure_celery_app(app, celery):
 def configure_celery_app(app, celery):
     """Configures the celery app."""
     """Configures the celery app."""
     app.config.update({'BROKER_URL': app.config["CELERY_BROKER_URL"]})
     app.config.update({'BROKER_URL': app.config["CELERY_BROKER_URL"]})

+ 22 - 0
flaskbb/cli/__init__.py

@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+"""
+    flaskbb.cli
+    ~~~~~~~~~~~
+
+    FlaskBB's Command Line Interface.
+    To make it work, you have to install FlaskBB via ``pip install -e .``.
+
+    Plugin and Theme templates are generated via cookiecutter.
+    In order to generate those project templates you have to
+    cookiecutter first::
+
+        pip install cookiecutter
+
+    :copyright: (c) 2016 by the FlaskBB Team.
+    :license: BSD, see LICENSE for more details.
+"""
+from flaskbb.cli.main import flaskbb
+from flaskbb.cli.plugins import plugins
+from flaskbb.cli.themes import themes
+from flaskbb.cli.translations import translations
+from flaskbb.cli.users import users

+ 405 - 0
flaskbb/cli/main.py

@@ -0,0 +1,405 @@
+# -*- coding: utf-8 -*-
+"""
+    flaskbb.cli.commands
+    ~~~~~~~~~~~~~~~~~~~~
+
+    This module contains the main commands.
+
+    :copyright: (c) 2016 by the FlaskBB Team.
+    :license: BSD, see LICENSE for more details.
+"""
+import sys
+import os
+import time
+import requests
+
+import click
+from werkzeug.utils import import_string, ImportStringError
+from flask import current_app
+from flask.cli import FlaskGroup, ScriptInfo, with_appcontext
+from sqlalchemy_utils.functions import database_exists, create_database, drop_database
+from flask_migrate import upgrade as upgrade_database
+
+from flaskbb import create_app
+from flaskbb._compat import iteritems
+from flaskbb.extensions import db, whooshee, celery
+from flaskbb.cli.utils import (get_version, save_user_prompt, FlaskBBCLIError,
+                               EmailType)
+from flaskbb.utils.populate import (create_test_data, create_welcome_forum,
+                                    create_default_groups,
+                                    create_default_settings, insert_bulk_data,
+                                    update_settings_from_fixture)
+from flaskbb.utils.translations import compile_translations
+
+
+def make_app(script_info):
+    config_file = getattr(script_info, "config_file")
+    if config_file is not None:
+        # check if config file exists
+        if os.path.exists(os.path.abspath(config_file)):
+            click.secho("[+] Using config from: {}".format(
+                        os.path.abspath(config_file)), fg="cyan")
+        # config file doesn't exist, maybe it's a module
+        else:
+            try:
+                import_string(config_file)
+                click.secho("[+] Using config from: {}".format(config_file),
+                            fg="cyan")
+            except ImportStringError:
+                click.secho("[~] Config '{}' doesn't exist. "
+                            "Using default config.".format(config_file),
+                            fg="red")
+                config_file = None
+    else:
+        click.secho("[~] Using default config.", fg="yellow")
+
+    return create_app(config_file)
+
+
+def set_config(ctx, param, value):
+    """This will pass the config file to the create_app function."""
+    ctx.ensure_object(ScriptInfo).config_file = value
+
+
+@click.group(cls=FlaskGroup, create_app=make_app, add_version_option=False)
+@click.option("--config", expose_value=False, callback=set_config,
+              required=False, is_flag=False, is_eager=True, metavar="CONFIG",
+              help="Specify the config to use in dotted module notation "
+                   "e.g. flaskbb.configs.default.DefaultConfig")
+@click.option("--version", expose_value=False, callback=get_version,
+              is_flag=True, is_eager=True, help="Show the FlaskBB version.")
+def flaskbb():
+    """This is the commandline interface for flaskbb."""
+    pass
+
+
+@flaskbb.command()
+@click.option("--welcome", "-w", default=True, is_flag=True,
+              help="Disable the welcome forum.")
+@click.option("--force", "-f", default=False, is_flag=True,
+              help="Doesn't ask for confirmation.")
+@click.option("--username", "-u", help="The username of the user.")
+@click.option("--email", "-e", type=EmailType(),
+              help="The email address of the user.")
+@click.option("--password", "-p", help="The password of the user.")
+@click.option("--group", "-g", help="The group of the user.",
+              type=click.Choice(["admin", "super_mod", "mod", "member"]))
+def install(welcome, force, username, email, password, group):
+    """Installs flaskbb. If no arguments are used, an interactive setup
+    will be run.
+    """
+    click.secho("[+] Installing FlaskBB...", fg="cyan")
+    if database_exists(db.engine.url):
+        if force or click.confirm(click.style(
+            "Existing database found. Do you want to delete the old one and "
+            "create a new one?", fg="magenta")
+        ):
+            drop_database(db.engine.url)
+        else:
+            sys.exit(0)
+    create_database(db.engine.url)
+    upgrade_database()
+
+    click.secho("[+] Creating default settings...", fg="cyan")
+    create_default_groups()
+    create_default_settings()
+
+    click.secho("[+] Creating admin user...", fg="cyan")
+    save_user_prompt(username, email, password, group)
+
+    if welcome:
+        click.secho("[+] Creating welcome forum...", fg="cyan")
+        create_welcome_forum()
+
+    click.secho("[+] Compiling translations...", fg="cyan")
+    compile_translations()
+
+    click.secho("[+] FlaskBB has been successfully installed!",
+                fg="green", bold=True)
+
+
+@flaskbb.command()
+@click.option("--test-data", "-t", default=False, is_flag=True,
+              help="Adds some test data.")
+@click.option("--bulk-data", "-b", default=False, is_flag=True,
+              help="Adds a lot of data.")
+@click.option("--posts", default=100,
+              help="Number of posts to create in each topic (default: 100).")
+@click.option("--topics", default=100,
+              help="Number of topics to create (default: 100).")
+@click.option("--force", "-f", is_flag=True,
+              help="Will delete the database before populating it.")
+@click.option("--initdb", "-i", is_flag=True,
+              help="Initializes the database before populating it.")
+def populate(bulk_data, test_data, posts, topics, force, initdb):
+    """Creates the necessary tables and groups for FlaskBB."""
+    if force:
+        click.secho("[+] Recreating database...", fg="cyan")
+        drop_database(db.engine.url)
+
+        # do not initialize the db if -i is passed
+        if not initdb:
+            upgrade_database()
+
+    if initdb:
+        click.secho("[+] Initializing database...", fg="cyan")
+        upgrade_database()
+
+    if test_data:
+        click.secho("[+] Adding some test data...", fg="cyan")
+        create_test_data()
+
+    if bulk_data:
+        timer = time.time()
+        topic_count, post_count = insert_bulk_data(int(topics), int(posts))
+        elapsed = time.time() - timer
+        click.secho("[+] It took {} seconds to create {} topics and {} posts"
+                    .format(elapsed, topic_count, post_count), fg="cyan")
+
+    # this just makes the most sense for the command name; use -i to
+    # init the db as well
+    if not test_data:
+        click.secho("[+] Populating the database with some defaults...",
+                    fg="cyan")
+        create_default_groups()
+        create_default_settings()
+
+
+@flaskbb.command()
+def reindex():
+    """Reindexes the search index."""
+    click.secho("[+] Reindexing search index...", fg="cyan")
+    whooshee.reindex()
+
+
+@flaskbb.command()
+@click.option("all_latest", "--all", "-a", default=False, is_flag=True,
+              help="Upgrades migrations AND fixtures to the latest version.")
+@click.option("--fixture/", "-f", default=None,
+              help="The fixture which should be upgraded or installed.")
+@click.option("--force", default=False, is_flag=True,
+              help="Forcefully upgrades the fixtures.")
+def upgrade(all_latest, fixture, force):
+    """Updates the migrations and fixtures."""
+    if all_latest:
+        click.secho("[+] Upgrading migrations to the latest version...",
+                    fg="cyan")
+        upgrade_database()
+
+    if fixture or all_latest:
+        try:
+            settings = import_string(
+                "flaskbb.fixtures.{}".format(fixture)
+            )
+            settings = settings.fixture
+        except ImportError:
+            raise FlaskBBCLIError("{} fixture is not available"
+                                  .format(fixture), fg="red")
+
+        click.secho("[+] Updating fixtures...")
+        count = update_settings_from_fixture(
+            fixture=settings, overwrite_group=force, overwrite_setting=force
+        )
+        click.secho("[+] {} groups and {} settings updated.".format(
+            len(count.keys()), len(count.values()), fg="green")
+        )
+
+
+@flaskbb.command("download-emojis")
+@with_appcontext
+def download_emoji():
+    """Downloads emojis from emoji-cheat-sheet.com.
+    This command is probably going to be removed in future version.
+    """
+    click.secho("[+] Downloading emojis...", fg="cyan")
+    HOSTNAME = "https://api.github.com"
+    REPO = "/repos/arvida/emoji-cheat-sheet.com/contents/public/graphics/emojis"
+    FULL_URL = "{}{}".format(HOSTNAME, REPO)
+    DOWNLOAD_PATH = os.path.join(current_app.static_folder, "emoji")
+    response = requests.get(FULL_URL)
+
+    cached_count = 0
+    count = 0
+    for image in response.json():
+        if not os.path.exists(os.path.abspath(DOWNLOAD_PATH)):
+            raise FlaskBBCLIError(
+                "{} does not exist.".format(os.path.abspath(DOWNLOAD_PATH)),
+                fg="red")
+
+        full_path = os.path.join(DOWNLOAD_PATH, image["name"])
+        if not os.path.exists(full_path):
+            count += 1
+            f = open(full_path, 'wb')
+            f.write(requests.get(image["download_url"]).content)
+            f.close()
+            if count == cached_count + 50:
+                cached_count = count
+                click.secho("[+] {} out of {} Emojis downloaded...".format(
+                            cached_count, len(response.json())), fg="cyan")
+
+    click.secho("[+] Finished downloading {} Emojis.".format(count),
+                fg="green")
+
+
+@flaskbb.command("celery", context_settings=dict(ignore_unknown_options=True,))
+@click.argument('celery_args', nargs=-1, type=click.UNPROCESSED)
+@click.option("show_help", "--help", "-h", is_flag=True,
+              help="Shows this message and exits")
+@click.option("show_celery_help", "--help-celery", is_flag=True,
+              help="Shows the celery help message")
+@click.pass_context
+@with_appcontext
+def start_celery(ctx, show_help, show_celery_help, celery_args):
+    """Preconfigured wrapper around the 'celery' command.
+    Additional CELERY_ARGS arguments are passed to celery."""
+    if show_help:
+        click.echo(ctx.get_help())
+        sys.exit(0)
+
+    if show_celery_help:
+        click.echo(celery.start(argv=["--help"]))
+        sys.exit(0)
+
+    default_args = ['celery']
+    default_args = default_args + list(celery_args)
+    celery.start(argv=default_args)
+
+
+@flaskbb.command()
+@click.option("--server", "-s", default="gunicorn",
+              type=click.Choice(["gunicorn", "gevent"]),
+              help="The WSGI Server to run FlaskBB on.")
+@click.option("--host", "-h", default="127.0.0.1",
+              help="The interface to bind FlaskBB to.")
+@click.option("--port", "-p", default="8000", type=int,
+              help="The port to bind FlaskBB to.")
+@click.option("--workers", "-w", default=4,
+              help="The number of worker processes for handling requests.")
+@click.option("--daemon", "-d", default=False, is_flag=True,
+              help="Starts gunicorn as daemon.")
+@click.option("--config", "-c",
+              help="The configuration file to use for FlaskBB.")
+def start(server, host, port, workers, config, daemon):
+    """Starts a production ready wsgi server.
+    TODO: Figure out a way how to forward additional args to gunicorn
+          without causing any errors.
+    """
+    if server == "gunicorn":
+        try:
+            from gunicorn.app.base import Application
+
+            class FlaskBBApplication(Application):
+                def __init__(self, app, options=None):
+                    self.options = options or {}
+                    self.application = app
+                    super(FlaskBBApplication, self).__init__()
+
+                def load_config(self):
+                    config = dict([
+                        (key, value) for key, value in iteritems(self.options)
+                        if key in self.cfg.settings and value is not None
+                    ])
+                    for key, value in iteritems(config):
+                        self.cfg.set(key.lower(), value)
+
+                def load(self):
+                    return self.application
+
+            options = {
+                "bind": "{}:{}".format(host, port),
+                "workers": workers,
+                "daemon": daemon,
+            }
+            FlaskBBApplication(create_app(config=config), options).run()
+        except ImportError:
+            raise FlaskBBCLIError("Cannot import gunicorn. "
+                                  "Make sure it is installed.", fg="red")
+
+    elif server == "gevent":
+        try:
+            from gevent import __version__
+            from gevent.pywsgi import WSGIServer
+            click.secho("* Starting gevent {}".format(__version__))
+            click.secho("* Listening on http://{}:{}/".format(host, port))
+            http_server = WSGIServer((host, port), create_app(config=config))
+            http_server.serve_forever()
+        except ImportError:
+            raise FlaskBBCLIError("Cannot import gevent. "
+                                  "Make sure it is installed.", fg="red")
+
+
+@flaskbb.command("shell", short_help="Runs a shell in the app context.")
+@with_appcontext
+def shell_command():
+    """Runs an interactive Python shell in the context of a given
+    Flask application.  The application will populate the default
+    namespace of this shell according to it"s configuration.
+    This is useful for executing small snippets of management code
+    without having to manually configuring the application.
+
+    This code snippet is taken from Flask"s cli module and modified to
+    run IPython and falls back to the normal shell if IPython is not
+    available.
+    """
+    import code
+    banner = "Python %s on %s\nInstance Path: %s" % (
+        sys.version,
+        sys.platform,
+        current_app.instance_path,
+    )
+    ctx = {"db": db}
+
+    # Support the regular Python interpreter startup script if someone
+    # is using it.
+    startup = os.environ.get("PYTHONSTARTUP")
+    if startup and os.path.isfile(startup):
+        with open(startup, "r") as f:
+            eval(compile(f.read(), startup, "exec"), ctx)
+
+    ctx.update(current_app.make_shell_context())
+
+    try:
+        import IPython
+        IPython.embed(banner1=banner, user_ns=ctx)
+    except ImportError:
+        code.interact(banner=banner, local=ctx)
+
+
+@flaskbb.command("urls", short_help="Show routes for the app.")
+@click.option("--route", "-r", "order_by", flag_value="rule", default=True,
+              help="Order by route")
+@click.option("--endpoint", "-e", "order_by", flag_value="endpoint",
+              help="Order by endpoint")
+@click.option("--methods", "-m", "order_by", flag_value="methods",
+              help="Order by methods")
+@with_appcontext
+def list_urls(order_by):
+    """Lists all available routes."""
+    from flask import current_app
+
+    rules = sorted(
+        current_app.url_map.iter_rules(),
+        key=lambda rule: getattr(rule, order_by)
+    )
+
+    max_rule_len = max(len(rule.rule) for rule in rules)
+    max_rule_len = max(max_rule_len, len("Route"))
+
+    max_endpoint_len = max(len(rule.endpoint) for rule in rules)
+    max_endpoint_len = max(max_endpoint_len, len("Endpoint"))
+
+    max_method_len = max(len(", ".join(rule.methods)) for rule in rules)
+    max_method_len = max(max_method_len, len("Methods"))
+
+    column_header_len = max_rule_len + max_endpoint_len + max_method_len + 4
+    column_template = "{:<%s}  {:<%s}  {:<%s}" % (
+        max_rule_len, max_endpoint_len, max_method_len
+    )
+
+    click.secho(column_template.format("Route", "Endpoint", "Methods"),
+                fg="blue", bold=True)
+    click.secho("=" * column_header_len, bold=True)
+
+    for rule in rules:
+        methods = ", ".join(rule.methods)
+        click.echo(column_template.format(rule.rule, rule.endpoint, methods))

+ 130 - 0
flaskbb/cli/plugins.py

@@ -0,0 +1,130 @@
+# -*- coding: utf-8 -*-
+"""
+    flaskbb.cli.plugins
+    ~~~~~~~~~~~~~~~~~~~
+
+    This module contains all plugin commands.
+
+    :copyright: (c) 2016 by the FlaskBB Team.
+    :license: BSD, see LICENSE for more details.
+"""
+import sys
+import os
+import shutil
+
+import click
+from flask import current_app
+from flask_plugins import (get_all_plugins, get_enabled_plugins,
+                           get_plugin_from_all)
+
+from flaskbb.cli.main import flaskbb
+from flaskbb.cli.utils import check_cookiecutter, validate_plugin
+from flaskbb.extensions import plugin_manager
+
+try:
+    from cookiecutter.main import cookiecutter
+except ImportError:
+    pass
+
+
+@flaskbb.group()
+def plugins():
+    """Plugins command sub group."""
+    pass
+
+
+@plugins.command("new")
+@click.argument("plugin_identifier", callback=check_cookiecutter)
+@click.option("--template", "-t", type=click.STRING,
+              default="https://github.com/sh4nks/cookiecutter-flaskbb-plugin",
+              help="Path to a cookiecutter template or to a valid git repo.")
+def new_plugin(plugin_identifier, template):
+    """Creates a new plugin based on the cookiecutter plugin
+    template. Defaults to this template:
+    https://github.com/sh4nks/cookiecutter-flaskbb-plugin.
+    It will either accept a valid path on the filesystem
+    or a URL to a Git repository which contains the cookiecutter template.
+    """
+    out_dir = os.path.join(current_app.root_path, "plugins", plugin_identifier)
+    click.secho("[+] Creating new plugin {}".format(plugin_identifier),
+                fg="cyan")
+    cookiecutter(template, output_dir=out_dir)
+    click.secho("[+] Done. Created in {}".format(out_dir),
+                fg="green", bold=True)
+
+
+@plugins.command("install")
+@click.argument("plugin_identifier")
+def install_plugin(plugin_identifier):
+    """Installs a new plugin."""
+    validate_plugin(plugin_identifier)
+    plugin = get_plugin_from_all(plugin_identifier)
+    click.secho("[+] Installing plugin {}...".format(plugin.name), fg="cyan")
+    try:
+        plugin_manager.install_plugins([plugin])
+    except Exception as e:
+        click.secho("[-] Couldn't install plugin because of following "
+                    "exception: \n{}".format(e), fg="red")
+
+
+@plugins.command("uninstall")
+@click.argument("plugin_identifier")
+def uninstall_plugin(plugin_identifier):
+    """Uninstalls a plugin from FlaskBB."""
+    validate_plugin(plugin_identifier)
+    plugin = get_plugin_from_all(plugin_identifier)
+    click.secho("[+] Uninstalling plugin {}...".format(plugin.name), fg="cyan")
+    try:
+        plugin_manager.uninstall_plugins([plugin])
+    except AttributeError:
+        pass
+
+
+@plugins.command("remove")
+@click.argument("plugin_identifier")
+@click.option("--force", "-f", default=False, is_flag=True,
+              help="Removes the plugin without asking for confirmation.")
+def remove_plugin(plugin_identifier, force):
+    """Removes a plugin from the filesystem."""
+    validate_plugin(plugin_identifier)
+    if not force and not \
+            click.confirm(click.style("Are you sure?", fg="magenta")):
+        sys.exit(0)
+
+    plugin = get_plugin_from_all(plugin_identifier)
+    click.secho("[+] Uninstalling plugin {}...".format(plugin.name), fg="cyan")
+    try:
+        plugin_manager.uninstall_plugins([plugin])
+    except Exception as e:
+        click.secho("[-] Couldn't uninstall plugin because of following "
+                    "exception: \n{}".format(e), fg="red")
+        if not click.confirm(click.style(
+            "Do you want to continue anyway?", fg="magenta")
+        ):
+            sys.exit(0)
+
+    click.secho("[+] Removing plugin from filesystem...", fg="cyan")
+    shutil.rmtree(plugin.path, ignore_errors=False, onerror=None)
+
+
+@plugins.command("list")
+def list_plugins():
+    """Lists all installed plugins."""
+    click.secho("[+] Listing all installed plugins...", fg="cyan")
+
+    # This is subject to change as I am not happy with the current
+    # plugin system
+    enabled_plugins = get_enabled_plugins()
+    disabled_plugins = set(get_all_plugins()) - set(enabled_plugins)
+    if len(enabled_plugins) > 0:
+        click.secho("[+] Enabled Plugins:", fg="blue", bold=True)
+        for plugin in enabled_plugins:
+            click.secho("    - {} (version {})".format(
+                plugin.name, plugin.version), bold=True
+            )
+    if len(disabled_plugins) > 0:
+        click.secho("[+] Disabled Plugins:", fg="yellow", bold=True)
+        for plugin in disabled_plugins:
+            click.secho("    - {} (version {})".format(
+                plugin.name, plugin.version), bold=True
+            )

+ 84 - 0
flaskbb/cli/themes.py

@@ -0,0 +1,84 @@
+# -*- coding: utf-8 -*-
+"""
+    flaskbb.cli.themes
+    ~~~~~~~~~~~~~~~~~~
+
+    This module contains all theme commands.
+
+    :copyright: (c) 2016 by the FlaskBB Team.
+    :license: BSD, see LICENSE for more details.
+"""
+import sys
+import os
+import shutil
+
+import click
+from flask import current_app
+from flask_themes2 import get_themes_list, get_theme
+
+from flaskbb.cli.main import flaskbb
+from flaskbb.cli.utils import check_cookiecutter, validate_theme
+from flaskbb.utils.settings import flaskbb_config
+
+
+@flaskbb.group()
+def themes():
+    """Themes command sub group."""
+    pass
+
+
+@themes.command("list")
+def list_themes():
+    """Lists all installed themes."""
+    click.secho("[+] Listing all installed themes...", fg="cyan")
+
+    active_theme = get_theme(flaskbb_config['DEFAULT_THEME'])
+    available_themes = set(get_themes_list()) - set([active_theme])
+
+    click.secho("[+] Active Theme:", fg="blue", bold=True)
+    click.secho("    - {} (version {})".format(
+        active_theme.name, active_theme.version), bold=True
+    )
+
+    click.secho("[+] Available Themes:", fg="yellow", bold=True)
+    for theme in available_themes:
+        click.secho("    - {} (version {})".format(
+            theme.name, theme.version), bold=True
+        )
+
+
+@themes.command("new")
+@click.argument("theme_identifier", callback=check_cookiecutter)
+@click.option("--template", "-t", type=click.STRING,
+              default="https://github.com/sh4nks/cookiecutter-flaskbb-theme",
+              help="Path to a cookiecutter template or to a valid git repo.")
+def new_theme(theme_identifier, template):
+    """Creates a new theme based on the cookiecutter theme
+    template. Defaults to this template:
+    https://github.com/sh4nks/cookiecutter-flaskbb-theme.
+    It will either accept a valid path on the filesystem
+    or a URL to a Git repository which contains the cookiecutter template.
+    """
+    from cookiecutter.main import cookiecutter
+    out_dir = os.path.join(current_app.root_path, "themes")
+    click.secho("[+] Creating new theme {}".format(theme_identifier),
+                fg="cyan")
+    cookiecutter(template, output_dir=out_dir)
+    click.secho("[+] Done. Created in {}".format(out_dir),
+                fg="green", bold=True)
+
+
+@themes.command("remove")
+@click.argument("theme_identifier")
+@click.option("--force", "-f", default=False, is_flag=True,
+              help="Removes the theme without asking for confirmation.")
+def remove_theme(theme_identifier, force):
+    """Removes a theme from the filesystem."""
+    validate_theme(theme_identifier)
+    if not force and not \
+            click.confirm(click.style("Are you sure?", fg="magenta")):
+        sys.exit(0)
+
+    theme = get_theme(theme_identifier)
+    click.secho("[+] Removing theme from filesystem...", fg="cyan")
+    shutil.rmtree(theme.path, ignore_errors=False, onerror=None)

+ 76 - 0
flaskbb/cli/translations.py

@@ -0,0 +1,76 @@
+# -*- coding: utf-8 -*-
+"""
+    flaskbb.cli.translations
+    ~~~~~~~~~~~~~~~~~~~~~~~~
+
+    This module contains all translation commands.
+
+    :copyright: (c) 2016 by the FlaskBB Team.
+    :license: BSD, see LICENSE for more details.
+"""
+import click
+
+from flaskbb.cli.main import flaskbb
+from flaskbb.cli.utils import validate_plugin
+from flaskbb.utils.translations import (add_translations, compile_translations,
+                                        update_translations,
+                                        add_plugin_translations,
+                                        compile_plugin_translations,
+                                        update_plugin_translations)
+
+
+@flaskbb.group()
+def translations():
+    """Translations command sub group."""
+    pass
+
+
+@translations.command("new")
+@click.option("--plugin", "-p", type=click.STRING,
+              help="Adds a new language to a plugin.")
+@click.argument("lang")
+def new_translation(lang, plugin):
+    """Adds a new language to the translations. "lang" is the language code
+    of the language, like, "de_AT"."""
+    if plugin:
+        validate_plugin(plugin)
+        click.secho("[+] Adding new language {} for plugin {}..."
+                    .format(lang, plugin), fg="cyan")
+        add_plugin_translations(plugin, lang)
+    else:
+        click.secho("[+] Adding new language {}...".format(lang), fg="cyan")
+        add_translations(lang)
+
+
+@translations.command("update")
+@click.option("is_all", "--all", "-a", default=True, is_flag=True,
+              help="Updates the plugin translations as well.")
+@click.option("--plugin", "-p", type=click.STRING,
+              help="Updates the language of the given plugin.")
+def update_translation(is_all, plugin):
+    """Updates all translations."""
+    if plugin is not None:
+        validate_plugin(plugin)
+        click.secho("[+] Updating language files for plugin {}..."
+                    .format(plugin), fg="cyan")
+        update_plugin_translations(plugin)
+    else:
+        click.secho("[+] Updating language files...", fg="cyan")
+        update_translations(include_plugins=is_all)
+
+
+@translations.command("compile")
+@click.option("is_all", "--all", "-a", default=True, is_flag=True,
+              help="Compiles the plugin translations as well.")
+@click.option("--plugin", "-p", type=click.STRING,
+              help="Compiles the translations for a given plugin.")
+def compile_translation(is_all, plugin):
+    """Compiles the translations."""
+    if plugin is not None:
+        validate_plugin(plugin)
+        click.secho("[+] Compiling language files for plugin {}..."
+                    .format(plugin), fg="cyan")
+        compile_plugin_translations(plugin)
+    else:
+        click.secho("[+] Compiling language files...", fg="cyan")
+        compile_translations(include_plugins=is_all)

+ 89 - 0
flaskbb/cli/users.py

@@ -0,0 +1,89 @@
+# -*- coding: utf-8 -*-
+"""
+    flaskbb.cli.users
+    ~~~~~~~~~~~~~~~~~
+
+    This module contains all user commands.
+
+    :copyright: (c) 2016 by the FlaskBB Team.
+    :license: BSD, see LICENSE for more details.
+"""
+import sys
+import os
+
+import click
+from sqlalchemy.exc import IntegrityError
+
+from flaskbb.cli.main import flaskbb
+from flaskbb.cli.utils import FlaskBBCLIError, EmailType, save_user_prompt
+from flaskbb.user.models import User
+
+
+@flaskbb.group()
+def users():
+    """Create, update or delete users."""
+    pass
+
+
+@users.command("new")
+@click.option("--username", "-u", help="The username of the user.")
+@click.option("--email", "-e", type=EmailType(),
+              help="The email address of the user.")
+@click.option("--password", "-p", help="The password of the user.")
+@click.option("--group", "-g", help="The group of the user.",
+              type=click.Choice(["admin", "super_mod", "mod", "member"]))
+def new_user(username, email, password, group):
+    """Creates a new user. Omit any options to use the interactive mode."""
+    try:
+        user = save_user_prompt(username, email, password, group)
+
+        click.secho("[+] User {} with Email {} in Group {} created.".format(
+            user.username, user.email, user.primary_group.name), fg="cyan"
+        )
+    except IntegrityError:
+        raise FlaskBBCLIError("Couldn't create the user because the "
+                              "username or email address is already taken.",
+                              fg="red")
+
+
+@users.command("update")
+@click.option("--username", "-u", help="The username of the user.")
+@click.option("--email", "-e", type=EmailType(),
+              help="The email address of the user.")
+@click.option("--password", "-p", help="The password of the user.")
+@click.option("--group", "-g", help="The group of the user.",
+              type=click.Choice(["admin", "super_mod", "mod", "member"]))
+def change_user(username, password, email, group):
+    """Updates an user. Omit any options to use the interactive mode."""
+
+    user = save_user_prompt(username, password, email, group)
+    if user is None:
+        raise FlaskBBCLIError("The user with username {} does not exist."
+                              .format(username), fg="red")
+
+    click.secho("[+] User {} updated.".format(user.username), fg="cyan")
+
+
+@users.command("delete")
+@click.option("--username", "-u", help="The username of the user.")
+@click.option("--force", "-f", default=False, is_flag=True,
+              help="Removes the user without asking for confirmation.")
+def delete_user(username, force):
+    """Deletes an user."""
+    if not username:
+        username = click.prompt(
+            click.style("Username", fg="magenta"), type=str,
+            default=os.environ.get("USER", "")
+        )
+
+    user = User.query.filter_by(username=username).first()
+    if user is None:
+        raise FlaskBBCLIError("The user with username {} does not exist."
+                              .format(username), fg="red")
+
+    if not force and not \
+            click.confirm(click.style("Are you sure?", fg="magenta")):
+        sys.exit(0)
+
+    user.delete()
+    click.secho("[+] User {} deleted.".format(user.username), fg="cyan")

+ 139 - 0
flaskbb/cli/utils.py

@@ -0,0 +1,139 @@
+# -*- coding: utf-8 -*-
+"""
+    flaskbb.cli.utils
+    ~~~~~~~~~~~~~~~~~
+
+    This module contains some utility helpers that are used across
+    commands.
+
+    :copyright: (c) 2016 by the FlaskBB Team.
+    :license: BSD, see LICENSE for more details.
+"""
+import sys
+import os
+import re
+
+
+import click
+
+from flask import __version__ as flask_version
+from flask_themes2 import get_theme
+
+from flaskbb import __version__
+from flaskbb.extensions import plugin_manager
+from flaskbb.utils.populate import create_user, update_user
+
+
+cookiecutter_available = False
+try:
+    from cookiecutter.main import cookiecutter
+    cookiecutter_available = True
+except ImportError:
+    pass
+
+_email_regex = r"[^@]+@[^@]+\.[^@]+"
+
+
+class FlaskBBCLIError(click.ClickException):
+    """An exception that signals a usage error including color support.
+    This aborts any further handling.
+
+    :param styles: The style kwargs which should be forwarded to click.secho.
+    """
+
+    def __init__(self, message, **styles):
+        click.ClickException.__init__(self, message)
+        self.styles = styles
+
+    def show(self, file=None):
+        if file is None:
+            file = click._compat.get_text_stderr()
+        click.secho("[-] Error: %s" % self.format_message(), file=file,
+                    **self.styles)
+
+
+class EmailType(click.ParamType):
+    """The choice type allows a value to be checked against a fixed set of
+    supported values.  All of these values have to be strings.
+    See :ref:`choice-opts` for an example.
+    """
+    name = "email"
+
+    def convert(self, value, param, ctx):
+        # Exact match
+        if re.match(_email_regex, value):
+            return value
+        else:
+            self.fail(("invalid email: %s" % value), param, ctx)
+
+    def __repr__(self):
+        return "email"
+
+
+def save_user_prompt(username, email, password, group, only_update=False):
+    if not username:
+        username = click.prompt(
+            click.style("Username", fg="magenta"), type=str,
+            default=os.environ.get("USER", "")
+        )
+    if not email:
+        email = click.prompt(
+            click.style("Email address", fg="magenta"), type=EmailType()
+        )
+    if not password:
+        password = click.prompt(
+            click.style("Password", fg="magenta"), hide_input=True,
+            confirmation_prompt=True
+        )
+    if not group:
+        group = click.prompt(
+            click.style("Group", fg="magenta"),
+            type=click.Choice(["admin", "super_mod", "mod", "member"]),
+            default="admin"
+        )
+
+    if only_update:
+        return update_user(username, password, email, group)
+    return create_user(username, password, email, group)
+
+
+def validate_plugin(plugin):
+    """Checks if a plugin is installed.
+    TODO: Figure out how to use this in a callback. Doesn't work because
+          the appcontext can't be found and using with_appcontext doesn't
+          help either.
+    """
+    if plugin not in plugin_manager.all_plugins.keys():
+        raise FlaskBBCLIError("Plugin {} not found.".format(plugin), fg="red")
+    return True
+
+
+def validate_theme(theme):
+    """Checks if a theme is installed."""
+    try:
+        get_theme(theme)
+    except KeyError:
+        raise FlaskBBCLIError("Theme {} not found.".format(theme), fg="red")
+
+
+def check_cookiecutter(ctx, param, value):
+    if not cookiecutter_available:
+        raise FlaskBBCLIError(
+            "Can't create {} because cookiecutter is not installed. "
+            "You can install it with 'pip install cookiecutter'.".
+            format(value), fg="red"
+        )
+    return value
+
+
+def get_version(ctx, param, value):
+    if not value or ctx.resilient_parsing:
+        return
+    message = ("FlaskBB %(version)s using Flask %(flask_version)s on "
+               "Python %(python_version)s")
+    click.echo(message % {
+        'version': __version__,
+        'flask_version': flask_version,
+        'python_version': sys.version.split("\n")[0]
+    }, color=ctx.color)
+    ctx.exit()

+ 2 - 0
flaskbb/configs/default.py

@@ -182,3 +182,5 @@ class DefaultConfig(object):
     MESSAGE_URL_PREFIX = "/message"
     MESSAGE_URL_PREFIX = "/message"
     AUTH_URL_PREFIX = "/auth"
     AUTH_URL_PREFIX = "/auth"
     ADMIN_URL_PREFIX = "/admin"
     ADMIN_URL_PREFIX = "/admin"
+    # Plugin Folder
+    PLUGINS_FOLDER = os.path.join(_basedir, "flaskbb", "plugins")

+ 2 - 2
flaskbb/extensions.py

@@ -21,7 +21,7 @@ from flask_migrate import Migrate
 from flask_themes2 import Themes
 from flask_themes2 import Themes
 from flask_plugins import PluginManager
 from flask_plugins import PluginManager
 from flask_babelplus import Babel
 from flask_babelplus import Babel
-from flask_wtf.csrf import CsrfProtect
+from flask_wtf.csrf import CSRFProtect
 from flask_limiter import Limiter
 from flask_limiter import Limiter
 from flask_limiter.util import get_remote_address
 from flask_limiter.util import get_remote_address
 from flaskbb.exceptions import AuthorizationRequired
 from flaskbb.exceptions import AuthorizationRequired
@@ -64,7 +64,7 @@ plugin_manager = PluginManager()
 babel = Babel()
 babel = Babel()
 
 
 # CSRF
 # CSRF
-csrf = CsrfProtect()
+csrf = CSRFProtect()
 
 
 # Rate Limiting
 # Rate Limiting
 limiter = Limiter(auto_check=False, key_func=get_remote_address)
 limiter = Limiter(auto_check=False, key_func=get_remote_address)

+ 5 - 3
flaskbb/forum/models.py

@@ -98,7 +98,10 @@ class Report(db.Model, CRUDMixin):
     zapped_by = db.Column(db.Integer, db.ForeignKey("users.id"))
     zapped_by = db.Column(db.Integer, db.ForeignKey("users.id"))
     reason = db.Column(db.Text)
     reason = db.Column(db.Text)
 
 
-    post = db.relationship("Post", backref="report", lazy="joined")
+    post = db.relationship(
+        "Post", lazy="joined",
+        backref=db.backref('report', cascade='all, delete-orphan')
+    )
     reporter = db.relationship("User", lazy="joined",
     reporter = db.relationship("User", lazy="joined",
                                foreign_keys=[reporter_id])
                                foreign_keys=[reporter_id])
     zapper = db.relationship("User", lazy="joined", foreign_keys=[zapped_by])
     zapper = db.relationship("User", lazy="joined", foreign_keys=[zapped_by])
@@ -339,14 +342,13 @@ class Topic(db.Model, CRUDMixin):
 
 
         # If the topic is unread try to get the first unread post
         # If the topic is unread try to get the first unread post
         if topic_is_unread(self, topicsread, user, forumsread):
         if topic_is_unread(self, topicsread, user, forumsread):
-            # 
             query = Post.query.filter(Post.topic_id == self.id)
             query = Post.query.filter(Post.topic_id == self.id)
             if topicsread is not None:
             if topicsread is not None:
                 query = query.filter(Post.date_created > topicsread.last_read)
                 query = query.filter(Post.date_created > topicsread.last_read)
             post = query.order_by(Post.id.asc()).first()
             post = query.order_by(Post.id.asc()).first()
             if post is not None:
             if post is not None:
                 return post.url
                 return post.url
-        
+
         return self.url
         return self.url
 
 
     # Methods
     # Methods

+ 8 - 0
flaskbb/user/models.py

@@ -70,6 +70,14 @@ class Group(db.Model, CRUDMixin):
     def get_guest_group(cls):
     def get_guest_group(cls):
         return cls.query.filter(cls.guest == True).first()
         return cls.query.filter(cls.guest == True).first()
 
 
+    @classmethod
+    def get_member_group(cls):
+        """Returns the first member group."""
+        # This feels ugly..
+        return cls.query.filter(cls.admin == False, cls.super_mod == False,
+                                cls.mod == False, cls.guest == False,
+                                cls.banned == False).first()
+
 
 
 class User(db.Model, UserMixin, CRUDMixin):
 class User(db.Model, UserMixin, CRUDMixin):
     __tablename__ = "users"
     __tablename__ = "users"

+ 5 - 0
flaskbb/utils/database.py

@@ -17,6 +17,11 @@ class CRUDMixin(object):
     def __repr__(self):
     def __repr__(self):
         return "<{}>".format(self.__class__.__name__)
         return "<{}>".format(self.__class__.__name__)
 
 
+    @classmethod
+    def create(cls, **kwargs):
+        instance = cls(**kwargs)
+        return instance.save()
+
     def save(self):
     def save(self):
         """Saves the object to the database."""
         """Saves the object to the database."""
         db.session.add(self)
         db.session.add(self)

+ 35 - 11
flaskbb/utils/populate.py

@@ -157,25 +157,49 @@ def create_default_groups():
     return result
     return result
 
 
 
 
-def create_admin_user(username, password, email):
-    """Creates the administrator user.
-    Returns the created admin user.
+def create_user(username, password, email, groupname):
+    """Creates a user.
+    Returns the created user.
 
 
     :param username: The username of the user.
     :param username: The username of the user.
     :param password: The password of the user.
     :param password: The password of the user.
     :param email: The email address of the user.
     :param email: The email address of the user.
+    :param groupname: The name of the group to which the user
+                      should belong to.
     """
     """
-    admin_group = Group.query.filter_by(admin=True).first()
-    user = User()
+    if groupname == "member":
+        group = Group.get_member_group()
+    else:
+        group = Group.query.filter(getattr(Group, groupname) == True).first()
+
+    user = User.create(username=username, password=password, email=email,
+                       primary_group_id=group.id, activated=True)
+    return user
+
+
+def update_user(username, password, email, groupname):
+    """Update an existing user.
+    Returns the updated user.
+
+    :param username: The username of the user.
+    :param password: The password of the user.
+    :param email: The email address of the user.
+    :param groupname: The name of the group to which the user
+                      should belong to.
+    """
+    user = User.query.filter_by(username=username).first()
+    if user is None:
+        return None
+
+    if groupname == "member":
+        group = Group.get_member_group()
+    else:
+        group = Group.query.filter(getattr(Group, groupname) == True).first()
 
 
-    user.username = username
     user.password = password
     user.password = password
     user.email = email
     user.email = email
-    user.primary_group_id = admin_group.id
-    user.activated = True
-
-    user.save()
-    return user
+    user.primary_group_id = group.id
+    return user.save()
 
 
 
 
 def create_welcome_forum():
 def create_welcome_forum():

+ 21 - 20
flaskbb/utils/search.py

@@ -12,6 +12,7 @@
 import whoosh
 import whoosh
 from flask_whooshee import AbstractWhoosheer
 from flask_whooshee import AbstractWhoosheer
 
 
+from flaskbb._compat import text_type
 from flaskbb.forum.models import Forum, Topic, Post
 from flaskbb.forum.models import Forum, Topic, Post
 from flaskbb.user.models import User
 from flaskbb.user.models import User
 
 
@@ -30,18 +31,18 @@ class PostWhoosheer(AbstractWhoosheer):
     def update_post(cls, writer, post):
     def update_post(cls, writer, post):
         writer.update_document(
         writer.update_document(
             post_id=post.id,
             post_id=post.id,
-            username=post.username,
-            modified_by=post.modified_by,
-            content=post.content
+            username=text_type(post.username),
+            modified_by=text_type(post.modified_by),
+            content=text_type(post.content)
         )
         )
 
 
     @classmethod
     @classmethod
     def insert_post(cls, writer, post):
     def insert_post(cls, writer, post):
         writer.add_document(
         writer.add_document(
             post_id=post.id,
             post_id=post.id,
-            username=post.username,
-            modified_by=post.modified_by,
-            content=post.content
+            username=text_type(post.username),
+            modified_by=text_type(post.modified_by),
+            content=text_type(post.content)
         )
         )
 
 
     @classmethod
     @classmethod
@@ -63,18 +64,18 @@ class TopicWhoosheer(AbstractWhoosheer):
     def update_topic(cls, writer, topic):
     def update_topic(cls, writer, topic):
         writer.update_document(
         writer.update_document(
             topic_id=topic.id,
             topic_id=topic.id,
-            title=topic.title,
-            username=topic.username,
-            content=getattr(topic.first_post,'content',None)
+            title=text_type(topic.title),
+            username=text_type(topic.username),
+            content=text_type(getattr(topic.first_post, 'content', None))
         )
         )
 
 
     @classmethod
     @classmethod
     def insert_topic(cls, writer, topic):
     def insert_topic(cls, writer, topic):
         writer.add_document(
         writer.add_document(
             topic_id=topic.id,
             topic_id=topic.id,
-            title=topic.title,
-            username=topic.username,
-            content=getattr(topic.first_post,'content',None)
+            title=text_type(topic.title),
+            username=text_type(topic.username),
+            content=text_type(getattr(topic.first_post, 'content', None))
         )
         )
 
 
     @classmethod
     @classmethod
@@ -95,16 +96,16 @@ class ForumWhoosheer(AbstractWhoosheer):
     def update_forum(cls, writer, forum):
     def update_forum(cls, writer, forum):
         writer.update_document(
         writer.update_document(
             forum_id=forum.id,
             forum_id=forum.id,
-            title=forum.title,
-            description=forum.description
+            title=text_type(forum.title),
+            description=text_type(forum.description)
         )
         )
 
 
     @classmethod
     @classmethod
     def insert_forum(cls, writer, forum):
     def insert_forum(cls, writer, forum):
         writer.add_document(
         writer.add_document(
             forum_id=forum.id,
             forum_id=forum.id,
-            title=forum.title,
-            description=forum.description
+            title=text_type(forum.title),
+            description=text_type(forum.description)
         )
         )
 
 
     @classmethod
     @classmethod
@@ -125,16 +126,16 @@ class UserWhoosheer(AbstractWhoosheer):
     def update_user(cls, writer, user):
     def update_user(cls, writer, user):
         writer.update_document(
         writer.update_document(
             user_id=user.id,
             user_id=user.id,
-            username=user.username,
-            email=user.email
+            username=text_type(user.username),
+            email=text_type(user.email)
         )
         )
 
 
     @classmethod
     @classmethod
     def insert_user(cls, writer, user):
     def insert_user(cls, writer, user):
         writer.add_document(
         writer.add_document(
             user_id=user.id,
             user_id=user.id,
-            username=user.username,
-            email=user.email
+            username=text_type(user.username),
+            email=text_type(user.email)
         )
         )
 
 
     @classmethod
     @classmethod

+ 94 - 0
flaskbb/utils/translations.py

@@ -9,12 +9,16 @@
     :license: BSD, see LICENSE for more details.
     :license: BSD, see LICENSE for more details.
 """
 """
 import os
 import os
+import subprocess
 
 
 import babel
 import babel
+from flask import current_app
 
 
 from flask_babelplus import Domain, get_locale
 from flask_babelplus import Domain, get_locale
 from flask_plugins import get_enabled_plugins
 from flask_plugins import get_enabled_plugins
 
 
+from flaskbb.extensions import plugin_manager
+
 
 
 class FlaskBBDomain(Domain):
 class FlaskBBDomain(Domain):
     def __init__(self, app):
     def __init__(self, app):
@@ -72,3 +76,93 @@ class FlaskBBDomain(Domain):
             cache[str(locale)] = translations
             cache[str(locale)] = translations
 
 
         return translations
         return translations
+
+
+def update_translations(include_plugins=False):
+    """Updates all translations.
+
+    :param include_plugins: If set to `True` it will also update the
+                            translations for all plugins.
+    """
+
+    # update flaskbb translations
+    translations_folder = os.path.join(current_app.root_path, "translations")
+    source_file = os.path.join(translations_folder, "messages.pot")
+
+    subprocess.call(["pybabel", "extract", "-F", "babel.cfg",
+                     "-k", "lazy_gettext", "-o", source_file, "."])
+    subprocess.call(["pybabel", "update", "-i", source_file,
+                     "-d", translations_folder])
+
+    if include_plugins:
+        # updates all plugin translations too
+        for plugin in plugin_manager.all_plugins:
+            update_plugin_translations(plugin)
+
+
+def add_translations(translation):
+    """Adds a new language to the translations."""
+
+    translations_folder = os.path.join(current_app.root_path, "translations")
+    source_file = os.path.join(translations_folder, "messages.pot")
+
+    subprocess.call(["pybabel", "extract", "-F", "babel.cfg",
+                     "-k", "lazy_gettext", "-o", source_file, "."])
+    subprocess.call(["pybabel", "init", "-i", source_file,
+                     "-d", translations_folder, "-l", translation])
+
+
+def compile_translations(include_plugins=False):
+    """Compiles all translations.
+
+    :param include_plugins: If set to `True` it will also compile the
+                            translations for all plugins.
+    """
+
+    # compile flaskbb translations
+    translations_folder = os.path.join(current_app.root_path, "translations")
+    subprocess.call(["pybabel", "compile", "-d", translations_folder])
+
+    if include_plugins:
+        # compile all plugin translations
+        for plugin in plugin_manager.all_plugins:
+            compile_plugin_translations(plugin)
+
+
+def add_plugin_translations(plugin, translation):
+    """Adds a new language to the plugin translations. Expects the name
+    of the plugin and the translations name like "en".
+    """
+
+    plugin_folder = os.path.join(current_app.config["PLUGINS_FOLDER"], plugin)
+    translations_folder = os.path.join(plugin_folder, "translations")
+    source_file = os.path.join(translations_folder, "messages.pot")
+
+    subprocess.call(["pybabel", "extract", "-F", "babel.cfg",
+                     "-k", "lazy_gettext", "-o", source_file,
+                     plugin_folder])
+    subprocess.call(["pybabel", "init", "-i", source_file,
+                     "-d", translations_folder, "-l", translation])
+
+
+def update_plugin_translations(plugin):
+    """Updates the plugin translations. Expects the name of the plugin."""
+
+    plugin_folder = os.path.join(current_app.config["PLUGINS_FOLDER"], plugin)
+    translations_folder = os.path.join(plugin_folder, "translations")
+    source_file = os.path.join(translations_folder, "messages.pot")
+
+    subprocess.call(["pybabel", "extract", "-F", "babel.cfg",
+                     "-k", "lazy_gettext", "-o", source_file,
+                     plugin_folder])
+    subprocess.call(["pybabel", "update", "-i", source_file,
+                     "-d", translations_folder])
+
+
+def compile_plugin_translations(plugin):
+    """Compile the plugin translations. Expects the name of the plugin."""
+
+    plugin_folder = os.path.join(current_app.config["PLUGINS_FOLDER"], plugin)
+    translations_folder = os.path.join(plugin_folder, "translations")
+
+    subprocess.call(["pybabel", "compile", "-d", translations_folder])

+ 0 - 339
manage.py

@@ -1,339 +0,0 @@
-#!/usr/bin/env python
-
-"""
-    flaskbb.manage
-    ~~~~~~~~~~~~~~~~~~~~
-
-    This script provides some easy to use commands for
-    creating the database with or without some sample content.
-    You can also run the development server with it.
-    Just type `python manage.py` to see the full list of commands.
-
-    TODO: When Flask 1.0 is released, get rid of Flask-Script and use click.
-          Then it's also possible to split the commands in "command groups"
-          which would make the commands better seperated from each other
-          and less confusing.
-
-    :copyright: (c) 2014 by the FlaskBB Team.
-    :license: BSD, see LICENSE for more details.
-"""
-from __future__ import print_function
-import sys
-import os
-import subprocess
-import requests
-import time
-
-from flask import current_app
-from werkzeug.utils import import_string
-from sqlalchemy.exc import IntegrityError, OperationalError
-from flask_script import (Manager, Shell, Server, prompt, prompt_pass,
-                          prompt_bool)
-from flask_migrate import MigrateCommand, upgrade
-
-from flaskbb import create_app
-from flaskbb.extensions import db, plugin_manager, whooshee
-from flaskbb.utils.populate import (create_test_data, create_welcome_forum,
-                                    create_admin_user, create_default_groups,
-                                    create_default_settings, insert_bulk_data,
-                                    update_settings_from_fixture)
-
-# Use the development configuration if available
-try:
-    from flaskbb.configs.development import DevelopmentConfig as Config
-except ImportError:
-    try:
-        from flaskbb.configs.production import ProductionConfig as Config
-    except ImportError:
-        from flaskbb.configs.default import DefaultConfig as Config
-
-app = create_app(Config)
-manager = Manager(app)
-
-# Used to get the plugin translations
-PLUGINS_FOLDER = os.path.join(app.root_path, "plugins")
-
-# Run local server
-manager.add_command("runserver", Server("localhost", port=8080))
-
-# Migration commands
-manager.add_command('db', MigrateCommand)
-
-
-# Add interactive project shell
-def make_shell_context():
-    return dict(app=current_app, db=db)
-manager.add_command("shell", Shell(make_context=make_shell_context))
-
-
-@manager.command
-def initdb(default_settings=True):
-    """Creates the database."""
-    upgrade()
-
-    if default_settings:
-        print("Creating default data...")
-        create_default_groups()
-        create_default_settings()
-
-
-@manager.command
-def dropdb():
-    """Deletes the database."""
-    db.drop_all()
-
-
-@manager.command
-def populate(dropdb=False, createdb=False):
-    """Creates the database with some default data.
-    To drop or create the databse use the '-d' or '-c' options.
-    """
-    if dropdb:
-        print("Dropping database...")
-        db.drop_all()
-
-    if createdb:
-        print("Creating database...")
-        upgrade()
-
-    print("Creating test data...")
-    create_test_data()
-
-
-@manager.command
-def reindex():
-    """Reindexes the search index."""
-    print("Reindexing search index...")
-    whooshee.reindex()
-
-
-@manager.option('-u', '--username', dest='username')
-@manager.option('-p', '--password', dest='password')
-@manager.option('-e', '--email', dest='email')
-def create_admin(username=None, password=None, email=None):
-    """Creates the admin user."""
-
-    if not (username and password and email):
-        username = unicode(prompt("Username"))
-        email = unicode(prompt("A valid email address"))
-        password = unicode(prompt_pass("Password"))
-
-    create_admin_user(username=username, password=password, email=email)
-
-
-@manager.option('-u', '--username', dest='username')
-@manager.option('-p', '--password', dest='password')
-@manager.option('-e', '--email', dest='email')
-def install(username=None, password=None, email=None):
-    """Installs FlaskBB with all necessary data."""
-
-    print("Creating default data...")
-    try:
-        create_default_groups()
-        create_default_settings()
-    except IntegrityError:
-        print("Couldn't create the default data because it already exist!")
-        if prompt_bool("Found an existing database."
-                       "Do you want to recreate the database? (y/n)"):
-            db.session.rollback()
-            db.drop_all()
-            upgrade()
-            create_default_groups()
-            create_default_settings()
-        else:
-            sys.exit(0)
-    except OperationalError:
-        print("No database found.")
-        if prompt_bool("Do you want to create the database now? (y/n)"):
-            db.session.rollback()
-            upgrade()
-            create_default_groups()
-            create_default_settings()
-        else:
-            sys.exit(0)
-
-    print("Creating admin user...")
-    if username and password and email:
-        create_admin_user(username=username, password=password, email=email)
-    else:
-        create_admin()
-
-    print("Creating welcome forum...")
-    create_welcome_forum()
-
-    print("Compiling translations...")
-    compile_translations()
-
-    if prompt_bool("Do you want to use Emojis? (y/n)"):
-        print("Downloading emojis. This can take a few minutes.")
-        download_emoji()
-
-    print("Congratulations! FlaskBB has been successfully installed")
-
-
-@manager.option('-t', '--topics', dest="topics", default=100)
-@manager.option('-p', '--posts', dest="posts", default=100)
-def insertbulkdata(topics, posts):
-    """Warning: This can take a long time!.
-    Creates 100 topics and each topic contains 100 posts.
-    """
-    timer = time.time()
-    created_topics, created_posts = insert_bulk_data(int(topics), int(posts))
-    elapsed = time.time() - timer
-
-    print("It took {time} seconds to create {topics} topics and "
-          "{posts} posts"
-          .format(time=elapsed, topics=created_topics, posts=created_posts))
-
-
-@manager.option('-f', '--force', dest="force", default=False)
-@manager.option('-s', '--settings', dest="settings")
-def update(settings=None, force=False):
-    """Updates the settings via a fixture. All fixtures have to be placed
-    in the `fixture`.
-    Usage: python manage.py update -s your_fixture
-    """
-    if settings is None:
-        settings = "settings"
-
-    try:
-        fixture = import_string(
-            "flaskbb.fixtures.{}".format(settings)
-        )
-        fixture = fixture.fixture
-    except ImportError:
-        raise "{} fixture is not available".format(settings)
-
-    overwrite_group = overwrite_setting = False
-    if force:
-        overwrite_group = overwrite_setting = True
-
-    count = update_settings_from_fixture(
-        fixture=fixture,
-        overwrite_group=overwrite_group,
-        overwrite_setting=overwrite_setting
-    )
-    print("{} groups and {} settings updated.".format(
-        len(count.keys()), len(count.values()))
-    )
-
-
-@manager.command
-def update_translations():
-    """Updates all translations."""
-
-    # update flaskbb translations
-    translations_folder = os.path.join(app.root_path, "translations")
-    source_file = os.path.join(translations_folder, "messages.pot")
-
-    subprocess.call(["pybabel", "extract", "-F", "babel.cfg",
-                     "-k", "lazy_gettext", "-o", source_file, "."])
-    subprocess.call(["pybabel", "update", "-i", source_file,
-                     "-d", translations_folder])
-
-    # updates all plugin translations too
-    for plugin in plugin_manager.all_plugins:
-        update_plugin_translations(plugin)
-
-
-@manager.command
-def add_translations(translation):
-    """Adds a new language to the translations."""
-
-    translations_folder = os.path.join(app.root_path, "translations")
-    source_file = os.path.join(translations_folder, "messages.pot")
-
-    subprocess.call(["pybabel", "extract", "-F", "babel.cfg",
-                     "-k", "lazy_gettext", "-o", source_file, "."])
-    subprocess.call(["pybabel", "init", "-i", source_file,
-                     "-d", translations_folder, "-l", translation])
-
-
-@manager.command
-def compile_translations():
-    """Compiles all translations."""
-
-    # compile flaskbb translations
-    translations_folder = os.path.join(app.root_path, "translations")
-    subprocess.call(["pybabel", "compile", "-d", translations_folder])
-
-    # compile all plugin translations
-    for plugin in plugin_manager.all_plugins:
-        compile_plugin_translations(plugin)
-
-
-# Plugin translation commands
-@manager.command
-def add_plugin_translations(plugin, translation):
-    """Adds a new language to the plugin translations. Expects the name
-    of the plugin and the translations name like "en".
-    """
-
-    plugin_folder = os.path.join(PLUGINS_FOLDER, plugin)
-    translations_folder = os.path.join(plugin_folder, "translations")
-    source_file = os.path.join(translations_folder, "messages.pot")
-
-    subprocess.call(["pybabel", "extract", "-F", "babel.cfg",
-                     "-k", "lazy_gettext", "-o", source_file,
-                     plugin_folder])
-    subprocess.call(["pybabel", "init", "-i", source_file,
-                     "-d", translations_folder, "-l", translation])
-
-
-@manager.command
-def update_plugin_translations(plugin):
-    """Updates the plugin translations. Expects the name of the plugin."""
-
-    plugin_folder = os.path.join(PLUGINS_FOLDER, plugin)
-    translations_folder = os.path.join(plugin_folder, "translations")
-    source_file = os.path.join(translations_folder, "messages.pot")
-
-    subprocess.call(["pybabel", "extract", "-F", "babel.cfg",
-                     "-k", "lazy_gettext", "-o", source_file,
-                     plugin_folder])
-    subprocess.call(["pybabel", "update", "-i", source_file,
-                     "-d", translations_folder])
-
-
-@manager.command
-def compile_plugin_translations(plugin):
-    """Compile the plugin translations. Expects the name of the plugin."""
-
-    plugin_folder = os.path.join(PLUGINS_FOLDER, plugin)
-    translations_folder = os.path.join(plugin_folder, "translations")
-
-    subprocess.call(["pybabel", "compile", "-d", translations_folder])
-
-
-@manager.command
-def download_emoji():
-    """Downloads emojis from emoji-cheat-sheet.com."""
-    HOSTNAME = "https://api.github.com"
-    REPO = "/repos/arvida/emoji-cheat-sheet.com/contents/public/graphics/emojis"
-    FULL_URL = "{}{}".format(HOSTNAME, REPO)
-    DOWNLOAD_PATH = os.path.join(app.static_folder, "emoji")
-
-    response = requests.get(FULL_URL)
-
-    cached_count = 0
-    count = 0
-    for image in response.json():
-        if not os.path.exists(os.path.abspath(DOWNLOAD_PATH)):
-            print("{} does not exist.".format(os.path.abspath(DOWNLOAD_PATH)))
-            sys.exit(1)
-
-        full_path = os.path.join(DOWNLOAD_PATH, image["name"])
-        if not os.path.exists(full_path):
-            count += 1
-            f = open(full_path, 'wb')
-            f.write(requests.get(image["download_url"]).content)
-            f.close()
-            if count == cached_count + 50:
-                cached_count = count
-                print("{} out of {} Emojis downloaded...".format(
-                      cached_count, len(response.json())))
-
-    print("Finished downloading {} Emojis.".format(count))
-
-if __name__ == "__main__":
-    manager.run()

+ 6 - 4
migrations/versions/221d918aa9f0_add_user_authentication_infos.py

@@ -15,10 +15,12 @@ import sqlalchemy as sa
 
 
 
 
 def upgrade():
 def upgrade():
-    ### commands auto generated by Alembic - please adjust! ###
-    op.add_column('users', sa.Column('activated', sa.Boolean(), nullable=True))
-    op.add_column('users', sa.Column('last_failed_login', sa.DateTime(), nullable=True))
-    op.add_column('users', sa.Column('login_attempts', sa.Integer(), nullable=True))
+    # has no effect on other DBMS
+    # read here for more info: http://alembic.zzzcomputing.com/en/latest/ops.html#alembic.operations.Operations.batch_alter_table
+    with op.batch_alter_table('users', schema=None) as batch_op:
+        batch_op.add_column(sa.Column('activated', sa.Boolean(), nullable=True))
+        batch_op.add_column(sa.Column('last_failed_login', sa.DateTime(), nullable=True))
+        batch_op.add_column(sa.Column('login_attempts', sa.Integer(), nullable=True))
     ### end Alembic commands ###
     ### end Alembic commands ###
 
 
 
 

+ 27 - 26
requirements.txt

@@ -1,47 +1,48 @@
-alembic==0.8.8
-amqp==1.4.9
+alembic==0.8.9
+amqp==2.1.4
 anyjson==0.3.3
 anyjson==0.3.3
 Babel==2.3.4
 Babel==2.3.4
-billiard==3.3.0.23
+billiard==3.5.0.2
 blinker==1.4
 blinker==1.4
-celery==3.1.24
-click==6.6
-Flask==0.11.1
-flask-allows==0.1.0
+celery==4.0.2
+click==6.7
+Flask==0.12
+flask-allows==0.2.0
 Flask-BabelPlus==1.0.1
 Flask-BabelPlus==1.0.1
-Flask-Caching==1.0.1
+Flask-Caching==1.1.1
 Flask-DebugToolbar==0.10.0
 Flask-DebugToolbar==0.10.0
 Flask-Limiter==0.9.3
 Flask-Limiter==0.9.3
-Flask-Login==0.3.2
+Flask-Login==0.4.0
 Flask-Mail==0.9.1
 Flask-Mail==0.9.1
-Flask-Migrate==2.0.0
+Flask-Migrate==2.0.2
 Flask-Plugins==1.6.1
 Flask-Plugins==1.6.1
 Flask-Redis==0.3.0
 Flask-Redis==0.3.0
 Flask-Script==2.0.5
 Flask-Script==2.0.5
 Flask-SQLAlchemy==2.1
 Flask-SQLAlchemy==2.1
 Flask-Themes2==0.1.4
 Flask-Themes2==0.1.4
-flask-whooshee==0.2.3
-Flask-WTF==0.13.1
+flask-whooshee==0.4.1
+Flask-WTF==0.14.2
 itsdangerous==0.24
 itsdangerous==0.24
-Jinja2==2.8
-kombu==3.0.37
-limits==1.1.1
-Mako==1.0.4
+Jinja2==2.9.4
+kombu==4.0.2
+limits==1.2.1
+Mako==1.0.6
 MarkupSafe==0.23
 MarkupSafe==0.23
 mistune==0.7.3
 mistune==0.7.3
-Pillow==3.4.1
+olefile==0.44
+Pillow==4.0.0
 Pygments==2.1.3
 Pygments==2.1.3
-python-editor==1.0.1
-pytz==2016.7
+python-editor==1.0.3
+pytz==2016.10
 redis==2.10.5
 redis==2.10.5
-requests==2.11.1
-simplejson==3.8.2
+requests==2.12.4
+simplejson==3.10.0
 six==1.10.0
 six==1.10.0
 speaklater==1.3
 speaklater==1.3
-SQLAlchemy==1.1.1
-SQLAlchemy-Utils==0.32.9
-Unidecode==0.4.19
-vine==1.1.2
-Werkzeug==0.11.11
+SQLAlchemy==1.1.4
+SQLAlchemy-Utils==0.32.12
+Unidecode==0.4.20
+vine==1.1.3
+Werkzeug==0.11.15
 Whoosh==2.7.4
 Whoosh==2.7.4
 WTForms==2.1
 WTForms==2.1

+ 14 - 10
setup.py

@@ -9,21 +9,22 @@ And Easy to Setup
 -----------------
 -----------------
 
 
 .. code:: bash
 .. code:: bash
-    $ python manage.py createall
+    $ pip install -e .
 
 
-    $ python manage.py runserver
+    $ flaskbb install
+
+    $ flaskbb runserver
      * Running on http://localhost:8080/
      * Running on http://localhost:8080/
 
 
 
 
 Resources
 Resources
 ---------
 ---------
 
 
-* `website <http://flaskbb.org>`_
+* `website <https://flaskbb.org>`_
 * `source <https://github.com/sh4nks/flaskbb>`_
 * `source <https://github.com/sh4nks/flaskbb>`_
 * `issues <https://github.com/sh4nks/flaskbb/issues>`_
 * `issues <https://github.com/sh4nks/flaskbb/issues>`_
-
 """
 """
-from setuptools import setup
+from setuptools import setup, find_packages
 from setuptools.command.test import test as TestCommand
 from setuptools.command.test import test as TestCommand
 import sys
 import sys
 
 
@@ -51,11 +52,11 @@ setup(
     version='1.0.dev0',
     version='1.0.dev0',
     url='http://github.com/sh4nks/flaskbb/',
     url='http://github.com/sh4nks/flaskbb/',
     license='BSD',
     license='BSD',
-    author='sh4nks',
-    author_email='sh4nks7@gmail.com',
-    description='A forum software written with flask',
+    author='Peter Justin',
+    author_email='peter.justin@outlook.com',
+    description='A classic Forum Software in Python using Flask.',
     long_description=__doc__,
     long_description=__doc__,
-    packages=['flaskbb'],
+    packages=find_packages(),
     include_package_data=True,
     include_package_data=True,
     zip_safe=False,
     zip_safe=False,
     platforms='any',
     platforms='any',
@@ -81,7 +82,6 @@ setup(
         'Flask-Migrate',
         'Flask-Migrate',
         'Flask-Plugins',
         'Flask-Plugins',
         'Flask-Redis',
         'Flask-Redis',
-        'Flask-Script',
         'Flask-SQLAlchemy',
         'Flask-SQLAlchemy',
         'Flask-Themes2',
         'Flask-Themes2',
         'flask-whooshee',
         'flask-whooshee',
@@ -109,6 +109,10 @@ setup(
         'Whoosh',
         'Whoosh',
         'WTForms'
         'WTForms'
     ],
     ],
+    entry_points='''
+        [console_scripts]
+        flaskbb=flaskbb.cli:flaskbb
+    ''',
     test_suite='tests',
     test_suite='tests',
     tests_require=[
     tests_require=[
         'py',
         'py',

+ 5 - 5
tests/unit/utils/test_populate.py

@@ -42,12 +42,12 @@ def test_update_settings_from_fixture(database):
     assert len(force_updated) == SettingsGroup.query.count()
     assert len(force_updated) == SettingsGroup.query.count()
 
 
 
 
-def test_create_admin_user(default_groups):
+def test_create_user(default_groups):
     user = User.query.filter_by(username="admin").first()
     user = User.query.filter_by(username="admin").first()
     assert not user
     assert not user
 
 
-    user = create_admin_user(username="admin", password="test",
-                             email="test@example.org")
+    user = create_user(username="admin", password="test",
+                       email="test@example.org", groupname="admin")
     assert user.username == "admin"
     assert user.username == "admin"
     assert user.permissions["admin"]
     assert user.permissions["admin"]
 
 
@@ -55,8 +55,8 @@ def test_create_admin_user(default_groups):
 def test_create_welcome_forum(default_groups):
 def test_create_welcome_forum(default_groups):
     assert not create_welcome_forum()
     assert not create_welcome_forum()
 
 
-    create_admin_user(username="admin", password="test",
-                      email="test@example.org")
+    create_user(username="admin", password="test",
+                email="test@example.org", groupname="admin")
     assert create_welcome_forum()
     assert create_welcome_forum()