Browse Source

Added dynamic form composition for the setting fields.

sh4nks 11 years ago
parent
commit
df006a5686
5 changed files with 165 additions and 99 deletions
  1. 4 0
      flaskbb/admin/forms.py
  2. 112 8
      flaskbb/admin/models.py
  3. 20 2
      flaskbb/admin/views.py
  4. 7 0
      flaskbb/app.py
  5. 22 89
      flaskbb/templates/admin/settings.html

+ 4 - 0
flaskbb/admin/forms.py

@@ -354,3 +354,7 @@ class CategoryForm(Form):
     def save(self):
         category = Category(**self.data)
         return category.save()
+
+
+class SettingsForm(Form):
+    pass

+ 112 - 8
flaskbb/admin/models.py

@@ -5,14 +5,16 @@ try:
 except ImportError:
     import pickle
 
+from wtforms import (TextField, IntegerField, BooleanField, SelectField,
+                     FloatField, validators)
+from flask.ext.wtf import Form
 from flaskbb.extensions import db, cache
 
 
 def normalize_to(value, value_type, reverse=False):
     """Converts a value to a specified value type.
-    Available value types are: string, int, boolean, list.
+    Available value types are: string, integer, boolean and array.
     A boolean type is handled as 0 for false and 1 for true.
-    Raises a exception if the value couldn't be converted
 
     :param value: The value which should be converted.
     :param value_type: The value_type.
@@ -77,12 +79,14 @@ class Setting(db.Model):
     # Available types: string, integer, boolean, array, float
     value_type = db.Column(db.String, nullable=False)
 
-    # Available types: text, choice, yesno (used for the form creation process)
+    # Available types: text, number, choice, yesno
+    # They are used in the form creation process
     input_type = db.Column(db.String, nullable=False)
 
     # Extra attributes like, validation things (min, max length...)
     _extra = db.Column("extra", db.String)
 
+    # Properties
     @property
     def value(self):
         return normalize_to(self._value, self.value_type)
@@ -101,17 +105,117 @@ class Setting(db.Model):
             pickle.dumps((extra), pickle.HIGHEST_PROTOCOL)
         )
 
-    def get_field(self):
-        pass
-
     @classmethod
     @cache.memoize(timeout=sys.maxint)
+    def config(self):
+        """Returns the configs as a dict (only self.key and self.value).
+        If a value/key has changed, you need to invalidate the cache."""
+        settings = {}
+        for setting in self.get_all():
+            settings[setting.key.upper()] = setting.value
+
+        return settings
+
+    @classmethod
     def get_all(cls):
-        pass
+        return cls.query.all()
+
+    @classmethod
+    def get_form(cls, group):
+        """Returns a Form for all settings found in :class:`SettingsGroup`.
+
+        :param group: The settingsgroup name. It is used to get the settings
+                      which are in the specified group. Aborts with 404 if the
+                      group is found.
+        """
+        settings = SettingsGroup.query.filter_by(key=group).first_or_404()
+
+        class SettingsForm(Form):
+            pass
+
+        # now parse that shit
+        for setting in settings.settings:
+            field_validators = []
+
+            # generate the validators
+            # TODO: Do this in another function
+            if "min" in setting.extra:
+                # Min number validator
+                if setting.value_type in ("integer", "float"):
+                    field_validators.append(
+                        validators.NumberRange(min=setting.extra["min"])
+                    )
+
+                # Min text length validator
+                elif setting.value_type in ("string", "array"):
+                    field_validators.append(
+                        validators.Length(min=setting.extra["min"])
+                    )
+
+            if "max" in setting.extra:
+                # Max number validator
+                if setting.value_type in ("integer", "float"):
+                    field_validators.append(
+                        validators.NumberRange(max=setting.extra["max"])
+                    )
+
+                # Max text length validator
+                elif setting.value_type in ("string", "array"):
+                    field_validators.append(
+                        validators.Length(max=setting.extra["max"])
+                    )
+
+            # Generate the fields based on input_type and value_type
+            if setting.input_type == "number":
+                # IntegerField
+                if setting.value_type == "integer":
+                    setattr(
+                        SettingsForm, setting.key,
+                        IntegerField(setting.name, field_validators)
+                    )
+                # FloatField
+                elif setting.value_type == "float":
+                    setattr(
+                        SettingsForm, setting.key,
+                        FloatField(setting.name, field_validators)
+                    )
+
+            # TextField
+            if setting.input_type == "text":
+                setattr(
+                    SettingsForm, setting.key,
+                    TextField(setting.name, field_validators)
+                )
+
+            # SelectField
+            if setting.input_type == "choice" and "choices" in setting.extra:
+                setattr(
+                    SettingsForm, setting.key,
+                    SelectField(setting.name, choices=setting.extra['choices'])
+                )
+
+            # BooleanField
+            if setting.input_type == "yesno":
+                setattr(
+                    SettingsForm, setting.key,
+                    BooleanField(setting.name)
+                )
+
+        return SettingsForm()
+
+    @classmethod
+    def update(self, app):
+        """Updates the config for the app
+
+        :param app: The application.
+        """
+        self.invalidate_cache()
+
+        app.config.update(self.config())
 
     def save(self):
         db.session.add(self)
         db.session.commit()
 
     def invalidate_cache(self):
-        cache.delete_memoized(self.get_all, self)
+        cache.delete_memoized(self.config, self)

+ 20 - 2
flaskbb/admin/views.py

@@ -23,6 +23,7 @@ from flaskbb.utils.decorators import admin_required
 from flaskbb.extensions import db
 from flaskbb.user.models import User, Group
 from flaskbb.forum.models import Post, Topic, Forum, Category, Report
+from flaskbb.admin.models import Setting, SettingsGroup
 from flaskbb.admin.forms import (AddUserForm, EditUserForm, AddGroupForm,
                                  EditGroupForm, EditForumForm, AddForumForm,
                                  CategoryForm)
@@ -48,9 +49,26 @@ def overview():
 
 
 @admin.route("/settings", methods=["GET", "POST"])
+@admin.route("/settings/<path:slug>", methods=["GET", "POST"])
 @admin_required
-def settings():
-    return render_template("admin/settings.html", themes=[])
+def settings(slug=None):
+    # Get all settinggroups so that we can build the settings navigation
+    settingsgroup = SettingsGroup.query.all()
+
+    if slug is not None:
+        form = Setting.get_form(slug)
+    else:
+        # or should we display an index with all available settingsgroups?
+        form = Setting.get_form("general")
+
+    if form.validate_on_submit():
+        # update the db rows
+        # invalidate the cache
+        # update the app config
+        pass
+
+    return render_template("admin/settings.html", form=form,
+                           settingsgroup=settingsgroup)
 
 
 @admin.route("/users", methods=['GET', 'POST'])

+ 7 - 0
flaskbb/app.py

@@ -22,6 +22,7 @@ from flaskbb.user.models import User, Guest, PrivateMessage
 from flaskbb.auth.views import auth
 # Import the admin blueprint
 from flaskbb.admin.views import admin
+from flaskbb.admin.models import Setting
 # Import the forum blueprint
 from flaskbb.forum.views import forum
 from flaskbb.forum.models import Post, Topic, Category, Forum
@@ -59,6 +60,7 @@ def create_app(config=None):
     configure_before_handlers(app)
     configure_errorhandlers(app)
     configure_logging(app)
+    update_settings_from_db(app)
 
     return app
 
@@ -131,6 +133,11 @@ def configure_extensions(app):
     login_manager.init_app(app)
 
 
+def update_settings_from_db(app):
+    with app.app_context():
+        app.config.update(Setting.config())
+
+
 def configure_template_filters(app):
     """
     Configures the template filters

