Skip to content

Commit v0.22

kwmccabe edited this page Apr 17, 2018 · 8 revisions

v0.22 - User module: tables.sql, models, forms, views, templates


Files changed (15)

File mysql/scripts/seeddata.sql MODIFIED

  • Add test row for table flaskapp.user.
+DELETE FROM `user`;
+INSERT INTO `user` (keyname) VALUES ("admin");
+OPTIMIZE TABLE `user`;

File mysql/scripts/tables.sql MODIFIED

  • Add minimal database table user.
+-- -------------------------------------------- --
+-- user
+-- -------------------------------------------- --
+DROP TABLE IF EXISTS `user`;
+CREATE TABLE `user` (
+    `id`            bigint(20)      NOT NULL AUTO_INCREMENT,
+    `keyname`       varchar(63)     NOT NULL,
+    `active`        tinyint(1)      NOT NULL DEFAULT '1',
+
+    PRIMARY KEY (`id`),
+    UNIQUE KEY `user_keyname` (`keyname`),
+    KEY `user_active` (`active`)
+) ENGINE=InnoDB AUTO_INCREMENT=1000 DEFAULT CHARSET=utf8;
+DESCRIBE `user`;
+SELECT "table `user` created" AS MSG;

File web/app/init.py MODIFIED

  • Add the user module via app.register_blueprint().
+    from .user import user as user_blueprint
+    app.register_blueprint(user_blueprint)

File web/app/main/templates/admin.html MODIFIED

  • Add link from /admin to the Admin Users area.
 <ol>
 <li><a href="{{ url_for('item.item_list') }}">Items</a></li>
+<li><a href="{{ url_for('user.user_list') }}">Users</a></li>
 </ol>

File web/app/main/templates/navbar_main.html MODIFIED

  • Add main navbar link for public page /user/.
  • Add main navbar link for admin page /admin/user/list.
       <ul class="nav navbar-nav">
         <!-- li class="active"><a href="#">Link <span class="sr-only">(current)</span></a></li -->
         <li><a href="{{ url_for('item.item_page') }}">Items</a></li>
-        <li><a href="#">Users</a></li>
+        <li><a href="{{ url_for('user.user_page') }}">Users</a></li>
         <li class="dropdown">
           <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Admin <span class="caret"></span></a>
           <ul class="dropdown-menu">
             <li><a href="{{ url_for('main.main_page', page='admin') }}">Dashboard</a></li>
             <li role="separator" class="divider"></li>
             <li><a href="{{ url_for('item.item_list') }}">Items</a></li>
-            <li><a href="#">Users</a></li>
+            <li><a href="{{ url_for('user.user_list') }}">Users</a></li>
           </ul>
         </li>
       </ul>

File web/app/user/init.py ADDED

  • Init the user module.
  • Create a Blueprint object user for use by create_app() in web/app/__init__.py.
+from flask import Blueprint
+
+user = Blueprint('user', __name__, template_folder='templates')
+
+from . import views

File web/app/user/forms.py ADDED

  • Create minimal CreateUserForm with keyname field.
  • Create minimal EditUserForm with id, keyname, and active fields.
+import re
+
+from flask_wtf import FlaskForm
+from wtforms import BooleanField, DecimalField, FloatField, IntegerField, \
+    DateTimeField, DateField, \
+    FileField, PasswordField, StringField, TextAreaField, \
+    RadioField, SelectField, SelectMultipleField, \
+    HiddenField, SubmitField
+from wtforms.validators import Email, InputRequired, Length
+from wtforms import ValidationError
+#from flask_pagedown.fields import PageDownField
+from .models import UserModel
+
+
+def filter_username(data):
+    return re.sub('[^a-z0-9_-]', '', str(data).lower())
+
+def validate_username(self, field):
+    if field.data != self.user.keyname and \
+            UserModel.query.filter_by(keyname=field.data).first():
+        raise ValidationError('Username already in use.')
+
+
+class CreatUserForm(FlaskForm):
+    keyname    = StringField('Username', validators=[InputRequired(),Length(2,63),validate_username], filters=[filter_username])
+    submit     = SubmitField('Create User')
+
+    def __init__(self, user, *args, **kwargs):
+        super(CreatUserForm, self).__init__(*args, **kwargs)
+        #self.role.choices = [(role.id, role.name) for role in Role.query.order_by(Role.name).all()]
+        self.user = user
+
+
+class EditUserForm(FlaskForm):
+    id         = HiddenField('id')
+    keyname    = StringField('Username', validators=[InputRequired(),Length(2,63),validate_username], filters=[filter_username])
+    active     = BooleanField('Active')
+    submit     = SubmitField('Update User')
+
+    def __init__(self, user, *args, **kwargs):
+        super(EditUserForm, self).__init__(*args, **kwargs)
+        self.user = user