+ 22 - 89
flaskbb/templates/admin/settings.html

@@ -1,100 +1,33 @@
 {% extends theme("admin/admin_layout.html") %}
 {% block admin_content %}
-{% from theme('macros.html') import render_pagination %}
+{% from theme('macros.html') import render_boolean_field, render_select_field, render_field %}
 
 <legend>Settings</legend>
 
 <form class="form-horizontal" role="form" method="post">
 
-  <div class="form-group">
-    <label for="project_title" class="col-sm-2 control-label">Project title</label>
-    <div class="col-sm-10">
-      <input type="text" class="form-control" name="project_title", id="project_title", value="{{ config['PROJECT_TITLE'] }}">
-      <span class="help-block">The name of your project</span>
+    {% for field in form %}
+        {% if field.type in ["HiddenField", "CSRFTokenField"] %}
+            {{ field() }}
+        {% endif %}
+        {% if field.type not in ["TextField", "IntegerField"] %}
+            {% if field.type == "BooleanField" %}
+                {{ render_boolean_field }}
+            {% endif %}
+
+            {% if field.type in ["SelectField", "SelectMultipleField"] %}
+                {{ render_select_field(field) }}
+            {% endif %}
+        {% else %}
+            {{ render_field(field) }}
+        {% endif %}
+    {%  endfor %}
+
+    <div class="form-group">
+        <div class="col-sm-offset-2 col-sm-10">
+            <button type="submit" class="btn btn-default">Save</button>
+        </div>
     </div>