File web/app/user/models.py ADDED

  • Create minimal UserModel.
+from flask import url_for
+from .. import db
+
+
+class UserModel(db.Model):
+    __tablename__ = 'user'
+    id         = db.Column(db.BigInteger, autoincrement=True, primary_key=True)
+    keyname    = db.Column(db.String(63), nullable=False, unique=True, index=True, default='')
+    active     = db.Column(db.Boolean, nullable=False, index=True, default=1)
+
+    def to_json(self):
+        json_user = {
+            #'url': url_for('api.get_user', id=self.id),
+            'id'        : self.id,
+            'keyname'   : self.keyname,
+            'active'    : self.active,
+        }
+        return json_user
+
+    def __repr__(self):
+        return '<UserModel %r>' % (self.id)

File web/app/user/templates/user_create.html ADDED

  • Template for route /admin/user/create.
+{% extends "base.html" %}
+
+
+<!-- BLOCK: title -->
+{% block title %}{{super()}} - Admin - Create User{% endblock %}
+
+
+<!-- BLOCK: breadcrumb -->
+{% block breadcrumb %}
+{{super()}}
+<li><a href="{{ url_for('main.main_page', page='admin') }}">Admin</a></li>
+<li><a href="{{ url_for('.user_list') }}">Users</a></li>
+<li><a href="{{ url_for('.user_create') }}">Create</a></li>
+{% endblock %}
+
+
+<!-- BLOCK: content -->
+{% block content %}
+<div id="item_create_panel" class="panel panel-info">
+    <div class="panel-heading">
+        <h3 class="condensed">Create User</h3>
+    </div>
+    <div class="panel-body">
+
+<form class="form-horizontal" action="{{ url_for('.user_create') }}" method="post">
+    {{ form.csrf_token }}
+    <div class="form-group">
+        *{{ form.keyname.label }}
+        {{ form.keyname(class_='form-control',placeholder='keyname') }}
+        <p class="help-block">unique textual identifier - letters, numbers, dashes and underscores allowed, no spaces</p>
+    </div>
+    {{ form.submit(class_='btn btn-primary') }}
+    <span class="form-submit-buttons">
+        <a class="btn btn-default" href="{{ url_for('.user_list') }}">Cancel</a>
+    </span>
+</form>
+
+    </div> <!-- end class="panel-body" -->
+</div> <!-- end class="panel" -->
+{% endblock %}
+
+
+<!-- BLOCK: templates -->
+{% block templates %}{{super()}} - user_create.html{% endblock %}
+
+{# end web/app/user/templates/user_create.html #}

File web/app/user/templates/user_edit.html ADDED

  • Template for route /admin/user/edit/<int:id>.
+{% extends "base.html" %}
+
+
+<!-- BLOCK: title -->
+{% block title %}{{super()}} - Admin - Edit '{{ form.user.keyname }}'{% endblock %}
+
+
+<!-- BLOCK: breadcrumb -->
+{% block breadcrumb %}
+{{super()}}
+<li><a href="{{ url_for('main.main_page', page='admin') }}">Admin</a></li>
+<li><a href="{{ url_for('.user_list') }}">Users</a></li>
+<li><a href="{{ url_for('.user_edit', id=form.id.data) }}">Edit '{{ form.user.keyname }}'</a></li>
+{% endblock %}
+
+
+<!-- BLOCK: content -->
+{% block content %}
+<div id="item_edit_panel" class="panel panel-info">
+    <div class="panel-heading">
+<p style="float: right;">
+[ <a href="{{ url_for('.user_view', id=form.id.data) }}">View User</a> ]
+[ <a href="{{ url_for('.user_edit', id=form.id.data) }}">Edit User</a> ]
+</p>
+        <h3 class="condensed">Edit User</h3>
+    </div>
+    <div class="panel-body">
+
+<form class="form-horizontal" action="{{ url_for('.user_edit', id=form.id.data) }}" method="post">
+    {{ form.csrf_token }}
+    {{ form.id }}
+    <div class="form-group">
+        *{{ form.keyname.label }}
+        {{ form.keyname(class_='form-control',placeholder='keyname') }}
+        <p class="help-block">unique textual identifier - letters, numbers, dashes and underscores allowed - no spaces</p>
+    </div>
+    <div class="form-group">
+        {{ form.active.label }}
+        {{ form.active(class_='form-control') }}
+    </div>
+    {{ form.submit(class_='btn btn-primary') }}
+    <span class="form-submit-buttons">
+        <a data-confirm-link="Delete User? This action cannot be undone." href="{{ url_for('.user_delete', id=form.id.data) }}"><span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete User</a>
+    </span>
+</form>
+
+    </div> <!-- end class="panel-body" -->
+</div> <!-- end class="panel" -->
+{% endblock %}
+
+
+<!-- BLOCK: templates -->
+{% block templates %}{{super()}} - user_edit.html{% endblock %}
+
+{# end web/app/user/templates/user_edit.html #}

File web/app/user/templates/user_index.html ADDED

  • Template for route /user/.
+{% extends "base.html" %}
+
+<!-- BLOCK: title -->
+{% block title %}{{super()}} - Users{% endblock %}
+
+<!-- BLOCK: breadcrumb -->
+{% block breadcrumb %}
+{{super()}}
+<li><a href="{{ url_for('.user_page') }}">Users</a></li>
+{% endblock %}
+
+<!-- BLOCK: content -->
+{% block content %}
+
+<p>This is public landing page for the User Module.</p>
+
+{% endblock %}
+
+
+<!-- BLOCK: templates -->
+{% block templates %}{{super()}} - user_index.html{% endblock %}
+
+{# end web/app/user/templates/user_index.html #}

File web/app/user/templates/user_list.html ADDED

  • Template for route /admin/user/list.
+{% extends "base.html" %}
+
+
+<!-- BLOCK: title -->
+{% block title %}{{super()}} - Admin - Users{% endblock %}
+
+
+<!-- BLOCK: breadcrumb -->
+{% block breadcrumb %}
+{{super()}}
+<li><a href="{{ url_for('main.main_page', page='admin') }}">Admin</a></li>
+<li><a href="{{ url_for('.user_list') }}">Users</a></li>
+{% endblock %}
+
+
+<!-- BLOCK: content -->
+{% block content %}
+<form class="form-inline" action="{{ url_for('.user_action') }}" method="post">
+
+<div id="item_list_panel" class="panel panel-info">
+    <div class="panel-heading">
+
+<div class="table-responsive">
+<table class="table table-condensed table-borderless">
+<tr>
+<td>
+    Showing {{ rowcnt }} of {{ session[opts_key]['itemcnt'] }} Users
+</td>
+<td class="text-right">
+    {% if session[opts_key]['page'] > 1 %}
+        <a href="{{ url_for('.user_list') }}?page=1"><span class="glyphicon glyphicon-backward" aria-hidden="true"></span></a>
+        <a href="{{ url_for('.user_list') }}?page={{ session[opts_key]['page'] - 1 }}"><span class="glyphicon glyphicon-arrow-left" aria-hidden="true"></span></a>
+    {% elif session[opts_key]['pagecnt'] > 1 %}
+        <span class="glyphicon glyphicon-backward" aria-hidden="true"></span>
+        <span class="glyphicon glyphicon-arrow-left" aria-hidden="true"></span>
+    {% endif %}
+    <span>Page {{ session[opts_key]['page'] }} of {{ session[opts_key]['pagecnt'] }}</span>
+    {% if session[opts_key]['page'] < session[opts_key]['pagecnt'] %}
+        <a href="{{ url_for('.user_list') }}?page={{ session[opts_key]['page'] + 1 }}"><span class="glyphicon glyphicon-arrow-right" aria-hidden="true"></span></a>
+        <a href="{{ url_for('.user_list') }}?page={{ session[opts_key]['pagecnt'] }}"><span class="glyphicon glyphicon-forward" aria-hidden="true"></span></a>
+    {% elif session[opts_key]['pagecnt'] > 1 %}
+        <span class="glyphicon glyphicon-arrow-right" aria-hidden="true"></span>
+        <span class="glyphicon glyphicon-forward" aria-hidden="true"></span>
+    {% endif %}
+</td>
+</tr>
+<tr>
+<td>
+    [ <a href="{{ url_for('.user_create') }}">Create User</a> ]
+</td>
+<td class="text-right">
+    {% if session[opts_key]['status'] == 'active' %}
+        [<a href="{{ url_for('.user_list') }}?status=all">All</a>]
+        [Active]
+        [<a href="{{ url_for('.user_list') }}?status=inactive">Inactive</a>]
+    {% elif session[opts_key]['status'] == 'inactive' %}
+        [<a href="{{ url_for('.user_list') }}?status=all">All</a>]
+        [<a href="{{ url_for('.user_list') }}?status=active">Active</a>]
+        [Inactive]
+    {% else %}
+        [All]
+        [<a href="{{ url_for('.user_list') }}?status=active">Active</a>]
+        [<a href="{{ url_for('.user_list') }}?status=inactive">Inactive</a>]
+    {% endif %}
+</td>
+</tr>
+</table>
+</div>
+
+    </div> <!-- end class="panel-heading" -->
+    <div class="panel-body">
+
+
+<div class="table-responsive">
+<table class="table table-condensed table-hover">
+<tr>
+<th>
+<input type="checkbox" class="checkall" onclick="checkAll('user_id',this.checked);" />
+</th>
+{% for col in cols %}
+    <th>
+    {% if col == session[opts_key]['sort'] and session[opts_key]['order'] == 'asc' %}
+        <a href="{{ url_for('.user_list') }}?sort={{ col }}&order=desc"><span class="glyphicon glyphicon-sort-by-attributes" aria-hidden="true"></span></a>
+    {% elif col == session[opts_key]['sort'] %}
+        <a href="{{ url_for('.user_list') }}?sort={{ col }}&order=asc"><span class="glyphicon glyphicon-sort-by-attributes-alt" aria-hidden="true"></span></a>
+    {% else %}
+        <a href="{{ url_for('.user_list') }}?sort={{ col }}"><span class="glyphicon glyphicon-sort" aria-hidden="true"></span></a>
+    {% endif %}
+    {{ col }}
+    </th>
+{% endfor %}
+<th></th>
+</tr>
+
+{% for row in rows %}
+    <tr>
+    <td>
+        <input type="checkbox" name="user_id" value="{{row.id}}">
+    </td>
+    {% for col in cols %}
+        <td onclick="window.location='{{ url_for('.user_view', id=row.id) }}';">{{ row[col] }}</td>
+    {% endfor %}
+    <td>
+        <a href="{{ url_for('.user_edit', id=row.id) }}"><span class="glyphicon glyphicon-pencil" aria-hidden="true"></span></a>
+    </td>
+    </tr>
+{% endfor %}
+
+</table>
+</div>
+
+    </div> <!-- end class="panel-body" -->
+    <div class="panel-footer">
+<div id="list-actions">
+    <div class="form-group">
+        <label for="action" class="sr-only">Action</label>
+        <select id="action" name="action" class="form-control">
+            <option value="">Selected Users...</option>
+            <option value="active">Set Active</option>
+            <option value="inactive">Set Inactive</option>
+            <option value="delete">Delete</option>
+        </select>
+    </div>
+    <button class="btn btn-default" type="submit">Apply</button>
+</div>
+    </div> <!-- end class="panel-footer" -->
+</div> <!-- end class="panel" -->
+
+</form>
+{% endblock content %}
+
+
+<!-- BLOCK: templates -->
+{% block templates %}{{super()}} - user_list.html{% endblock %}
+
+{# end web/app/user/templates/user_list.html #}

File web/app/user/templates/user_view.html ADDED

  • Template for route /admin/user/view/<int:id>.
+{% extends "base.html" %}
+
+
+<!-- BLOCK: title -->
+{% block title %}{{super()}} - Admin - View '{{ user.keyname }}'{% endblock %}
+
+
+<!-- BLOCK: breadcrumb -->
+{% block breadcrumb %}
+{{super()}}
+<li><a href="{{ url_for('main.main_page', page='admin') }}">Admin</a></li>
+<li><a href="{{ url_for('.user_list') }}">Users</a></li>
+<li><a href="{{ url_for('.user_view', id=user.id) }}">View '{{ user.keyname }}'</a></li>
+{% endblock %}
+
+
+<!-- BLOCK: content -->
+{% block content %}
+<div id="item_list_panel" class="panel panel-info">
+    <div class="panel-heading">
+<p style="float: right;">
+[ <a href="{{ url_for('.user_view', id=user.id) }}">View User</a> ]
+[ <a href="{{ url_for('.user_edit', id=user.id) }}">Edit User</a> ]
+</p>
+        <h3 class="condensed">View User</h3>
+    </div>
+    <div class="panel-body">
+
+<div class="table-responsive">
+<table class="table table-condensed table-hover">
+<tr>
+    <th>column</th>
+    <th>value</th>
+</tr>
+
+{% for col in cols %}
+    <tr>
+    <td>{{ col }}</td>
+    <td>{{ user[col] }}</td>
+    </tr>
+{% endfor %}
+
+</table>
+</div>
+
+    </div> <!-- end class="panel-body" -->
+</div> <!-- end class="panel" -->
+{% endblock %}
+
+
+<!-- BLOCK: templates -->
+{% block templates %}{{super()}} - user_view.html{% endblock %}
+
+{# end web/app/user/templates/user_view.html #}

File web/app/user/views.py ADDED

  • Create general user page route /user/<page>/.
  • Create route /admin/user/action for use by /admin/user/list.
  • Create route /admin/user/delete/<int:id>.
  • Create route /admin/user/create/.
  • Create route /admin/user/edit/<int:id>.
  • Create route /admin/user/view/<int:id>.
  • Create route /admin/user/list.
+import logging
+import math
+
+from flask import flash, redirect, render_template, request, session, url_for
+from jinja2 import TemplateNotFound
+from .. import db, flash_errors
+from . import user
+from .models import UserModel
+from .forms import CreatUserForm, EditUserForm
+from ..decorators import get_list_opts
+
+
+@user.route('/user/', defaults={'page': 'index'})
+@user.route('/user/<page>/')
+def user_page(page):
+    try:
+        logging.debug( 'user_page( page:%s )' % (page) )
+        return render_template('user_%s.html' % (page))
+    except TemplateNotFound:
+        logging.info('TemplateNotFound: user_%s.html' % (page))
+        abort(404)
+
+
+@user.route('/admin/user/action', methods=['POST'])
+def user_action():
+    action   = request.values.get('action', '')
+    user_ids = request.form.getlist('user_id')
+    id_str = "["+",".join([str(id) for id in user_ids])+"]"
+
+    if action and user_ids:
+        if action == 'delete':
+            for id in user_ids:
+                user = UserModel.query.get_or_404(id)
+                db.session.delete(user)
+            db.session.commit()
+            flash('Users Deleted (id='+id_str+')')
+        if action == 'active':
+            for id in user_ids:
+                user = UserModel.query.get_or_404(id)
+                user.active = True
+                db.session.add(user)
+            db.session.commit()
+            flash('Users Activated (id='+id_str+')')
+        if action == 'inactive':
+            for id in user_ids:
+                user = UserModel.query.get_or_404(id)
+                user.active = False
+                db.session.add(user)
+            db.session.commit()
+            flash('Users Deactivated (id='+id_str+')')
+    logging.info('user_action - action:%s, user_ids:%s' % (action, id_str))
+    return redirect(url_for('.user_list'))
+
+
+@user.route('/admin/user/delete/<int:id>', methods=['GET','POST'])
+def user_delete( id ):
+    user = UserModel.query.get_or_404(id)
+    db.session.delete(user)
+    db.session.commit()
+    flash('User deleted (id=%s)' % (user.id))
+    logging.info('user_delete( id:%s )' % (user.id))
+    return redirect(url_for('.user_list'))
+
+
+@user.route('/admin/user/create', methods=['GET','POST'])
+def user_create():
+    user = UserModel()
+    form = CreatUserForm(user)
+    if form.validate_on_submit():
+        form.populate_obj(user)
+        db.session.add(user)
+        db.session.commit()
+        flash('User created (id=%s)' % (user.id))
+        logging.info('user_create( id:%s )' % (user.id))
+        return redirect(url_for('.user_view', id=user.id))
+    else:
+        flash_errors(form)
+    if request.method == 'GET':
+        user.keyname = ''
+        form.process(obj=user)
+    return render_template('user_create.html', form=form)
+
+
+@user.route('/admin/user/edit/<int:id>', methods=['GET','POST'])
+def user_edit( id ):
+    user = UserModel.query.get_or_404(id)
+    form = EditUserForm(user)
+    if form.validate_on_submit():
+        form.populate_obj(user)
+        db.session.add(user)
+        db.session.commit()
+        flash('User updated (id=%s)' % (user.id))
+        logging.info('user_edit( id:%s )' % (user.id))
+        return redirect(url_for('.user_view', id=user.id))
+    else:
+        flash_errors(form)
+    form.process(obj=user)
+    return render_template('user_edit.html', form=form)
+
+
+@user.route('/admin/user/view/<int:id>')
+def user_view( id ):
+    user = UserModel.query.get_or_404(id)
+    cols = UserModel.__table__.columns.keys()
+    return render_template('user_view.html', cols=cols, user=user)
+
+
+@user.route('/admin/user/list', methods=['GET','POST'])
+@get_list_opts('user_list_opts')
+def user_list():
+    cols = UserModel.__table__.columns.keys()
+    rows = db.session.query(UserModel)
+
+    opts_key = 'user_list_opts'
+    S = session[opts_key]
+
+    if S['status'] in ['active', 'inactive']:
+        rows = rows.filter(UserModel.active == (S['status'] == 'active'))
+
+    S['itemcnt'] = rows.count()
+    S['pagecnt'] = int(math.ceil( float(S['itemcnt'])/float(S['limit']) ))
+
+    if S['page'] > S['pagecnt']:
+        S['page'] = S['pagecnt']
+    S['offset'] = 0
+    if ((S['page'] - 1) * S['limit']) < S['itemcnt']:
+        S['offset'] = (S['page'] - 1) * S['limit']
+    session[opts_key] = S
+
+    if S['sort'] in cols:
+        if S['order'] == 'desc':
+            rows = rows.order_by(getattr( UserModel, S['sort'] ).desc())
+        else:
+            rows = rows.order_by(getattr( UserModel, S['sort'] ).asc())
+    if S['offset'] > 0:
+        rows = rows.offset(S['offset'])
+    if S['limit'] > 0:
+        rows = rows.limit(S['limit'])
+
+    rows = rows.all()
+    rowcnt = len(rows)
+
+    logging.debug('user_list - %s' % (rowcnt))
+    return render_template('user_list.html', cols=cols,rows=rows,rowcnt=rowcnt,opts_key=opts_key)

File web/flaskapp.py MODIFIED

  • Update test route /hello_db.
     #stmt = "SELECT CONNECTION_ID()"  # Return the connection ID (thread ID) for the connection
     #stmt = "SELECT CURRENT_USER()"   # The authenticated user name and host name
     #stmt = "SELECT DATABASE()"       # Return the default (current) database name
-    #stmt = "SHOW TABLES"             # Return list of non-temporary tables in current database
+    stmt = "SHOW TABLES"             # Return list of non-temporary tables in current database
 
     #stmt = "show create database %s;" % current_app.config['MYSQL_DB']
     #stmt = "show grants for %s;" % current_app.config['MYSQL_USER']
 
-    #stmt = "select * from item"             # Return list of non-temporary tables in current database
+    #stmt = "select * from item"
+    #stmt = "select * from user"

Commit-v0.21 | Commit-v0.22 | Commit-v0.23

Clone this wiki locally