-  </div>
-
-  <div class="form-group">
-    <label for="project_subtitle" class="col-sm-2 control-label">Project subtitle</label>
-    <div class="col-sm-10">
-      <input type="text" class="form-control" name="project_subtitle", id="project_subtitle", value="{{ config['PROJECT_SUBTITLE'] }}">
-      <span class="help-block">The subtitle of your project (if any).</span>
-    </div>
-  </div>
-
-  <div class="form-group">
-    <label class="col-sm-2 control-label" for="default_theme">Default theme</label>
-    <div class="col-sm-10">
-      <select class="form-control" id="default_theme" name="default_theme">
-        <option selected value="{{ config['DEFAULT_THEME'].identifier }}">{{ config['DEFAULT_THEME'].name }}</option>
-        {% for theme in themes %}
-          <option value="{{ theme.identifier }}">{{ theme.name }}</option>
-        {% endfor %}
-      </select>
-      <span class="help-block">The default theme for FlaskBB.</span>
-    </div>
-  </div>
-
-  <div class="form-group">
-    <label class="col-sm-2 control-label" for="tracker_length">Tracker length</label>
-    <div class="col-sm-10">
-    <input type="text" class="form-control" name="tracker_length" id="tracker_length", value="{{ config['TRACKER_LENGTH'] }}">
-      <span class="help-block">The tracker length defines how long the topics will stay as unread. <b>0</b> to disable it.</span>
-    </div>
-  </div>
-
-  <div class="form-group">
-    <label class="col-sm-2 control-label" for="users_per_page">Users Per Page</label>
-    <div class="col-sm-10">
-    <input type="text" class="form-control" name="users_per_page" id="users_per_page", value="{{ config['USERS_PER_PAGE'] }}">
-      <span class="help-block">How many users per page are displayed.</span>
-    </div>
-  </div>
-
-  <div class="form-group">
-    <label class="col-sm-2 control-label" for="topics_per_page">Topics Per Page</label>
-    <div class="col-sm-10">
-    <input type="text" class="form-control" name="topics_per_page" id="topics_per_page", value="{{ config['TOPICS_PER_PAGE'] }}">
-      <span class="help-block">How many topics per page are displayed.</span>
-    </div>
-  </div>
-
-  <div class="form-group">
-    <label class="col-sm-2 control-label" for="posts_per_page">Posts Per Page</label>
-    <div class="col-sm-10">
-    <input type="text" class="form-control" name="posts_per_page" id="posts_per_page", value="{{ config['POSTS_PER_PAGE'] }}">
-      <span class="help-block">How many posts per page are displayed.</span>
-    </div>
-  </div>
-
-  <div class="form-group">
-    <label class="col-sm-2 control-label" for="online_last_minutes">Online Last Minutes</label>
-    <div class="col-sm-10">
-    <input type="text" class="form-control" name="online_last_minutes" id="online_last_minutes", value="{{ config['ONLINE_LAST_MINUTES'] }}">
-      <span class="help-block">How long the use can be inactive before he is marked as offline.</span>
-    </div>
-  </div>
-
-  <!--
-  <div class="form-group">
-    <label for="optionsRadio" class="col-sm-2 control-label">Simple yes/no</label>
-    <div class="col-sm-10">
-      <label class="radio-inline">
-        <input type="radio" name="optionsRadios" id="optionsRadios1" value="option1" checked> Yes
-      </label>
-      <label class="radio-inline">
-        <input type="radio" name="optionsRadios" id="optionsRadios2" value="option2"> No
-      </label>
-      <span class="help-block">A block of help text that breaks onto a new line and may extend beyond one line.</span>
-    </div>
-  </div>
-  -->
-
-  <div class="form-group">
-    <div class="col-sm-offset-2 col-sm-10">
-      <button type="submit" class="btn btn-default">Save</button>
-    </div>
-  </div>
 
 </form>