├── .dockerignore
├── deploy
├── app
│ ├── templates
│ │ ├── __init__.py
│ │ ├── auth
│ │ │ ├── __init__.py
│ │ │ ├── change_password.html
│ │ │ ├── change_username.html
│ │ │ ├── register.html
│ │ │ ├── reset_password.html
│ │ │ └── login.html
│ │ ├── 404.html
│ │ ├── email
│ │ │ ├── reminder.txt
│ │ │ ├── reminder.html
│ │ │ ├── reset_password.txt
│ │ │ └── reset_password.html
│ │ ├── report
│ │ │ ├── write.html
│ │ │ ├── statistics_department.html
│ │ │ ├── read.html
│ │ │ ├── statistics_crew.html
│ │ │ ├── read_department.html
│ │ │ └── read_crew.html
│ │ ├── admin
│ │ │ ├── base.html
│ │ │ └── model
│ │ │ │ └── report_list_template.html
│ │ └── base.html
│ ├── __init__.pyc
│ ├── static
│ │ ├── favicon.ico
│ │ ├── wangEditor
│ │ │ ├── fonts
│ │ │ │ ├── icomoon.eot
│ │ │ │ ├── icomoon.ttf
│ │ │ │ └── icomoon.woff
│ │ │ └── css
│ │ │ │ └── wangEditor.min.css
│ │ ├── js
│ │ │ ├── html5shiv.min.js
│ │ │ └── respond.min.js
│ │ └── css
│ │ │ ├── style.css
│ │ │ └── font-awesome.min.css
│ ├── babel.cfg
│ ├── auth
│ │ ├── __init__.py
│ │ ├── __pycache__
│ │ │ ├── forms.cpython-36.pyc
│ │ │ ├── views.cpython-36.pyc
│ │ │ └── __init__.cpython-36.pyc
│ │ ├── forms.py
│ │ └── views.py
│ ├── report
│ │ ├── __init__.py
│ │ ├── __pycache__
│ │ │ ├── forms.cpython-36.pyc
│ │ │ ├── views.cpython-36.pyc
│ │ │ └── __init__.cpython-36.pyc
│ │ ├── forms.py
│ │ └── views.py
│ ├── main
│ │ ├── __init__.py
│ │ ├── __pycache__
│ │ │ ├── errors.cpython-36.pyc
│ │ │ ├── views.cpython-36.pyc
│ │ │ └── __init__.cpython-36.pyc
│ │ ├── errors.py
│ │ └── views.py
│ ├── __pycache__
│ │ ├── email.cpython-36.pyc
│ │ ├── models.cpython-36.pyc
│ │ ├── utils.cpython-36.pyc
│ │ ├── __init__.cpython-36.pyc
│ │ └── json_encoder.cpython-36.pyc
│ ├── translations
│ │ └── zh_Hans_CN
│ │ │ └── LC_MESSAGES
│ │ │ ├── messages.mo
│ │ │ └── messages.po
│ ├── json_encoder.py
│ ├── email.py
│ ├── __init__.py
│ ├── utils.py
│ ├── messages.pot
│ └── models.py
├── logs
│ └── .gitignore
├── migrations
│ ├── README
│ ├── __pycache__
│ │ └── env.cpython-36.pyc
│ ├── versions
│ │ ├── __pycache__
│ │ │ └── 4e32e2d01c28_.cpython-36.pyc
│ │ └── 4e32e2d01c28_.py
│ ├── script.py.mako
│ ├── alembic.ini
│ └── env.py
├── __pycache__
│ └── wsgi.cpython-36.pyc
├── postgres
│ └── README.md
├── wsgi.py
└── config.py
├── supervisord.conf
├── gunicorn.conf
├── .gitignore
├── requirements.txt
├── entrypoint.sh
├── docker-compose.yml
├── LICENSE
├── README.md
├── Dockerfile
└── checkdb.py
/.dockerignore:
--------------------------------------------------------------------------------
1 | pg_data
2 |
--------------------------------------------------------------------------------
/deploy/app/templates/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/deploy/app/templates/auth/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/deploy/logs/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/supervisord.conf:
--------------------------------------------------------------------------------
1 | [supervisord]
2 | nodaemon=true
3 |
--------------------------------------------------------------------------------
/deploy/migrations/README:
--------------------------------------------------------------------------------
1 | Generic single-database configuration.
--------------------------------------------------------------------------------
/deploy/app/__init__.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodingCrush/WeeklyReport/HEAD/deploy/app/__init__.pyc
--------------------------------------------------------------------------------
/deploy/app/static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodingCrush/WeeklyReport/HEAD/deploy/app/static/favicon.ico
--------------------------------------------------------------------------------
/gunicorn.conf:
--------------------------------------------------------------------------------
1 | [program:gunicorn]
2 | command=gunicorn --workers=3 wsgi:app -b 0.0.0.0:5000
3 | directory= /deploy
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | mongo_data/
3 | pg96_data/
4 | *.log
5 | mongo/
6 | *.sqlite
7 | upload/
8 | wr_prd.sqlite
9 | *.pyc
--------------------------------------------------------------------------------
/deploy/app/babel.cfg:
--------------------------------------------------------------------------------
1 | [python: **.py]
2 | [jinja2: **/templates/**.html]
3 | extensions=jinja2.ext.autoescape,jinja2.ext.with_
--------------------------------------------------------------------------------
/deploy/app/auth/__init__.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint
2 |
3 | auth = Blueprint('auth', __name__)
4 |
5 | from . import views
6 |
--------------------------------------------------------------------------------
/deploy/__pycache__/wsgi.cpython-36.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodingCrush/WeeklyReport/HEAD/deploy/__pycache__/wsgi.cpython-36.pyc
--------------------------------------------------------------------------------
/deploy/app/report/__init__.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint
2 |
3 | report = Blueprint('report', __name__)
4 |
5 | from . import views
6 |
--------------------------------------------------------------------------------
/deploy/app/main/__init__.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint
2 |
3 | main = Blueprint('main', __name__)
4 |
5 | from . import views, errors
6 |
--------------------------------------------------------------------------------
/deploy/app/__pycache__/email.cpython-36.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodingCrush/WeeklyReport/HEAD/deploy/app/__pycache__/email.cpython-36.pyc
--------------------------------------------------------------------------------
/deploy/app/__pycache__/models.cpython-36.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodingCrush/WeeklyReport/HEAD/deploy/app/__pycache__/models.cpython-36.pyc
--------------------------------------------------------------------------------
/deploy/app/__pycache__/utils.cpython-36.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodingCrush/WeeklyReport/HEAD/deploy/app/__pycache__/utils.cpython-36.pyc
--------------------------------------------------------------------------------
/deploy/app/__pycache__/__init__.cpython-36.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodingCrush/WeeklyReport/HEAD/deploy/app/__pycache__/__init__.cpython-36.pyc
--------------------------------------------------------------------------------
/deploy/app/static/wangEditor/fonts/icomoon.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodingCrush/WeeklyReport/HEAD/deploy/app/static/wangEditor/fonts/icomoon.eot
--------------------------------------------------------------------------------
/deploy/app/static/wangEditor/fonts/icomoon.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodingCrush/WeeklyReport/HEAD/deploy/app/static/wangEditor/fonts/icomoon.ttf
--------------------------------------------------------------------------------
/deploy/app/auth/__pycache__/forms.cpython-36.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodingCrush/WeeklyReport/HEAD/deploy/app/auth/__pycache__/forms.cpython-36.pyc
--------------------------------------------------------------------------------
/deploy/app/auth/__pycache__/views.cpython-36.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodingCrush/WeeklyReport/HEAD/deploy/app/auth/__pycache__/views.cpython-36.pyc
--------------------------------------------------------------------------------
/deploy/app/main/__pycache__/errors.cpython-36.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodingCrush/WeeklyReport/HEAD/deploy/app/main/__pycache__/errors.cpython-36.pyc
--------------------------------------------------------------------------------
/deploy/app/main/__pycache__/views.cpython-36.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodingCrush/WeeklyReport/HEAD/deploy/app/main/__pycache__/views.cpython-36.pyc
--------------------------------------------------------------------------------
/deploy/app/static/wangEditor/fonts/icomoon.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodingCrush/WeeklyReport/HEAD/deploy/app/static/wangEditor/fonts/icomoon.woff
--------------------------------------------------------------------------------
/deploy/migrations/__pycache__/env.cpython-36.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodingCrush/WeeklyReport/HEAD/deploy/migrations/__pycache__/env.cpython-36.pyc
--------------------------------------------------------------------------------
/deploy/app/__pycache__/json_encoder.cpython-36.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodingCrush/WeeklyReport/HEAD/deploy/app/__pycache__/json_encoder.cpython-36.pyc
--------------------------------------------------------------------------------
/deploy/app/auth/__pycache__/__init__.cpython-36.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodingCrush/WeeklyReport/HEAD/deploy/app/auth/__pycache__/__init__.cpython-36.pyc
--------------------------------------------------------------------------------
/deploy/app/main/__pycache__/__init__.cpython-36.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodingCrush/WeeklyReport/HEAD/deploy/app/main/__pycache__/__init__.cpython-36.pyc
--------------------------------------------------------------------------------
/deploy/app/report/__pycache__/forms.cpython-36.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodingCrush/WeeklyReport/HEAD/deploy/app/report/__pycache__/forms.cpython-36.pyc
--------------------------------------------------------------------------------
/deploy/app/report/__pycache__/views.cpython-36.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodingCrush/WeeklyReport/HEAD/deploy/app/report/__pycache__/views.cpython-36.pyc
--------------------------------------------------------------------------------
/deploy/app/report/__pycache__/__init__.cpython-36.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodingCrush/WeeklyReport/HEAD/deploy/app/report/__pycache__/__init__.cpython-36.pyc
--------------------------------------------------------------------------------
/deploy/app/translations/zh_Hans_CN/LC_MESSAGES/messages.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodingCrush/WeeklyReport/HEAD/deploy/app/translations/zh_Hans_CN/LC_MESSAGES/messages.mo
--------------------------------------------------------------------------------
/deploy/migrations/versions/__pycache__/4e32e2d01c28_.cpython-36.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodingCrush/WeeklyReport/HEAD/deploy/migrations/versions/__pycache__/4e32e2d01c28_.cpython-36.pyc
--------------------------------------------------------------------------------
/deploy/app/templates/404.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 |
4 | {% block page_content %}
5 |
6 |
{{_('URL is not available')}}
7 |
8 | {% endblock %}
9 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | flask
2 | flask_bootstrap
3 | flask_login
4 | flask_script
5 | flask_wtf
6 | Flask-Mail
7 | flask-admin
8 | gunicorn
9 | chartkick
10 | flask_sqlalchemy
11 | flask_migrate
12 | flask_babelex
13 | psycopg2
--------------------------------------------------------------------------------
/deploy/app/templates/email/reminder.txt:
--------------------------------------------------------------------------------
1 | Hello, Everyone!
2 | Please submit the report of week{{ week_count }} (From:{{start_at}} To:{{end_at}})
3 |
4 |
5 | By HR:{{user.email}}
6 | Note: replies to this email address are not monitored.
7 |
--------------------------------------------------------------------------------
/deploy/app/templates/email/reminder.html:
--------------------------------------------------------------------------------
1 | Hello, Everyone!
2 | Please submit the report of week{{ week_count }} (From:{{start_at}} To:{{end_at}})
3 |
4 |
5 | By HR:{{user.email}}
6 | Note: replies to this email address are not monitored.
7 |
--------------------------------------------------------------------------------
/deploy/app/templates/auth/change_password.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 |
3 | {% import "bootstrap/wtf.html" as wtf %}
4 |
5 | {% block page_content %}
6 |
7 |
8 | {{_("Change Password")}}
9 | {{ wtf.quick_form(form) }}
10 |
11 |
12 | {% endblock %}
--------------------------------------------------------------------------------
/deploy/app/templates/auth/change_username.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 |
3 | {% import "bootstrap/wtf.html" as wtf %}
4 |
5 | {% block page_content %}
6 |
7 |
8 | {{_("Change Username")}}
9 | {{ wtf.quick_form(form) }}
10 |
11 |
12 | {% endblock %}
--------------------------------------------------------------------------------
/deploy/app/json_encoder.py:
--------------------------------------------------------------------------------
1 | from flask._compat import text_type
2 | from flask.json import JSONEncoder as BaseEncoder
3 | from speaklater import _LazyString
4 |
5 |
6 | class JSONEncoder(BaseEncoder):
7 | def default(self, o):
8 | if isinstance(o, _LazyString):
9 | return text_type(o)
10 |
11 | return BaseEncoder.default(self, o)
12 |
--------------------------------------------------------------------------------
/deploy/app/templates/email/reset_password.txt:
--------------------------------------------------------------------------------
1 | Dear {{ user.username }},
2 |
3 | To reset your password click on the following link:
4 |
5 | {{ url_for('auth.password_reset', token=token, _external=True) }}
6 |
7 | If you have not requested a password reset simply ignore this message.
8 |
9 | WeeklyReport
10 | Note: replies to this email address are not monitored.
11 |
--------------------------------------------------------------------------------
/deploy/app/templates/auth/register.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 |
3 | {% import "bootstrap/wtf.html" as wtf %}
4 |
5 | {% block page_content %}
6 |
7 |
8 |
9 | {{_("Register")}}
10 |
11 | {{ wtf.quick_form(form) }}
12 |
13 |
14 |
15 |
16 | {% endblock %}
--------------------------------------------------------------------------------
/deploy/app/templates/auth/reset_password.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% import "bootstrap/wtf.html" as wtf %}
3 |
4 |
5 | {% block page_content %}
6 |
7 |
8 |
9 | {{_("Reset Password")}}
10 |
11 | {{ wtf.quick_form(form) }}
12 |
13 |
14 |
15 |
16 | {% endblock %}
--------------------------------------------------------------------------------
/deploy/postgres/README.md:
--------------------------------------------------------------------------------
1 | ## DEPLOYMENT
2 |
3 | 1. docker pull postgres:9.6.5
4 | 2. docker run -d \
5 | --restart=unless-stopped \
6 | --name wr-postgres \
7 | -p 5432:5432 \
8 | -v /etc/localtime:/etc/localtime:ro \
9 | -e "LANG=en_US.UTF-8" \
10 | -v $PWD/pg96_data:/var/lib/postgresql/data \
11 | -e POSTGRES_DB=wr_prd \
12 | -e POSTGRES_USER=postgres \
13 | -e POSTGRES_PASSWORD=postgres \
14 | postgres:9.6.5
15 |
--------------------------------------------------------------------------------
/deploy/app/templates/email/reset_password.html:
--------------------------------------------------------------------------------
1 | Dear {{ user.username }},
2 | To reset your password click here.
3 | Alternatively, you can paste the following link in your browser's address bar:
4 | {{ url_for('auth.password_reset', token=token, _external=True) }}
5 | If you have not requested a password reset simply ignore this message.
6 |
7 | WeeklyReport
8 | Note: replies to this email address are not monitored.
9 |
--------------------------------------------------------------------------------
/deploy/migrations/script.py.mako:
--------------------------------------------------------------------------------
1 | """${message}
2 |
3 | Revision ID: ${up_revision}
4 | Revises: ${down_revision | comma,n}
5 | Create Date: ${create_date}
6 |
7 | """
8 | from alembic import op
9 | import sqlalchemy as sa
10 | ${imports if imports else ""}
11 |
12 | # revision identifiers, used by Alembic.
13 | revision = ${repr(up_revision)}
14 | down_revision = ${repr(down_revision)}
15 | branch_labels = ${repr(branch_labels)}
16 | depends_on = ${repr(depends_on)}
17 |
18 |
19 | def upgrade():
20 | ${upgrades if upgrades else "pass"}
21 |
22 |
23 | def downgrade():
24 | ${downgrades if downgrades else "pass"}
25 |
--------------------------------------------------------------------------------
/entrypoint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -e
3 |
4 |
5 | #if [ -z "$TAIGA_SKIP_DB_CHECK" ]; then
6 | DB_CHECK_STATUS=$(python3.6 /scripts/checkdb.py)
7 |
8 |
9 | if [[ $DB_CHECK_STATUS == "missing_flask_users" ]]; then
10 | # Setup database automatically if needed
11 | echo "Configuring initial database"
12 | python3.6 wsgi.py deploy
13 | fi
14 | #fi
15 |
16 | # # Look for static folder, if it does not exist, then generate it
17 | # if [ "$(ls -A /home/taiga/static 2> /dev/null)" == "" ]; then
18 | # python /home/taiga//taiga-back/manage.py collectstatic --noinput
19 | # fi
20 |
21 | gunicorn --workers=3 wsgi:app -b 0.0.0.0:5000
22 |
23 | exec "$@"
24 |
--------------------------------------------------------------------------------
/deploy/app/main/errors.py:
--------------------------------------------------------------------------------
1 | #coding:utf-8
2 | from flask import render_template, current_app, request, redirect, url_for
3 | from flask_login import current_user
4 | from . import main
5 |
6 |
7 | @main.app_errorhandler(403)
8 | def forbidden(e):
9 |
10 | current_app.logger.error(
11 | '403 forbidden at {} by {} '.format(request.url, current_user.email))
12 |
13 | return redirect(url_for('main.index'))
14 |
15 |
16 | @main.app_errorhandler(404)
17 | def page_not_found(e):
18 |
19 | current_app.logger.error(
20 | '404 not found at {} by {} '.format(request.url, current_user.email))
21 |
22 | return render_template('404.html'), 404
23 |
--------------------------------------------------------------------------------
/deploy/app/templates/auth/login.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 | {% import "bootstrap/wtf.html" as wtf %}
3 |
4 |
5 | {% block page_content %}
6 |
7 |
22 |
23 | {% endblock %}
--------------------------------------------------------------------------------
/deploy/app/email.py:
--------------------------------------------------------------------------------
1 | #coding:utf-8
2 | from threading import Thread
3 | from flask import current_app, render_template
4 | from flask_mail import Message
5 | from . import mail
6 |
7 |
8 | def send_async_email(app, msg):
9 | with app.app_context():
10 | mail.send(msg)
11 |
12 |
13 | def send_email(to, subject, template, **kwargs):
14 | app = current_app._get_current_object()
15 | msg = Message(app.config['WR_MAIL_SUBJECT_PREFIX'] + ' ' + subject,
16 | sender=app.config['WR_MAIL_SENDER'],
17 | recipients=list(set(to)))
18 | msg.body = render_template(template + '.txt', **kwargs)
19 | msg.html = render_template(template + '.html', **kwargs)
20 | thr = Thread(target=send_async_email, args=[app, msg])
21 | thr.start()
22 | return thr
23 |
--------------------------------------------------------------------------------
/deploy/migrations/alembic.ini:
--------------------------------------------------------------------------------
1 | # A generic, single database configuration.
2 |
3 | [alembic]
4 | # template used to generate migration files
5 | # file_template = %%(rev)s_%%(slug)s
6 |
7 | # set to 'true' to run the environment during
8 | # the 'revision' command, regardless of autogenerate
9 | # revision_environment = false
10 |
11 |
12 | # Logging configuration
13 | [loggers]
14 | keys = root,sqlalchemy,alembic
15 |
16 | [handlers]
17 | keys = console
18 |
19 | [formatters]
20 | keys = generic
21 |
22 | [logger_root]
23 | level = WARN
24 | handlers = console
25 | qualname =
26 |
27 | [logger_sqlalchemy]
28 | level = WARN
29 | handlers =
30 | qualname = sqlalchemy.engine
31 |
32 | [logger_alembic]
33 | level = INFO
34 | handlers =
35 | qualname = alembic
36 |
37 | [handler_console]
38 | class = StreamHandler
39 | args = (sys.stderr,)
40 | level = NOTSET
41 | formatter = generic
42 |
43 | [formatter_generic]
44 | format = %(levelname)-5.5s [%(name)s] %(message)s
45 | datefmt = %H:%M:%S
46 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.3'
2 | services:
3 |
4 | db:
5 | image: postgres
6 | environment:
7 | - POSTGRES_DB=wr_prd
8 | - POSTGRES_USER=postgres
9 | - POSTGRES_PASSWORD=postgres
10 | volumes:
11 | - ./pg_data:/var/lib/postgresql/data
12 |
13 | adminer:
14 | image: adminer
15 | restart: always
16 | ports:
17 | - 8080:8080
18 |
19 | api:
20 | build: .
21 | #restart: always
22 | stdin_open: true
23 | tty: true
24 | volumes:
25 | - ./deploy:/deploy
26 | - /etc/localtime:/etc/localtime:ro
27 | # - ./gunicorn.conf:/etc/supervisor/conf.d/gunicorn.conf
28 | # - ./supervisord.conf:/etc/supervisor/conf.d/supervisord.conf
29 | depends_on:
30 | - db
31 | ports:
32 | - 8000:5000
33 |
34 | #command: '/bin/bash'
35 | #command: ['python3.6', ' wsgi.py', 'deploy']
36 | #command: '/usr/bin/supervisord'
37 | #command: ['gunicorn','--workers=3', 'wsgi:app', '-b 0.0.0.0:5000']
38 |
--------------------------------------------------------------------------------
/deploy/app/report/forms.py:
--------------------------------------------------------------------------------
1 | #coding:utf-8
2 | from flask_babelex import lazy_gettext as _
3 | from flask_wtf import FlaskForm
4 | from wtforms.validators import DataRequired
5 | from wtforms import SubmitField, TextAreaField, SelectField, HiddenField
6 | from wtforms.fields.html5 import DateField
7 |
8 |
9 | class WriteForm(FlaskForm):
10 | body = TextAreaField(_("This week's work content and plan of next week"),
11 | validators=[DataRequired()])
12 | last_content = HiddenField(_("This week's work content and plan of last week"))
13 | submit = SubmitField(_('Submit'))
14 |
15 |
16 | class ReadDepartmentForm(FlaskForm):
17 | user = SelectField(_('Username'))
18 | start_at = DateField(_('Start'), format='%Y-%m-%d')
19 | end_at = DateField(_('End'), format='%Y-%m-%d')
20 | submit = SubmitField(_('Query'))
21 |
22 |
23 | class ReadCrewForm(ReadDepartmentForm):
24 | department = SelectField(_('Department'))
25 |
26 |
27 | class EmailReminderForm(FlaskForm):
28 | submit = SubmitField(_('Send Reminder Email'))
29 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 CodingCrush
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/deploy/wsgi.py:
--------------------------------------------------------------------------------
1 | #coding:utf-8
2 | from app import create_app, db
3 | from app.models import Role, Department, Report
4 | from flask_migrate import Migrate, MigrateCommand
5 | from flask_script import Manager, Shell
6 | import os
7 | import sys
8 | reload(sys)
9 | sys.setdefaultencoding('utf8')
10 |
11 |
12 | config_file = os.path.join(
13 | os.path.dirname(os.path.realpath(__file__)), 'config.py')
14 |
15 | app = create_app(config_file)
16 |
17 |
18 | if __name__ == '__main__':
19 | manager = Manager(app)
20 | migrate = Migrate(app, db)
21 |
22 |
23 | def make_shell_context():
24 | return dict(app=app, db=db, Role=Role,
25 | Department=Department, Report=Report)
26 |
27 |
28 | manager.add_command("shell", Shell(make_context=make_shell_context))
29 | manager.add_command('db', MigrateCommand)
30 |
31 |
32 | @manager.command
33 | def profile(length=25, profile_dir=None):
34 | """Start the application under the code profiler."""
35 | from werkzeug.contrib.profiler import ProfilerMiddleware
36 | app.wsgi_app = ProfilerMiddleware(
37 | app.wsgi_app, restrictions=[length], profile_dir=profile_dir)
38 | app.run()
39 |
40 |
41 | @manager.command
42 | def deploy():
43 | db.create_all()
44 | Role.insert_roles()
45 | Department.delete_departments()
46 | Department.insert_departments()
47 |
48 | manager.run()
49 |
--------------------------------------------------------------------------------
/deploy/config.py:
--------------------------------------------------------------------------------
1 | #coding:utf-8
2 | import os
3 |
4 |
5 | base_dir = os.path.dirname(os.path.realpath(__file__))
6 |
7 | DEBUG = True
8 |
9 | SECRET_KEY = os.environ.get('SECRET_KEY') or 'nobody knows the password'
10 | PER_PAGE = 10
11 |
12 | SQLALCHEMY_COMMIT_ON_TEARDOWN = True
13 | SQLALCHEMY_TRACK_MODIFICATIONS = False
14 | SQLALCHEMY_RECORD_QUERIES = True
15 |
16 | IMAGE_UPLOAD_DIR = 'static/upload/'
17 | UPLOAD_FOLDER = os.path.join(base_dir, 'app/static/upload/')
18 |
19 | #MAIL_SERVER = 'smtp.163.com'
20 | MAIL_SERVER = ''
21 | MAIL_PORT = 465
22 | MAIL_USE_SSL = True
23 | MAIL_USERNAME = ''
24 | MAIL_PASSWORD = ''
25 |
26 | WR_MAIL_SUBJECT_PREFIX = '[WeeklyReport]'
27 | WR_MAIL_SENDER = 'WeeklyReport '
28 |
29 |
30 | DEPARTMENTS = (
31 | '人事行政部',
32 | '软件测试部',
33 | '产品开发部',
34 | '新技术研发部'
35 | )
36 |
37 | DEFAULT_CONTENT = "1、上周计划完成情况:
" \
38 | " 2、计划外工作(包含协助运维工作):
" \
39 | " 3、重要问题:
" \
40 | " 4、持续未处理解决的事情:
" \
41 | " 5、下周计划:
"
42 |
43 |
44 | #SQLALCHEMY_DATABASE_URI = 'postgresql://postgres:postgres@db/wr_prd'
45 | SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(base_dir, 'wr_prd.sqlite')
46 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | demo: https://wr.mcloud.fun:81
2 |
3 | #加入docker-compose
4 |
5 | 可以在新机器上, 直接一键启动了:
6 | docker-compose up
7 |
8 | 加入entrypoint.sh脚本:
9 | 1 启动时先等待pg启动
10 | 2 判断pg里是否已经有表
11 | 3 如果没有表, 初始化表
12 | 4 用gunicorn 启动 app
13 |
14 | ## 快速运行
15 | `-w `为开启的gunicorn worker进程数
16 | `-p 8000:80` 主机通过8000端口访问
17 |
18 | ```bash
19 | git clone https://github.com/CodingCrush/WeeklyReport && \
20 |
21 | cd WeeklyReport && \
22 |
23 | docker build -t weeklyreport:0.2 . && \
24 |
25 | docker run -d \
26 | --restart=unless-stopped \
27 | --name weeklyreport-server \
28 | -p 8000:80 \
29 | -v /etc/localtime:/etc/localtime:ro \
30 | -v $PWD:/opt/weeklyreport \
31 | weeklyreport:0.2 \
32 | gunicorn wsgi:app --bind 0.0.0.0:80 -w 2 --log-file logs/awsgi.log --log-level=DEBUG
33 | ```
34 |
35 | ## 更新说明
36 | V0.2: 简化了部署步骤
37 |
38 | ## 配置说明
39 |
40 | + 配置数据库
41 | 数据库默认使用sqlite,也可以使用postgres container,cd到postgres目录下,pull镜像,启动。
42 | 数据库URI地址由数据库名、用户名、密码、主机、端口号组成。
43 | ```
44 | SQLALCHEMY_DATABASE_URI = 'postgresql://postgres:postgres@localhost/wr_prd'
45 | ```
46 | 步骤见postgres目录下的readme.md
47 |
48 | + 配置config.py
49 |
50 | `DEPARTMENTS`: 这个元组为部门列表,第一次打开时自动初始化到数据库中,用户在注册时可以选择部门。
51 |
52 | `MAIL_USERNAME` : 用来发送邮件通知的邮箱账号
53 |
54 | `MAIL_PASSWORD` : 用来发送邮件通知的邮箱密码
55 |
56 |
57 | ## 后台管理
58 |
59 | 第一次注册的用户为超级管理员,永远有登录后台的权限。
60 | 管理员可以修改其他角色
61 |
62 | 默认用户角色为`EMPLOYEE`,仅具有读写自己的周报的权限,
63 |
64 | `MANAGER`可以读写周报,并查看本部门所有周报。而HR可以读写周报,并查看全部门所有周报。
65 |
66 | `ADMINISTRATOR`在HR基础上增加了进入后台的功能。
67 |
68 | `QUIT`用来标识离职后的员工,禁止其登录。
69 |
--------------------------------------------------------------------------------
/deploy/app/templates/report/write.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% import "bootstrap/wtf.html" as wtf %}
3 |
4 |
5 | {% block head_script %}
6 |
7 |
13 | {% endblock %}
14 |
15 |
16 | {% block page_content %}
17 |
18 | 请注意填写周报的日期
19 |
20 | 第{{ week_count }}周({{start_at}}至{{end_at}})工作周报
21 |
33 | {% endblock %}
34 |
35 |
36 |
37 | {% block script %}
38 |
39 |
49 |
50 | {% endblock %}
51 |
--------------------------------------------------------------------------------
/deploy/app/templates/report/statistics_department.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% import "bootstrap/wtf.html" as wtf %}
3 |
4 | {% block head_script %}
5 |
6 |
7 | {% endblock %}
8 |
9 |
10 | {% block page_content %}
11 |
12 |
13 |
14 |
15 |
16 |
17 | 第{{ week_count }}周 ({{start_at}}至{{end_at}}) {{current_user.department.name}} 周报统计
18 | {% if get_this_week_count() == week_count %}
19 |
20 | (查看上周)
21 |
22 | {% endif %}
23 |
24 |
25 | {% pie_chart data %}
26 |
27 |
28 |
29 |
30 |
31 |
32 | {{ current_user.department.name }}
33 |
34 |
35 |
36 |
37 |
已交:
38 | {% for name in names['has_submitted'] %}
39 |
{{ name }}
40 | {% endfor %}
41 |
42 |
未交:
43 | {% for name in names['not_yet'] %}
44 |
{{ name }}
45 | {% endfor %}
46 |
47 |
48 |
49 | {% endblock %}
50 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # image: weeklyreport:0.2
2 | FROM centos:7
3 | MAINTAINER CodingCrush
4 | ENV LANG en_US.UTF-8
5 | # TimeZone: Asia/Shanghai
6 | RUN ln -s -f /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
7 | curl -fsSL https://setup.ius.io/ | sh && \
8 | yum update -y && \
9 | yum install -y python36u python36u-devel python36u-pip gcc postgresql-libs postgresql-devel && \
10 | mkdir ~/.pip && \
11 | echo -e "[global]\nindex-url=http://pypi.douban.com/simple/\ntrusted-host=pypi.douban.com">~/.pip/pip.conf && \
12 | yum clean all
13 |
14 | RUN yum install -y supervisor
15 |
16 | RUN mkdir -p /deploy
17 | WORKDIR /deploy
18 | COPY requirements.txt /deploy/requirements.txt
19 | RUN pip3.6 install -r requirements.txt --timeout=120
20 |
21 | # Setup supervisord
22 | RUN mkdir -p /var/log/supervisor
23 | #COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
24 | #COPY gunicorn.conf /etc/supervisor/conf.d/gunicorn.conf
25 |
26 | # Start wp-server container
27 |
28 | # docker run -d \
29 | # --restart=unless-stopped \
30 | # --name weeklyreport-server \
31 | # -p 8000:80 \
32 | # -v /etc/localtime:/etc/localtime:ro \
33 | # -v $PWD:/opt/weeklyreport \
34 | # weeklyreport:0.2 \
35 | # gunicorn wsgi:app --bind 0.0.0.0:5000 -w 2 --log-file logs/awsgi.log --log-level=DEBUG
36 |
37 | # run sh. Start processes in docker-compose.yml
38 |
39 |
40 | #deploy
41 | COPY deploy /deploy
42 | #wait pg connected
43 | #RUN python3.6 checkdb.py
44 | # db init migrate
45 | #RUN python3.6 wsgi.py deploy
46 |
47 |
48 | RUN mkdir -p /scripts
49 | COPY checkdb.py /scripts/checkdb.py
50 | COPY entrypoint.sh /scripts/entrypoint.sh
51 | #RUN chown -R /scripts
52 | RUN chmod +x /scripts/entrypoint.sh
53 |
54 | CMD ["/scripts/entrypoint.sh"]
55 | #CMD ["/usr/bin/supervisord"]
56 | #CMD ["/bin/bash"]
57 |
--------------------------------------------------------------------------------
/deploy/app/templates/report/read.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% import "bootstrap/wtf.html" as wtf %}
3 |
4 |
5 | {% block page_content %}
6 |
7 |
8 |
9 | {% for report in pagination.items %}
10 |
31 | {% endfor %}
32 |
33 |
46 |
47 | {% endblock %}
48 |
--------------------------------------------------------------------------------
/checkdb.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import time
3 | import psycopg2
4 | import logging
5 |
6 |
7 | logging.basicConfig(format='%(levelname)s: %(message)s', level=logging.INFO)
8 | logging.info("Checking if table 'django_migrations' exists.")
9 | logging.info("If you want to skip this, just set the environment var")
10 | logging.info("TAIGA_SKIP_DB_CHECK=True on docker-compose.yml on service.")
11 | CONNECTION_STRING = "dbname='{}' user='{}' host='{}' password='{}'".format(
12 | 'wr_prd',
13 | 'postgres',
14 | 'db',
15 | 'postgres'
16 | )
17 | LIMIT_RETRIES = 5
18 | SLEEP_INTERVAL = 5
19 |
20 |
21 | def postgres_connection(connection_string, retry_counter=1):
22 | try:
23 | connection = psycopg2.connect(connection_string)
24 | except psycopg2.OperationalError as e:
25 | if retry_counter > LIMIT_RETRIES:
26 | logging.error("CAN'T CONNECT TO POSTGRES")
27 | logging.error("Check your connection settings.")
28 | logging.error("Or increase (in docker-compose.yml):")
29 | logging.error(
30 | "TAIGA_DB_CHECK_SLEEP_INTERVAL / TAIGA_DB_CHECK_LIMIT_RETRIES."
31 | )
32 | logging.error("Exception messsage: {}".format(e))
33 | sys.exit(1)
34 | else:
35 | logging.warning("Can't connect to Postgres. Will try again...")
36 | time.sleep(SLEEP_INTERVAL)
37 | retry_counter += 1
38 | return postgres_connection(connection_string, retry_counter)
39 | return connection
40 |
41 |
42 | cursor = postgres_connection(CONNECTION_STRING).cursor()
43 | cursor.execute(
44 | "select exists(select * from information_schema.tables where table_name=%s)",
45 | ('users',)
46 | )
47 | if not cursor.fetchone()[0]:
48 | logging.info("So, it seems like it's the first time you run the ")
49 | logging.info("service for taiga. Will try to:")
50 | logging.info("1) migrate DB; 2) load initial data; 3) compilemessages")
51 | print('missing_flask_users')
52 |
--------------------------------------------------------------------------------
/deploy/app/__init__.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 | from flask import Flask
3 | from flask_admin import Admin
4 | from flask_babelex import Babel
5 | from flask_bootstrap import Bootstrap
6 | from flask_mail import Mail
7 | from flask_login import LoginManager
8 | from flask_sqlalchemy import SQLAlchemy
9 | from .json_encoder import JSONEncoder
10 | from utils import get_last_week_content, get_week_days
11 |
12 |
13 | bootstrap = Bootstrap()
14 | mail = Mail()
15 | db = SQLAlchemy()
16 |
17 | login_manager = LoginManager()
18 | login_manager.session_protection = "basic"
19 | login_manager.login_view = 'auth.login'
20 |
21 | app = Flask(__name__)
22 |
23 | babel = Babel()
24 |
25 | admin = Admin(app, name='WeeklyReport', template_mode='bootstrap3')
26 |
27 |
28 | @babel.localeselector
29 | def get_locale():
30 | return 'zh_Hans_CN'
31 |
32 |
33 | def create_app(config_file):
34 |
35 | app.config.from_pyfile(config_file)
36 |
37 | mail.init_app(app)
38 | bootstrap.init_app(app)
39 | db.init_app(app)
40 | login_manager.init_app(app)
41 | babel.init_app(app)
42 |
43 | from .main import main as main_blueprint
44 | app.register_blueprint(main_blueprint)
45 |
46 | from .report import report as report_blueprint
47 | app.register_blueprint(report_blueprint, url_prefix='/report')
48 |
49 | from .auth import auth as auth_blueprint
50 | app.register_blueprint(auth_blueprint, url_prefix='/auth')
51 |
52 | # chartkick support
53 | app.jinja_env.add_extension('chartkick.ext.charts')
54 |
55 | # i18n support
56 | app.jinja_env.add_extension('jinja2.ext.i18n')
57 |
58 | # jinja env to help check statistics page under this week
59 | app.jinja_env.globals.update(
60 | get_this_week_count=lambda: datetime.now().isocalendar()[1])
61 |
62 | # lazy_gettext Json Error Fix
63 | app.json_encoder = JSONEncoder
64 |
65 | app.add_template_filter(get_last_week_content, 'get_last_week_content')
66 | app.add_template_filter(get_week_days, 'get_week_days')
67 | return app
68 |
--------------------------------------------------------------------------------
/deploy/app/templates/report/statistics_crew.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% import "bootstrap/wtf.html" as wtf %}
3 |
4 | {% block head_script %}
5 |
6 |
7 | {% endblock %}
8 |
9 |
10 | {% block page_content %}
11 |
12 |
13 |
14 |
15 |
16 | 第{{ week_count }}周 ({{start_at}}至{{end_at}}) 各部门未交周报人数统计
17 | {% if get_this_week_count() == week_count %}
18 |
19 | (查看上周)
20 |
21 | {% endif %}
22 |
23 |
24 | {% for key, value in contrast.iteritems() %}
25 |
{{ key }}:{{value}}
26 |
27 | {% endfor %}
28 |
29 |
30 |
31 |
32 | {% for dept in stash %}
33 |
34 |
35 |
36 | {{ dept.dept_name }}
37 |
38 |
39 |
40 |
41 |
已交:
42 | {% for name in dept['names']['has_submitted'] %}
43 |
{{ name }}
44 | {% endfor %}
45 |
46 |
47 |
48 |
未交:
49 | {% for name in dept['names']['not_yet'] %}
50 |
{{ name }}
51 | {% endfor %}
52 |
53 |
54 |
55 |
56 | {% endfor %}
57 |
58 |
59 |
60 | {#
61 | {{ wtf.quick_form(form) }}
62 | #}
63 |
64 | {% endblock %}
65 |
--------------------------------------------------------------------------------
/deploy/app/templates/report/read_department.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% import "bootstrap/wtf.html" as wtf %}
3 |
4 |
5 | {% block page_content %}
6 |
7 |
16 |
17 |
18 | {% for report in pagination.items %}
19 |
29 | {% endfor %}
30 |
31 |
61 |
62 | {% endblock %}
63 |
--------------------------------------------------------------------------------
/deploy/app/static/js/html5shiv.min.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @preserve HTML5 Shiv 3.7.3 | @afarkas @jdalton @jon_neal @rem | MIT/GPL2 Licensed
3 | */
4 | !function(a,b){function c(a,b){var c=a.createElement("p"),d=a.getElementsByTagName("head")[0]||a.documentElement;return c.innerHTML="x",d.insertBefore(c.lastChild,d.firstChild)}function d(){var a=t.elements;return"string"==typeof a?a.split(" "):a}function e(a,b){var c=t.elements;"string"!=typeof c&&(c=c.join(" ")),"string"!=typeof a&&(a=a.join(" ")),t.elements=c+" "+a,j(b)}function f(a){var b=s[a[q]];return b||(b={},r++,a[q]=r,s[r]=b),b}function g(a,c,d){if(c||(c=b),l)return c.createElement(a);d||(d=f(c));var e;return e=d.cache[a]?d.cache[a].cloneNode():p.test(a)?(d.cache[a]=d.createElem(a)).cloneNode():d.createElem(a),!e.canHaveChildren||o.test(a)||e.tagUrn?e:d.frag.appendChild(e)}function h(a,c){if(a||(a=b),l)return a.createDocumentFragment();c=c||f(a);for(var e=c.frag.cloneNode(),g=0,h=d(),i=h.length;i>g;g++)e.createElement(h[g]);return e}function i(a,b){b.cache||(b.cache={},b.createElem=a.createElement,b.createFrag=a.createDocumentFragment,b.frag=b.createFrag()),a.createElement=function(c){return t.shivMethods?g(c,a,b):b.createElem(c)},a.createDocumentFragment=Function("h,f","return function(){var n=f.cloneNode(),c=n.createElement;h.shivMethods&&("+d().join().replace(/[\w\-:]+/g,function(a){return b.createElem(a),b.frag.createElement(a),'c("'+a+'")'})+");return n}")(t,b.frag)}function j(a){a||(a=b);var d=f(a);return!t.shivCSS||k||d.hasCSS||(d.hasCSS=!!c(a,"article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}mark{background:#FF0;color:#000}template{display:none}")),l||i(a,d),a}var k,l,m="3.7.3",n=a.html5||{},o=/^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i,p=/^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i,q="_html5shiv",r=0,s={};!function(){try{var a=b.createElement("a");a.innerHTML="",k="hidden"in a,l=1==a.childNodes.length||function(){b.createElement("a");var a=b.createDocumentFragment();return"undefined"==typeof a.cloneNode||"undefined"==typeof a.createDocumentFragment||"undefined"==typeof a.createElement}()}catch(c){k=!0,l=!0}}();var t={elements:n.elements||"abbr article aside audio bdi canvas data datalist details dialog figcaption figure footer header hgroup main mark meter nav output picture progress section summary template time video",version:m,shivCSS:n.shivCSS!==!1,supportsUnknownElements:l,shivMethods:n.shivMethods!==!1,type:"default",shivDocument:j,createElement:g,createDocumentFragment:h,addElements:e};a.html5=t,j(b),"object"==typeof module&&module.exports&&(module.exports=t)}("undefined"!=typeof window?window:this,document);
--------------------------------------------------------------------------------
/deploy/app/utils.py:
--------------------------------------------------------------------------------
1 | #coding:utf-8
2 | import datetime
3 | from functools import wraps
4 | from flask import abort
5 | from flask_login import current_user
6 | import re
7 |
8 |
9 | def permission_required(permission):
10 | def decorator(f):
11 | @wraps(f)
12 | def decorated_function(*args, **kwargs):
13 | if not current_user.is_authenticated or \
14 | not current_user.can(permission):
15 | abort(403)
16 | return f(*args, **kwargs)
17 | return decorated_function
18 | return decorator
19 |
20 |
21 | def get_week_count(at=None):
22 | if at:
23 | return at.isocalendar()[1]
24 | else:
25 | return datetime.datetime.now().isocalendar()[1]
26 |
27 |
28 | def get_this_monday():
29 | today = datetime.date.today()
30 | weekday = today.weekday()
31 | return today-datetime.timedelta(weekday)
32 |
33 |
34 | def is_allowed_file(filename):
35 | return '.' in filename and \
36 | filename.rsplit('.', 1)[1] in {'png', 'jpg', 'jpeg', 'gif'}
37 |
38 |
39 | def get_last_week():
40 | return datetime.datetime.now() - datetime.timedelta(days=7)
41 |
42 |
43 | def get_last_week_start_at():
44 | return get_this_monday() - datetime.timedelta(days=7)
45 |
46 |
47 | def get_last_week_end_at():
48 | return get_this_monday()
49 |
50 | def get_last_week_content(last_week_content):
51 | content_index = last_week_content.find("next_week")
52 | if content_index != -1:
53 | return last_week_content[content_index+31:]
54 | return ""
55 |
56 | def clean_html(html):
57 | """
58 | Remove HTML markup from the given string.
59 | :param html: the HTML string to be cleaned
60 | :type html: str
61 | :rtype: str
62 | """
63 |
64 | # First we remove inline JavaScript/CSS:
65 | cleaned = re.sub(r"(?is)<(script|style).*?>.*?(\1>)", "", html.strip())
66 | # Then we remove html comments. This has to be done before removing regular
67 | # tags since comments can contain '>' characters.
68 | cleaned = re.sub(r"(?s)[\n]?", "", cleaned)
69 | # Next we can remove the remaining tags:
70 | cleaned = re.sub(r"(?s)<.*?>", " ", cleaned)
71 | # Finally, we deal with whitespace
72 | cleaned = re.sub(r" ", " ", cleaned)
73 | cleaned = re.sub(r" ", " ", cleaned)
74 | cleaned = re.sub(r" ", " ", cleaned)
75 | return cleaned.strip()
76 |
77 | def get_week_days(year, week, index):
78 | d = datetime.date(year, 1, 1)
79 | if (d.weekday() > 3):
80 | d = d + datetime.timedelta(7-d.weekday())
81 | else:
82 | d = d - datetime.timedelta(d.weekday())
83 | dlt = datetime.timedelta(days = (week - 1) * 7)
84 | return (d + dlt, d + dlt + datetime.timedelta(days=6))[index]
--------------------------------------------------------------------------------
/deploy/app/templates/admin/base.html:
--------------------------------------------------------------------------------
1 | {% import 'admin/layout.html' as layout with context -%}
2 | {% import 'admin/static.html' as admin_static with context %}
3 | {% extends "base.html" %}
4 |
5 |
6 | {% block head_script %}
7 | {% block title %}{% if admin_view.category %}{{ admin_view.category }} - {% endif %}{{ admin_view.name }} - {{ admin_view.admin.name }}{% endblock %}
8 |
9 | {% block head_css %}
10 |
11 |
12 | {% if admin_view.extra_css %}
13 | {% for css_url in admin_view.extra_css %}
14 |
15 | {% endfor %}
16 | {% endif %}
17 |
18 | {% endblock %}
19 | {% block head %}
20 | {% endblock %}
21 | {% block head_tail %}
22 | {% endblock %}
23 |
24 | {% endblock %}
25 |
26 |
27 |
28 |
29 | {% block container %}
30 |
31 | {% block page_body %}
32 |
33 |
61 |
62 | {% block messages %}
63 | {{ layout.messages() }}
64 | {% endblock %}
65 |
66 | {# store the jinja2 context for form_rules rendering logic #}
67 | {% set render_ctx = h.resolve_ctx() %}
68 |
69 | {% block body %}{% endblock %}
70 |
71 | {% endblock %}
72 |
73 | {% block tail_js %}
74 | {% if admin_view.extra_js %}
75 | {% for js_url in admin_view.extra_js %}
76 |
77 | {% endfor %}
78 | {% endif %}
79 | {% endblock %}
80 |
81 | {% block tail %}
82 | {% endblock %}
83 |
84 |
85 | {% endblock %}
86 |
87 |
--------------------------------------------------------------------------------
/deploy/migrations/versions/4e32e2d01c28_.py:
--------------------------------------------------------------------------------
1 | """empty message
2 |
3 | Revision ID: 4e32e2d01c28
4 | Revises:
5 | Create Date: 2017-12-22 12:06:04.886300
6 |
7 | """
8 | from alembic import op
9 | import sqlalchemy as sa
10 |
11 |
12 | # revision identifiers, used by Alembic.
13 | revision = '4e32e2d01c28'
14 | down_revision = None
15 | branch_labels = None
16 | depends_on = None
17 |
18 |
19 | def upgrade():
20 | # ### commands auto generated by Alembic - please adjust! ###
21 | op.create_table('departments',
22 | sa.Column('id', sa.Integer(), nullable=False),
23 | sa.Column('name', sa.String(length=64), nullable=True),
24 | sa.PrimaryKeyConstraint('id'),
25 | sa.UniqueConstraint('name')
26 | )
27 | op.create_table('roles',
28 | sa.Column('id', sa.Integer(), nullable=False),
29 | sa.Column('name', sa.String(length=64), nullable=True),
30 | sa.Column('permissions', sa.Integer(), nullable=True),
31 | sa.PrimaryKeyConstraint('id'),
32 | sa.UniqueConstraint('name')
33 | )
34 | op.create_table('users',
35 | sa.Column('id', sa.Integer(), nullable=False),
36 | sa.Column('email', sa.String(length=64), nullable=True),
37 | sa.Column('username', sa.String(length=64), nullable=True),
38 | sa.Column('password_hash', sa.String(length=128), nullable=True),
39 | sa.Column('is_ignored', sa.Boolean(), nullable=True),
40 | sa.Column('role_id', sa.Integer(), nullable=True),
41 | sa.Column('department_id', sa.Integer(), nullable=True),
42 | sa.Column('is_super_admin', sa.Boolean(), nullable=True),
43 | sa.ForeignKeyConstraint(['department_id'], ['departments.id'], ),
44 | sa.ForeignKeyConstraint(['role_id'], ['roles.id'], ),
45 | sa.PrimaryKeyConstraint('id')
46 | )
47 | op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=True)
48 | op.create_index(op.f('ix_users_username'), 'users', ['username'], unique=True)
49 | op.create_table('reports',
50 | sa.Column('id', sa.Integer(), nullable=False),
51 | sa.Column('created_at', sa.DateTime(), nullable=True),
52 | sa.Column('author_id', sa.Integer(), nullable=True),
53 | sa.Column('content', sa.Text(), nullable=True),
54 | sa.Column('week_count', sa.Integer(), nullable=True),
55 | sa.Column('year', sa.Integer(), nullable=True),
56 | sa.ForeignKeyConstraint(['author_id'], ['users.id'], ),
57 | sa.PrimaryKeyConstraint('id')
58 | )
59 | op.create_index(op.f('ix_reports_created_at'), 'reports', ['created_at'], unique=False)
60 | # ### end Alembic commands ###
61 |
62 |
63 | def downgrade():
64 | # ### commands auto generated by Alembic - please adjust! ###
65 | op.drop_index(op.f('ix_reports_created_at'), table_name='reports')
66 | op.drop_table('reports')
67 | op.drop_index(op.f('ix_users_username'), table_name='users')
68 | op.drop_index(op.f('ix_users_email'), table_name='users')
69 | op.drop_table('users')
70 | op.drop_table('roles')
71 | op.drop_table('departments')
72 | # ### end Alembic commands ###
73 |
--------------------------------------------------------------------------------
/deploy/migrations/env.py:
--------------------------------------------------------------------------------
1 | from __future__ import with_statement
2 | from alembic import context
3 | from sqlalchemy import engine_from_config, pool
4 | from logging.config import fileConfig
5 | import logging
6 |
7 | # this is the Alembic Config object, which provides
8 | # access to the values within the .ini file in use.
9 | config = context.config
10 |
11 | # Interpret the config file for Python logging.
12 | # This line sets up loggers basically.
13 | fileConfig(config.config_file_name)
14 | logger = logging.getLogger('alembic.env')
15 |
16 | # add your model's MetaData object here
17 | # for 'autogenerate' support
18 | # from myapp import mymodel
19 | # target_metadata = mymodel.Base.metadata
20 | from flask import current_app
21 | config.set_main_option('sqlalchemy.url',
22 | current_app.config.get('SQLALCHEMY_DATABASE_URI'))
23 | target_metadata = current_app.extensions['migrate'].db.metadata
24 |
25 | # other values from the config, defined by the needs of env.py,
26 | # can be acquired:
27 | # my_important_option = config.get_main_option("my_important_option")
28 | # ... etc.
29 |
30 |
31 | def run_migrations_offline():
32 | """Run migrations in 'offline' mode.
33 |
34 | This configures the context with just a URL
35 | and not an Engine, though an Engine is acceptable
36 | here as well. By skipping the Engine creation
37 | we don't even need a DBAPI to be available.
38 |
39 | Calls to context.execute() here emit the given string to the
40 | script output.
41 |
42 | """
43 | url = config.get_main_option("sqlalchemy.url")
44 | context.configure(url=url)
45 |
46 | with context.begin_transaction():
47 | context.run_migrations()
48 |
49 |
50 | def run_migrations_online():
51 | """Run migrations in 'online' mode.
52 |
53 | In this scenario we need to create an Engine
54 | and associate a connection with the context.
55 |
56 | """
57 |
58 | # this callback is used to prevent an auto-migration from being generated
59 | # when there are no changes to the schema
60 | # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
61 | def process_revision_directives(context, revision, directives):
62 | if getattr(config.cmd_opts, 'autogenerate', False):
63 | script = directives[0]
64 | if script.upgrade_ops.is_empty():
65 | directives[:] = []
66 | logger.info('No changes in schema detected.')
67 |
68 | engine = engine_from_config(config.get_section(config.config_ini_section),
69 | prefix='sqlalchemy.',
70 | poolclass=pool.NullPool)
71 |
72 | connection = engine.connect()
73 | context.configure(connection=connection,
74 | target_metadata=target_metadata,
75 | process_revision_directives=process_revision_directives,
76 | **current_app.extensions['migrate'].configure_args)
77 |
78 | try:
79 | with context.begin_transaction():
80 | context.run_migrations()
81 | finally:
82 | connection.close()
83 |
84 | if context.is_offline_mode():
85 | run_migrations_offline()
86 | else:
87 | run_migrations_online()
88 |
--------------------------------------------------------------------------------
/deploy/app/templates/report/read_crew.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% import "bootstrap/wtf.html" as wtf %}
3 |
4 |
5 | {% block page_content %}
6 |
7 |
19 |
20 |
21 |
22 | {% for report in pagination.items %}
23 |
58 | {% endfor %}
59 |
60 |
93 |
94 | {% endblock %}
95 |
--------------------------------------------------------------------------------
/deploy/app/auth/forms.py:
--------------------------------------------------------------------------------
1 | #coding:utf-8
2 | from flask_babelex import lazy_gettext as _
3 | from flask_wtf import FlaskForm
4 | from wtforms.fields.html5 import EmailField
5 | from wtforms import BooleanField, SubmitField, \
6 | StringField, PasswordField, SelectField
7 | from wtforms.validators import DataRequired, Length, EqualTo
8 | from wtforms import ValidationError
9 | from ..models import User
10 |
11 |
12 | class LoginForm(FlaskForm):
13 | email = EmailField(_('Email'), validators=[
14 | DataRequired(), Length(2, 64)])
15 | password = PasswordField(_('Password'), validators=[DataRequired()])
16 | remember_me = BooleanField(_('Remember Password'))
17 | submit = SubmitField(_('Submit'))
18 |
19 |
20 | class RegistrationForm(FlaskForm):
21 | email = EmailField(_('Email'), validators=[
22 | DataRequired(), Length(1, 64)])
23 | username = StringField(_('Username'), validators=[
24 | DataRequired(), Length(1, 64)
25 | ])
26 |
27 | password = PasswordField(_('Password'), validators=[
28 | DataRequired(), EqualTo('password2', message=_("Passwords doesn't match"))])
29 | password2 = PasswordField(_('Confirm Password'), validators=[DataRequired()])
30 | department = SelectField(_('Department'))
31 | submit = SubmitField(_('Register'))
32 |
33 | def validate_username(self, field):
34 | if User.query.filter_by(username=field.data).first():
35 | raise ValidationError(_('Username has been used'))
36 |
37 | def validate_email(self, field):
38 | if User.query.filter_by(email=field.data).first():
39 | raise ValidationError(_('Email has been registered'))
40 |
41 |
42 | class ChangePasswordForm(FlaskForm):
43 | old_password = PasswordField(_('Old Password'), validators=[DataRequired()])
44 | password = PasswordField(_('New Password'), validators=[
45 | DataRequired(), EqualTo('password2', message=_("Passwords doesn't match"))])
46 | password2 = PasswordField(_('Confirm New Password'), validators=[DataRequired()])
47 | submit = SubmitField(_('Update Password'))
48 |
49 |
50 | class ChangeUsernameForm(FlaskForm):
51 | password = PasswordField(_('Password'), validators=[
52 | DataRequired()])
53 | username = StringField(_('New Username'), validators=[
54 | DataRequired(), Length(1, 64),
55 | EqualTo('username2', message=_("Usernames doesn't match"))])
56 | username2 = StringField(_('Confirm New Username'), validators=[
57 | DataRequired(), Length(1, 64)])
58 | submit = SubmitField(_('Update Username'))
59 |
60 | def validate_username(self, field):
61 | if User.query.filter_by(username=field.data).first():
62 | raise ValidationError(_('Username has been used'))
63 |
64 |
65 | class PasswordResetRequestForm(FlaskForm):
66 | email = EmailField(_('Email'), validators=[DataRequired(), Length(1, 64)])
67 | submit = SubmitField(_('Reset Password'))
68 |
69 | def validate_email(self, field):
70 | if User.query.filter_by(email=field.data).first() is None:
71 | raise ValidationError(_('Unknown email address'))
72 |
73 |
74 | class PasswordResetForm(FlaskForm):
75 | email = EmailField(_('Email'), validators=[
76 | DataRequired(), Length(1, 64)])
77 | password = PasswordField(_('New Password'), validators=[
78 | DataRequired(), EqualTo('password2', message="Passwords doesn't match")])
79 | password2 = PasswordField(_('Confirm New password'),
80 | validators=[DataRequired()])
81 | submit = SubmitField(_('Reset Password'))
82 |
83 | def validate_email(self, field):
84 | if User.query.filter_by(email=field.data).first() is None:
85 | raise ValidationError(_('Unknown email address'))
86 |
--------------------------------------------------------------------------------
/deploy/app/static/js/respond.min.js:
--------------------------------------------------------------------------------
1 | /*! Respond.js v1.4.2: min/max-width media query polyfill * Copyright 2013 Scott Jehl
2 | * Licensed under https://github.com/scottjehl/Respond/blob/master/LICENSE-MIT
3 | * */
4 |
5 | !function(a){"use strict";a.matchMedia=a.matchMedia||function(a){var b,c=a.documentElement,d=c.firstElementChild||c.firstChild,e=a.createElement("body"),f=a.createElement("div");return f.id="mq-test-1",f.style.cssText="position:absolute;top:-100em",e.style.background="none",e.appendChild(f),function(a){return f.innerHTML='',c.insertBefore(e,d),b=42===f.offsetWidth,c.removeChild(e),{matches:b,media:a}}}(a.document)}(this),function(a){"use strict";function b(){u(!0)}var c={};a.respond=c,c.update=function(){};var d=[],e=function(){var b=!1;try{b=new a.XMLHttpRequest}catch(c){b=new a.ActiveXObject("Microsoft.XMLHTTP")}return function(){return b}}(),f=function(a,b){var c=e();c&&(c.open("GET",a,!0),c.onreadystatechange=function(){4!==c.readyState||200!==c.status&&304!==c.status||b(c.responseText)},4!==c.readyState&&c.send(null))};if(c.ajax=f,c.queue=d,c.regex={media:/@media[^\{]+\{([^\{\}]*\{[^\}\{]*\})+/gi,keyframes:/@(?:\-(?:o|moz|webkit)\-)?keyframes[^\{]+\{(?:[^\{\}]*\{[^\}\{]*\})+[^\}]*\}/gi,urls:/(url\()['"]?([^\/\)'"][^:\)'"]+)['"]?(\))/g,findStyles:/@media *([^\{]+)\{([\S\s]+?)$/,only:/(only\s+)?([a-zA-Z]+)\s?/,minw:/\([\s]*min\-width\s*:[\s]*([\s]*[0-9\.]+)(px|em)[\s]*\)/,maxw:/\([\s]*max\-width\s*:[\s]*([\s]*[0-9\.]+)(px|em)[\s]*\)/},c.mediaQueriesSupported=a.matchMedia&&null!==a.matchMedia("only all")&&a.matchMedia("only all").matches,!c.mediaQueriesSupported){var g,h,i,j=a.document,k=j.documentElement,l=[],m=[],n=[],o={},p=30,q=j.getElementsByTagName("head")[0]||k,r=j.getElementsByTagName("base")[0],s=q.getElementsByTagName("link"),t=function(){var a,b=j.createElement("div"),c=j.body,d=k.style.fontSize,e=c&&c.style.fontSize,f=!1;return b.style.cssText="position:absolute;font-size:1em;width:1em",c||(c=f=j.createElement("body"),c.style.background="none"),k.style.fontSize="100%",c.style.fontSize="100%",c.appendChild(b),f&&k.insertBefore(c,k.firstChild),a=b.offsetWidth,f?k.removeChild(c):c.removeChild(b),k.style.fontSize=d,e&&(c.style.fontSize=e),a=i=parseFloat(a)},u=function(b){var c="clientWidth",d=k[c],e="CSS1Compat"===j.compatMode&&d||j.body[c]||d,f={},o=s[s.length-1],r=(new Date).getTime();if(b&&g&&p>r-g)return a.clearTimeout(h),h=a.setTimeout(u,p),void 0;g=r;for(var v in l)if(l.hasOwnProperty(v)){var w=l[v],x=w.minw,y=w.maxw,z=null===x,A=null===y,B="em";x&&(x=parseFloat(x)*(x.indexOf(B)>-1?i||t():1)),y&&(y=parseFloat(y)*(y.indexOf(B)>-1?i||t():1)),w.hasquery&&(z&&A||!(z||e>=x)||!(A||y>=e))||(f[w.media]||(f[w.media]=[]),f[w.media].push(m[w.rules]))}for(var C in n)n.hasOwnProperty(C)&&n[C]&&n[C].parentNode===q&&q.removeChild(n[C]);n.length=0;for(var D in f)if(f.hasOwnProperty(D)){var E=j.createElement("style"),F=f[D].join("\n");E.type="text/css",E.media=D,q.insertBefore(E,o.nextSibling),E.styleSheet?E.styleSheet.cssText=F:E.appendChild(j.createTextNode(F)),n.push(E)}},v=function(a,b,d){var e=a.replace(c.regex.keyframes,"").match(c.regex.media),f=e&&e.length||0;b=b.substring(0,b.lastIndexOf("/"));var g=function(a){return a.replace(c.regex.urls,"$1"+b+"$2$3")},h=!f&&d;b.length&&(b+="/"),h&&(f=1);for(var i=0;f>i;i++){var j,k,n,o;h?(j=d,m.push(g(a))):(j=e[i].match(c.regex.findStyles)&&RegExp.$1,m.push(RegExp.$2&&g(RegExp.$2))),n=j.split(","),o=n.length;for(var p=0;o>p;p++)k=n[p],l.push({media:k.split("(")[0].match(c.regex.only)&&RegExp.$2||"all",rules:m.length-1,hasquery:k.indexOf("(")>-1,minw:k.match(c.regex.minw)&&parseFloat(RegExp.$1)+(RegExp.$2||""),maxw:k.match(c.regex.maxw)&&parseFloat(RegExp.$1)+(RegExp.$2||"")})}u()},w=function(){if(d.length){var b=d.shift();f(b.href,function(c){v(c,b.href,b.media),o[b.href]=!0,a.setTimeout(function(){w()},0)})}},x=function(){for(var b=0;b', methods=['GET', 'POST'])
129 | def password_reset(token):
130 | if not current_user.is_anonymous:
131 | return redirect(url_for('main.index'))
132 | form = PasswordResetForm()
133 | if form.validate_on_submit():
134 | user = User.query.filter_by(email=form.email.data).first()
135 | if user is None:
136 | return redirect(url_for('main.index'))
137 | if user.reset_password(token, form.password.data):
138 |
139 | current_app.logger.info(
140 | '{} reset password'.format(user.email))
141 |
142 | flash(_('Your password has been updated.'))
143 | return redirect(url_for('auth.login'))
144 | else:
145 | return redirect(url_for('main.index'))
146 | return render_template('auth/reset_password.html', form=form)
147 |
--------------------------------------------------------------------------------
/deploy/app/messages.pot:
--------------------------------------------------------------------------------
1 | # Translations template for PROJECT.
2 | # Copyright (C) 2017 ORGANIZATION
3 | # This file is distributed under the same license as the PROJECT project.
4 | # FIRST AUTHOR , 2017.
5 | #
6 | msgid ""
7 | msgstr ""
8 | "Project-Id-Version: PROJECT VERSION\n"
9 | "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
10 | "POT-Creation-Date: 2017-05-01 20:18+0800\n"
11 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
12 | "Last-Translator: FULL NAME \n"
13 | "Language-Team: LANGUAGE \n"
14 | "MIME-Version: 1.0\n"
15 | "Content-Type: text/plain; charset=utf-8\n"
16 | "Content-Transfer-Encoding: 8bit\n"
17 | "Generated-By: Babel 2.4.0\n"
18 |
19 | #: auth/forms.py:12 auth/forms.py:20 auth/forms.py:65 auth/forms.py:74
20 | msgid "Email"
21 | msgstr ""
22 |
23 | #: auth/forms.py:14 auth/forms.py:26 auth/forms.py:50
24 | msgid "Password"
25 | msgstr ""
26 |
27 | #: auth/forms.py:15
28 | msgid "Remember Password"
29 | msgstr ""
30 |
31 | #: auth/forms.py:16 report/forms.py:11
32 | msgid "Submit"
33 | msgstr ""
34 |
35 | #: auth/forms.py:22 report/forms.py:15
36 | msgid "Username"
37 | msgstr ""
38 |
39 | #: auth/forms.py:27 auth/forms.py:44
40 | msgid "Passwords doesn't match"
41 | msgstr ""
42 |
43 | #: auth/forms.py:28
44 | msgid "Confirm Password"
45 | msgstr ""
46 |
47 | #: auth/forms.py:29 report/forms.py:22 templates/base.html:48
48 | #: templates/report/read.html:14 templates/report/read_crew.html:27
49 | #: templates/report/read_department.html:25
50 | msgid "Department"
51 | msgstr ""
52 |
53 | #: auth/forms.py:30 templates/auth/register.html:9
54 | msgid "Register"
55 | msgstr ""
56 |
57 | #: auth/forms.py:34 auth/forms.py:61
58 | msgid "Username has been used"
59 | msgstr ""
60 |
61 | #: auth/forms.py:38
62 | msgid "Email has been registered"
63 | msgstr ""
64 |
65 | #: auth/forms.py:42
66 | msgid "Old Password"
67 | msgstr ""
68 |
69 | #: auth/forms.py:43 auth/forms.py:76
70 | msgid "New Password"
71 | msgstr ""
72 |
73 | #: auth/forms.py:45
74 | msgid "Confirm New Password"
75 | msgstr ""
76 |
77 | #: auth/forms.py:46
78 | msgid "Update Password"
79 | msgstr ""
80 |
81 | #: auth/forms.py:52
82 | msgid "New Username"
83 | msgstr ""
84 |
85 | #: auth/forms.py:54
86 | msgid "Usernames doesn't match"
87 | msgstr ""
88 |
89 | #: auth/forms.py:55
90 | msgid "Confirm New Username"
91 | msgstr ""
92 |
93 | #: auth/forms.py:57
94 | msgid "Update Username"
95 | msgstr ""
96 |
97 | #: auth/forms.py:66 auth/forms.py:80 templates/auth/reset_password.html:9
98 | msgid "Reset Password"
99 | msgstr ""
100 |
101 | #: auth/forms.py:70 auth/forms.py:84
102 | msgid "Unknown email address"
103 | msgstr ""
104 |
105 | #: auth/forms.py:78
106 | msgid "Confirm New password"
107 | msgstr ""
108 |
109 | #: auth/views.py:32
110 | msgid "You have been logged out"
111 | msgstr ""
112 |
113 | #: auth/views.py:57
114 | msgid "Successfully Registered, Please Login"
115 | msgstr ""
116 |
117 | #: auth/views.py:74
118 | msgid "Your password has been updated"
119 | msgstr ""
120 |
121 | #: auth/views.py:91
122 | msgid "Your username has been updated"
123 | msgstr ""
124 |
125 | #: auth/views.py:118
126 | msgid "An email with instructions to reset your password has been sent to "
127 | msgstr ""
128 |
129 | #: auth/views.py:138
130 | msgid "Your password has been updated."
131 | msgstr ""
132 |
133 | #: main/views.py:33
134 | msgid "Failed Uploading"
135 | msgstr ""
136 |
137 | #: report/forms.py:9
138 | msgid "This week's work content and plan of next week"
139 | msgstr ""
140 |
141 | #: report/forms.py:16
142 | msgid "Start"
143 | msgstr ""
144 |
145 | #: report/forms.py:17
146 | msgid "End"
147 | msgstr ""
148 |
149 | #: report/forms.py:18
150 | msgid "Query"
151 | msgstr ""
152 |
153 | #: report/forms.py:26
154 | msgid "Send Reminder Email"
155 | msgstr ""
156 |
157 | #: report/views.py:36 report/views.py:76
158 | msgid "Successfully submitted report"
159 | msgstr ""
160 |
161 | #: report/views.py:102
162 | msgid ""
163 | "Do you want to edit last week's "
164 | "report?"
165 | msgstr ""
166 |
167 | #: report/views.py:112
168 | msgid "You haven't submitted your weekly report"
169 | msgstr ""
170 |
171 | #: report/views.py:307 report/views.py:360
172 | msgid "Email has been sent to:"
173 | msgstr ""
174 |
175 | #: templates/403.html:6
176 | msgid "Forbidden"
177 | msgstr ""
178 |
179 | #: templates/404.html:6
180 | msgid "URL is not available"
181 | msgstr ""
182 |
183 | #: templates/base.html:39
184 | msgid "Write"
185 | msgstr ""
186 |
187 | #: templates/base.html:43
188 | msgid "My Reports"
189 | msgstr ""
190 |
191 | #: templates/base.html:52 templates/base.html:62
192 | msgid "Statistics"
193 | msgstr ""
194 |
195 | #: templates/base.html:58 templates/report/read.html:13
196 | #: templates/report/read_crew.html:26 templates/report/read_department.html:24
197 | msgid "Employee"
198 | msgstr ""
199 |
200 | #: templates/base.html:68
201 | msgid "Admin"
202 | msgstr ""
203 |
204 | #: templates/base.html:76
205 | msgid "Account"
206 | msgstr ""
207 |
208 | #: templates/auth/change_password.html:8 templates/base.html:80
209 | msgid "Change Password"
210 | msgstr ""
211 |
212 | #: templates/auth/change_username.html:8 templates/base.html:84
213 | msgid "Change Username"
214 | msgstr ""
215 |
216 | #: templates/base.html:88
217 | msgid "Exit"
218 | msgstr ""
219 |
220 | #: templates/auth/login.html:9 templates/base.html:96
221 | msgid "Login"
222 | msgstr ""
223 |
224 | #: templates/auth/login.html:14
225 | msgid "New User"
226 | msgstr ""
227 |
228 | #: templates/auth/login.html:15
229 | msgid "Click to Register"
230 | msgstr ""
231 |
232 | #: templates/auth/login.html:17
233 | msgid "Forget Password"
234 | msgstr ""
235 |
236 | #: templates/auth/login.html:18
237 | msgid "Click to Rest Password"
238 | msgstr ""
239 |
240 | #: templates/report/read.html:15 templates/report/read_crew.html:28
241 | #: templates/report/read_department.html:26
242 | msgid "Week Count"
243 | msgstr ""
244 |
245 | #: templates/report/read.html:16 templates/report/read_crew.html:29
246 | #: templates/report/read_department.html:27
247 | msgid "Year"
248 | msgstr ""
249 |
250 | #: templates/report/read.html:20 templates/report/read.html:24
251 | msgid "Edit"
252 | msgstr ""
253 |
254 |
--------------------------------------------------------------------------------
/deploy/app/main/views.py:
--------------------------------------------------------------------------------
1 | #coding:utf-8
2 | from datetime import date
3 | from flask import request, Response, redirect, url_for, current_app
4 | from flask_admin.model import typefmt
5 | from flask_admin.contrib.sqla import ModelView
6 | from flask_babelex import lazy_gettext as _
7 | from flask_login import current_user
8 | import os
9 | from werkzeug.utils import secure_filename
10 | from . import main
11 | from .. import admin, db
12 | from ..models import Permission, User, Role, Report, Department
13 | from ..utils import permission_required, is_allowed_file, clean_html
14 | from sqlalchemy.exc import OperationalError
15 |
16 | @main.route('/', methods=['GET', 'POST'])
17 | def index():
18 | # check if the database is initialized.
19 | try:
20 | User.query.all()
21 | except OperationalError:
22 | db.create_all()
23 | Role.insert_roles()
24 | Department.insert_departments()
25 |
26 | if not current_user.is_authenticated:
27 | return redirect(url_for('auth.login'))
28 | return redirect(url_for('report.read'))
29 |
30 |
31 | @main.route("/upload/", methods=["POST"])
32 | @permission_required(Permission.WRITE_REPORT)
33 | def upload():
34 | img = request.files.get('image')
35 | if img and is_allowed_file(img.filename):
36 | filename = secure_filename(img.filename)
37 | img.save(os.path.join(current_app.config['UPLOAD_FOLDER'], filename))
38 | img_url = request.url_root + current_app.config['IMAGE_UPLOAD_DIR'] + filename
39 | res = Response(img_url)
40 |
41 | current_app.logger.info(
42 | '{} uploaded image'.format(current_user.email))
43 | else:
44 | res = Response(_("Failed Uploading"))
45 | res.headers["ContentType"] = "text/html"
46 | res.headers["Charset"] = "utf-8"
47 |
48 | current_app.logger.error(
49 | '{} failed uploading image'.format(current_user.email))
50 | return res
51 |
52 |
53 | class WeeklyReportModelView(ModelView):
54 | base_template = '/base.html'
55 |
56 | def is_accessible(self):
57 | return current_user.is_admin
58 |
59 | def inaccessible_callback(self, name, **kwargs):
60 | return redirect(url_for('main.index'))
61 |
62 |
63 | class UserAdminView(WeeklyReportModelView):
64 | column_labels = dict(email='邮箱', username='姓名',
65 | is_ignored='不参与统计',
66 | role='角色', department='部门')
67 | form_columns = column_list = [
68 | 'email', 'username', 'is_ignored', 'role', 'department']
69 | can_delete = True
70 | can_create = False
71 | form_widget_args = {
72 | 'email': {
73 | 'readonly': True
74 | },
75 | }
76 |
77 | def on_model_delete(self, model):
78 |
79 | current_app.logger.info(
80 | '{} deleted user:{}'.format(current_user.email, model))
81 |
82 | for report in Report.query.filter_by(author_id=model.id):
83 | db.session.delete(report)
84 | db.session.commit()
85 |
86 |
87 | class RoleAdminView(WeeklyReportModelView):
88 | column_labels = dict(name='名称', users='成员')
89 | form_columns = ['name', 'users']
90 | column_list = ['name']
91 | can_create = False
92 | can_edit = True
93 | can_delete = False
94 | form_widget_args = {
95 | 'name': {
96 | 'readonly': True
97 | },
98 | }
99 |
100 |
101 | class DepartmentAdminView(WeeklyReportModelView):
102 | column_labels = dict(name='名称', users='成员')
103 | form_columns = ['name', 'users']
104 | can_edit = True
105 | can_delete = False
106 |
107 |
108 | class ReportAdminView(WeeklyReportModelView):
109 | column_labels = dict(year=u'年份', week_count=u'周次',
110 | created_at=u'创建时间', last_content=u'上周计划', content=u'内容',
111 | author=u'员工', department=u'部门')
112 | column_list = ('author', 'department', 'year', 'week_count', 'last_content',
113 | 'content', 'created_at')
114 | column_default_sort = ('created_at', True)
115 | column_searchable_list = ('week_count',)
116 | form_columns = ['created_at', 'week_count', 'year', 'content']
117 | list_template = '/admin/model/report_list_template.html'
118 | can_edit = True
119 | can_export = True
120 | export_types=['xls']
121 | form_widget_args = {
122 | 'year': {
123 | 'readonly': True
124 | },
125 | 'last_content': {
126 | 'readonly': True
127 | },
128 | 'created_at': {
129 | 'readonly': True
130 | },
131 | }
132 |
133 | def date_format(view, value):
134 | return value.strftime('%Y-%m-%d')
135 |
136 | def author_format(v, c, m, p):
137 | return str(m.author)
138 |
139 | def department_format(v, c, m, p):
140 | return str(m.department)
141 |
142 | def format_last_content(v, c, m, p):
143 | if m.last_content:
144 | return clean_html(m.last_content)
145 |
146 | return ''
147 |
148 | def format_content(v, c, m, p):
149 | if m.content:
150 | return clean_html(m.content)
151 |
152 | return ''
153 |
154 | def format_created_at(v, c, m, p):
155 | return m.created_at.strftime('%Y-%m-%d')
156 |
157 | REPORT_FORMATTERS = dict(typefmt.BASE_FORMATTERS)
158 | REPORT_FORMATTERS.update({
159 | date: date_format,
160 | })
161 | column_type_formatters = REPORT_FORMATTERS
162 |
163 | EXPORT_REPORT_FORMATTERS = dict(typefmt.BASE_FORMATTERS)
164 | EXPORT_REPORT_FORMATTERS.update({
165 | "author": author_format,
166 | "department": department_format,
167 | "last_content": format_last_content,
168 | "content": format_content,
169 | "created_at":format_created_at,
170 | })
171 | column_formatters_export = EXPORT_REPORT_FORMATTERS
172 |
173 |
174 | admin.add_view(UserAdminView(User, db.session, name='用户'))
175 | admin.add_view(RoleAdminView(Role, db.session, name='角色'))
176 | admin.add_view(ReportAdminView(Report, db.session, name='周报', endpoint="reports"))
177 | admin.add_view(DepartmentAdminView(Department, db.session, name='部门'))
178 |
--------------------------------------------------------------------------------
/deploy/app/templates/base.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | WeeklyReport
5 |
6 |
7 |
8 |
9 |
10 |
11 |
15 |
16 |
17 |
21 |
22 | {% block head_script %}
23 | {% endblock %}
24 |
25 |
26 |
27 |
28 |
104 |
105 |
106 |
108 |
109 |
110 |
111 | {% block container %}
112 |
113 | {% for message in get_flashed_messages() %}
114 |
115 |
116 | {{ message }}
117 |
118 | {% endfor %}
119 |
120 | {% block page_content %}
121 | {% endblock %}
122 |
123 | {% endblock %}
124 |
125 |
126 |
127 |
128 |
129 |
130 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 | {% block script %}
146 | {% endblock %}
147 |
148 |
149 |
--------------------------------------------------------------------------------
/deploy/app/translations/zh_Hans_CN/LC_MESSAGES/messages.po:
--------------------------------------------------------------------------------
1 | # Chinese (Simplified, China) translations for PROJECT.
2 | # Copyright (C) 2017 ORGANIZATION
3 | # This file is distributed under the same license as the PROJECT project.
4 | # FIRST AUTHOR , 2017.
5 | #
6 | msgid ""
7 | msgstr ""
8 | "Project-Id-Version: PROJECT VERSION\n"
9 | "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
10 | "POT-Creation-Date: 2017-05-01 20:18+0800\n"
11 | "PO-Revision-Date: 2017-04-26 15:28+0800\n"
12 | "Last-Translator: FULL NAME \n"
13 | "Language: zh_Hans_CN\n"
14 | "Language-Team: zh_Hans_CN \n"
15 | "Plural-Forms: nplurals=1; plural=0\n"
16 | "MIME-Version: 1.0\n"
17 | "Content-Type: text/plain; charset=utf-8\n"
18 | "Content-Transfer-Encoding: 8bit\n"
19 | "Generated-By: Babel 2.4.0\n"
20 |
21 | #: auth/forms.py:12 auth/forms.py:20 auth/forms.py:65 auth/forms.py:74
22 | msgid "Email"
23 | msgstr "邮箱"
24 |
25 | #: auth/forms.py:14 auth/forms.py:26 auth/forms.py:50
26 | msgid "Password"
27 | msgstr "密码"
28 |
29 | #: auth/forms.py:15
30 | msgid "Remember Password"
31 | msgstr "记住密码"
32 |
33 | #: auth/forms.py:16 report/forms.py:11
34 | msgid "Submit"
35 | msgstr "提交"
36 |
37 | #: auth/forms.py:22 report/forms.py:15
38 | msgid "Username"
39 | msgstr "姓名"
40 |
41 | #: auth/forms.py:27 auth/forms.py:44
42 | msgid "Passwords doesn't match"
43 | msgstr "两次密码不匹配"
44 |
45 | #: auth/forms.py:28
46 | msgid "Confirm Password"
47 | msgstr "确认密码"
48 |
49 | #: auth/forms.py:29 report/forms.py:22 templates/base.html:48
50 | #: templates/report/read.html:14 templates/report/read_crew.html:27
51 | #: templates/report/read_department.html:25
52 | msgid "Department"
53 | msgstr "部门"
54 |
55 | #: auth/forms.py:30 templates/auth/register.html:9
56 | msgid "Register"
57 | msgstr "注册"
58 |
59 | #: auth/forms.py:34 auth/forms.py:61
60 | msgid "Username has been used"
61 | msgstr "姓名已被使用"
62 |
63 | #: auth/forms.py:38
64 | msgid "Email has been registered"
65 | msgstr "邮箱已被注册"
66 |
67 | #: auth/forms.py:42
68 | msgid "Old Password"
69 | msgstr "旧密码"
70 |
71 | #: auth/forms.py:43 auth/forms.py:76
72 | msgid "New Password"
73 | msgstr "新密码"
74 |
75 | #: auth/forms.py:45
76 | msgid "Confirm New Password"
77 | msgstr "确认新密码"
78 |
79 | #: auth/forms.py:46
80 | msgid "Update Password"
81 | msgstr "更新密码"
82 |
83 | #: auth/forms.py:52
84 | msgid "New Username"
85 | msgstr "新姓名"
86 |
87 | #: auth/forms.py:54
88 | msgid "Usernames doesn't match"
89 | msgstr "两次姓名不匹配"
90 |
91 | #: auth/forms.py:55
92 | msgid "Confirm New Username"
93 | msgstr "确认新姓名"
94 |
95 | #: auth/forms.py:57
96 | msgid "Update Username"
97 | msgstr "更新姓名"
98 |
99 | #: auth/forms.py:66 auth/forms.py:80 templates/auth/reset_password.html:9
100 | msgid "Reset Password"
101 | msgstr "重置密码"
102 |
103 | #: auth/forms.py:70 auth/forms.py:84
104 | msgid "Unknown email address"
105 | msgstr "未知密码"
106 |
107 | #: auth/forms.py:78
108 | msgid "Confirm New password"
109 | msgstr "确认新密码"
110 |
111 | #: auth/views.py:33
112 | msgid "You have been logged out"
113 | msgstr "您已注销"
114 |
115 | #: auth/views.py:61
116 | msgid "Successfully Registered, Please Login"
117 | msgstr "成功注册,请登录"
118 |
119 | #: auth/views.py:78
120 | msgid "Your password has been updated"
121 | msgstr "您的密码已更新"
122 |
123 | #: auth/views.py:95
124 | msgid "Your username has been updated"
125 | msgstr "您的用户名已更新"
126 |
127 | #: auth/views.py:122
128 | msgid "An email with instructions to reset your password has been sent to "
129 | msgstr "密码重置邮件已发送到您的邮箱 "
130 |
131 | #: auth/views.py:142
132 | msgid "Your password has been updated."
133 | msgstr "您的密码已更新"
134 |
135 | #: main/views.py:33
136 | msgid "Failed Uploading"
137 | msgstr "上传失败"
138 |
139 | #: report/forms.py:9
140 | msgid "This week's work content and plan of next week"
141 | msgstr "本周工作内容与下周计划"
142 |
143 | #: report/forms.py:16
144 | msgid "Start"
145 | msgstr "开始"
146 |
147 | #: report/forms.py:17
148 | msgid "End"
149 | msgstr "结束"
150 |
151 | #: report/forms.py:18
152 | msgid "Query"
153 | msgstr "查询"
154 |
155 | #: report/forms.py:26
156 | msgid "Send Reminder Email"
157 | msgstr "发送催交邮件"
158 |
159 | #: report/views.py:36 report/views.py:76
160 | msgid "Successfully submitted report"
161 | msgstr "周报提交成功"
162 |
163 | #: report/views.py:102
164 | msgid ""
165 | "Do you want to edit last week's "
166 | "report?"
167 | msgstr "您上周工作周报还未提交,点击此处编辑"
168 |
169 | #: report/views.py:112
170 | msgid "You haven't submitted your weekly report"
171 | msgstr "您还未提交您本周的周报"
172 |
173 | #: report/views.py:307 report/views.py:360
174 | msgid "Email has been sent to:"
175 | msgstr "邮件已发送至:"
176 |
177 | #: templates/403.html:6
178 | msgid "Forbidden"
179 | msgstr "禁止访问"
180 |
181 | #: templates/404.html:6
182 | msgid "URL is not available"
183 | msgstr "URL不可用"
184 |
185 | #: templates/base.html:39
186 | msgid "Write"
187 | msgstr "写周报"
188 |
189 | #: templates/base.html:43
190 | msgid "My Reports"
191 | msgstr "我的周报"
192 |
193 | #: templates/base.html:52 templates/base.html:62
194 | msgid "Statistics"
195 | msgstr "统计"
196 |
197 | #: templates/base.html:58 templates/report/read.html:13
198 | #: templates/report/read_crew.html:26 templates/report/read_department.html:24
199 | msgid "Employee"
200 | msgstr "员工"
201 |
202 | #: templates/base.html:68
203 | msgid "Admin"
204 | msgstr "后台"
205 |
206 | #: templates/base.html:76
207 | msgid "Account"
208 | msgstr "账户"
209 |
210 | #: templates/auth/change_password.html:8 templates/base.html:80
211 | msgid "Change Password"
212 | msgstr "更改密码"
213 |
214 | #: templates/auth/change_username.html:8 templates/base.html:84
215 | msgid "Change Username"
216 | msgstr "更改姓名"
217 |
218 | #: templates/base.html:88
219 | msgid "Exit"
220 | msgstr "退出"
221 |
222 | #: templates/auth/login.html:9 templates/base.html:96
223 | msgid "Login"
224 | msgstr "登录"
225 |
226 | #: templates/auth/login.html:14
227 | msgid "New User"
228 | msgstr "新用户"
229 |
230 | #: templates/auth/login.html:15
231 | msgid "Click to Register"
232 | msgstr "点击注册"
233 |
234 | #: templates/auth/login.html:17
235 | msgid "Forget Password"
236 | msgstr "忘记密码"
237 |
238 | #: templates/auth/login.html:18
239 | msgid "Click to Rest Password"
240 | msgstr "点击重置密码"
241 |
242 | #: templates/report/read.html:15 templates/report/read_crew.html:28
243 | #: templates/report/read_department.html:26
244 | #: templates/admin/model/report_list_template.html:56
245 | msgid "Week Count"
246 | msgstr "周次"
247 |
248 | #: templates/report/read.html:16 templates/report/read_crew.html:29
249 | #: templates/report/read_department.html:27
250 | msgid "Year"
251 | msgstr "年份"
252 |
253 | #: templates/report/read.html:20 templates/report/read.html:24
254 | msgid "Edit"
255 | msgstr "编辑"
256 |
257 | #: auth/views.py:25
258 | msgid "Account or password is wrong"
259 | msgstr "邮箱或密码错误"
260 |
261 | #: templates/report/read_crew.html:33
262 | msgid "Last Week"
263 | msgstr "上周"
264 |
265 | #: templates/report/read_crew.html:37
266 | msgid "Current Week"
267 | msgstr "本周"
--------------------------------------------------------------------------------
/deploy/app/models.py:
--------------------------------------------------------------------------------
1 | #coding:utf-8
2 | from datetime import datetime
3 | from werkzeug.security import generate_password_hash, check_password_hash
4 | from flask import current_app
5 | from flask_login import UserMixin, AnonymousUserMixin
6 | from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
7 | from . import db, login_manager
8 | from .utils import get_week_count, get_last_week
9 |
10 |
11 | class Permission:
12 | DO_NOTHING = 0x00
13 | WRITE_REPORT = 0x01
14 | READ_DEPARTMENT_REPORT = 0x02
15 | READ_ALL_REPORT = 0X04
16 | ENTER_ADMIN = 0x08
17 |
18 |
19 | class Role(db.Model):
20 | __tablename__ = 'roles'
21 | id = db.Column(db.Integer, primary_key=True)
22 | name = db.Column(db.String(64), unique=True)
23 | permissions = db.Column(db.Integer)
24 | users = db.relationship('User', backref='role', lazy='dynamic')
25 |
26 | @staticmethod
27 | def insert_roles():
28 | roles = {
29 | 'QUIT': Permission.DO_NOTHING,
30 | 'EMPLOYEE': Permission.WRITE_REPORT,
31 | 'MANAGER': (Permission.WRITE_REPORT |
32 | Permission.READ_DEPARTMENT_REPORT |
33 | Permission.ENTER_ADMIN),
34 | 'HR': (Permission.WRITE_REPORT |
35 | Permission.READ_DEPARTMENT_REPORT |
36 | Permission.READ_ALL_REPORT),
37 | 'ADMINISTRATOR': 0xff,
38 | }
39 | for r in roles:
40 | role = Role.query.filter_by(name=unicode(r)).first()
41 | if role is None:
42 | role = Role(name=unicode(r),
43 | permissions=roles[r])
44 | db.session.add(role)
45 | db.session.commit()
46 |
47 | def __str__(self):
48 | return self.name
49 |
50 | def __repr__(self):
51 | return self.name
52 |
53 |
54 | class Department(db.Model):
55 | __tablename__ = 'departments'
56 | id = db.Column(db.Integer, primary_key=True)
57 | name = db.Column(db.String(64), unique=True)
58 | users = db.relationship('User', backref='department', lazy='dynamic')
59 |
60 | @staticmethod
61 | def insert_departments():
62 | for dept in current_app.config['DEPARTMENTS']:
63 | if not Department.query.filter_by(name=unicode(dept)).first():
64 | dept = Department(name=unicode(dept))
65 | db.session.add(dept)
66 | db.session.commit()
67 |
68 | @staticmethod
69 | def delete_departments():
70 | dept="1"
71 | dept= Department.query.filter_by(name=unicode(dept)).first()
72 | if dept:
73 | db.session.delete(dept)
74 | db.session.commit()
75 |
76 | def __str__(self):
77 | return self.name
78 |
79 | def __repr__(self):
80 | return self.name
81 |
82 |
83 | class User(db.Model, UserMixin):
84 | __tablename__ = 'users'
85 | id = db.Column(db.Integer, primary_key=True)
86 | email = db.Column(db.String(64), unique=True, index=True)
87 | username = db.Column(db.String(64), unique=True, index=True)
88 | password_hash = db.Column(db.String(128))
89 | is_ignored = db.Column(db.Boolean, default=False)
90 | role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))
91 | department_id = db.Column(db.Integer, db.ForeignKey('departments.id'))
92 | is_super_admin = db.Column(db.Boolean, default=False)
93 |
94 | @property
95 | def password(self):
96 | raise AttributeError('Password is not a readable attribute')
97 |
98 | @password.setter
99 | def password(self, password):
100 | self.password_hash = generate_password_hash(password)
101 |
102 | def verify_password(self, password):
103 | return check_password_hash(self.password_hash, password)
104 |
105 | def can(self, permisson):
106 | return self.role is not None and \
107 | (self.role.permissions & permisson) == permisson
108 |
109 | def generate_reset_token(self, expiration=3600):
110 | s = Serializer(current_app.config['SECRET_KEY'], expiration)
111 | return s.dumps({'reset': self.id})
112 |
113 | def reset_password(self, token, new_password):
114 | s = Serializer(current_app.config['SECRET_KEY'])
115 | try:
116 | data = s.loads(token)
117 | except:
118 | return False
119 | if data.get('reset') != self.id:
120 | return False
121 | self.password = new_password
122 | db.session.add(self)
123 | return True
124 |
125 | @property
126 | def is_admin(self):
127 | return self.role.name == 'ADMINISTRATOR' or self.is_super_admin
128 |
129 | @property
130 | def is_hr(self):
131 | return self.role is not None and self.role.name == 'HR'
132 |
133 | @property
134 | def is_manager(self):
135 | return self.role is not None and self.role.name == 'MANAGER'
136 |
137 | @property
138 | def is_authenticated(self):
139 | return self.can(Permission.WRITE_REPORT)
140 |
141 | def __str__(self):
142 | return self.username
143 |
144 | def __repr__(self):
145 | return self.username
146 |
147 |
148 | class AnonymousUser(AnonymousUserMixin):
149 |
150 | def can(self, permissions):
151 | return False
152 |
153 | @property
154 | def is_authenticated(self):
155 | return False
156 |
157 | @property
158 | def email(self):
159 | return 'AnonymousUser'
160 |
161 | @property
162 | def is_admin(self):
163 | return False
164 |
165 |
166 | class Report(db.Model):
167 | __tablename__ = 'reports'
168 | id = db.Column(db.Integer, primary_key=True)
169 | created_at = db.Column(db.DateTime, index=True, default=datetime.now)
170 | author_id = db.Column(db.Integer, db.ForeignKey('users.id'))
171 | content = db.Column(db.Text)
172 | last_content = db.Column(db.Text)
173 | week_count = db.Column(db.Integer)
174 | year = db.Column(db.Integer)
175 |
176 | @property
177 | def author(self):
178 | return User.query.get(self.author_id)
179 |
180 | @property
181 | def department(self):
182 | return User.query.get(self.author_id).department
183 |
184 | @property
185 | def is_of_current_week(self):
186 | if self.week_count == get_week_count() \
187 | and self.year == datetime.today().year:
188 | return True
189 | return False
190 |
191 | @property
192 | def is_of_last_week(self):
193 | if self.week_count == get_week_count(get_last_week()) \
194 | and self.year == get_last_week().year:
195 | return True
196 | return False
197 |
198 | @staticmethod
199 | def get_last_report(author_id, week_count):
200 | report = Report.query.filter_by(author_id=author_id,week_count=week_count).first()
201 | if report:
202 | return report
203 |
204 | def __str__(self):
205 | return 'Posted by {} at {}'.format(
206 | User.query.get(self.author_id).email, self.created_at)
207 |
208 | def __repr__(self):
209 | return 'Posted by {} at {}'.format(
210 | User.query.get(self.author_id).email, self.created_at)
211 |
212 |
213 | @login_manager.user_loader
214 | def load_user(user_id):
215 | #assert type(user_id) == str
216 | return User.query.get(int(user_id))
217 |
218 |
219 | login_manager.anonymous_user = AnonymousUser
220 |
--------------------------------------------------------------------------------
/deploy/app/templates/admin/model/report_list_template.html:
--------------------------------------------------------------------------------
1 | {% extends 'admin/master.html' %}
2 | {% import 'admin/lib.html' as lib with context %}
3 | {% import 'admin/static.html' as admin_static with context%}
4 | {% import 'admin/model/layout.html' as model_layout with context %}
5 | {% import 'admin/actions.html' as actionlib with context %}
6 | {% import 'admin/model/row_actions.html' as row_actions with context %}
7 |
8 | {% block head %}
9 | {{ super() }}
10 | {{ lib.form_css() }}
11 | {% endblock %}
12 |
13 | {% block body %}
14 | {% block model_menu_bar %}
15 |
16 | -
17 | {{ _gettext('List') }}{% if count %} ({{ count }}){% endif %}
18 |
19 |
20 | {% if admin_view.can_create %}
21 | -
22 | {%- if admin_view.create_modal -%}
23 | {{ lib.add_modal_button(url=get_url('.create_view', url=return_url, modal=True), title=_gettext('Create New Record'), content=_gettext('Create')) }}
24 | {% else %}
25 | {{ _gettext('Create') }}
26 | {%- endif -%}
27 |
28 | {% endif %}
29 |
30 | {% if admin_view.can_export %}
31 | {{ model_layout.export_options() }}
32 | {% endif %}
33 |
34 | {% block model_menu_bar_before_filters %}{% endblock %}
35 |
36 | {% if filters %}
37 | -
38 | {{ model_layout.filter_options() }}
39 |
40 | {% endif %}
41 |
42 | {% if can_set_page_size %}
43 | -
44 | {{ model_layout.page_size_form(page_size_url) }}
45 |
46 | {% endif %}
47 |
48 | {% if actions %}
49 | -
50 | {{ actionlib.dropdown(actions) }}
51 |
52 | {% endif %}
53 |
54 | {% if search_supported %}
55 | -
56 | {{_('Week Count')}}
57 | {{ model_layout.search_form() }}
58 |
59 | {% endif %}
60 | {% block model_menu_bar_after_filters %}{% endblock %}
61 |
62 | {% endblock %}
63 |
64 | {% if filters %}
65 | {{ model_layout.filter_form() }}
66 |
67 | {% endif %}
68 |
69 | {% block model_list_table %}
70 |
71 |
72 |
73 |
74 | {% block list_header scoped %}
75 | {% if actions %}
76 | |
77 |
78 | |
79 | {% endif %}
80 | {% block list_row_actions_header %}
81 | {% if admin_view.column_display_actions %}
82 | |
83 | {% endif %}
84 | {% endblock %}
85 | {% for c, name in list_columns %}
86 | {% set column = loop.index0 %}
87 |
88 | {% if admin_view.is_sortable(c) %}
89 | {% if sort_column == column %}
90 |
91 | {{ name }}
92 | {% if sort_desc %}
93 |
94 | {% else %}
95 |
96 | {% endif %}
97 |
98 | {% else %}
99 | {{ name }}
100 | {% endif %}
101 | {% else %}
102 | {{ name }}
103 | {% endif %}
104 | {% if admin_view.column_descriptions.get(c) %}
105 |
109 | {% endif %}
110 | |
111 | {% endfor %}
112 | {% endblock %}
113 |
114 |
115 | {% for row in data %}
116 |
117 | {% block list_row scoped %}
118 | {% if actions %}
119 | |
120 |
121 | |
122 | {% endif %}
123 | {% block list_row_actions_column scoped %}
124 | {% if admin_view.column_display_actions %}
125 |
126 | {% block list_row_actions scoped %}
127 | {% for action in list_row_actions %}
128 | {{ action.render_ctx(get_pk_value(row), row) }}
129 | {% endfor %}
130 | {% endblock %}
131 | |
132 | {%- endif -%}
133 | {% endblock %}
134 |
135 | {% for c, name in list_columns %}
136 |
137 | {% if admin_view.is_editable(c) %}
138 | {% set form = list_forms[get_pk_value(row)] %}
139 | {% if form.csrf_token %}
140 | {{ form[c](pk=get_pk_value(row), display_value=get_value(row, c), csrf=form.csrf_token._value()) }}
141 | {% else %}
142 | {{ form[c](pk=get_pk_value(row), display_value=get_value(row, c)) }}
143 | {% endif %}
144 | {% else %}
145 | {% if c=='content' %}
146 | {{ get_value(row, c) |safe}}
147 | {% elif c =='last_content'%}
148 | {{ get_value(row, c) |safe | get_last_week_content}}
149 | {% else %}
150 | {{ get_value(row, c) }}
151 | {% endif %}
152 | {% endif %}
153 | |
154 | {% endfor %}
155 | {% endblock %}
156 |
157 | {% else %}
158 |
159 | |
160 | {% block empty_list_message %}
161 |
162 | {{ admin_view.get_empty_list_message() }}
163 |
164 | {% endblock %}
165 | |
166 |
167 | {% endfor %}
168 |
169 |
170 | {% block list_pager %}
171 | {% if num_pages is not none %}
172 | {{ lib.pager(page, num_pages, pager_url) }}
173 | {% else %}
174 | {{ lib.simple_pager(page, data|length == page_size, pager_url) }}
175 | {% endif %}
176 | {% endblock %}
177 | {% endblock %}
178 |
179 | {% block actions %}
180 | {{ actionlib.form(actions, get_url('.action_view')) }}
181 | {% endblock %}
182 |
183 | {%- if admin_view.edit_modal or admin_view.create_modal or admin_view.details_modal -%}
184 | {{ lib.add_modal_window() }}
185 | {%- endif -%}
186 | {% endblock %}
187 |
188 | {% block tail %}
189 | {{ super() }}
190 |
191 | {{ lib.form_js() }}
192 |
193 | {{ actionlib.script(_gettext('Please select at least one record.'),
194 | actions,
195 | actions_confirmation) }}
196 |
197 |
229 | {% endblock %}
230 |
--------------------------------------------------------------------------------
/deploy/app/static/wangEditor/css/wangEditor.min.css:
--------------------------------------------------------------------------------
1 | .txt-toolbar a,.wangEditor-drop-list a,.wangEditor-drop-panel a,.wangEditor-menu-container a{text-decoration:none}.wangEditor-container{position:relative;background-color:#fff;border:1px solid #ccc;z-index:1;width:100%}.wangEditor-container img,.wangEditor-container textarea{border:none}.wangEditor-container a:focus,.wangEditor-container button:focus{outline:0}.wangEditor-container,.wangEditor-container *{margin:0;padding:0;box-sizing:border-box;line-height:1}.wangEditor-container .clearfix:after{content:'';display:table;clear:both}.wangEditor-container textarea:focus{outline:0}.wangEditor-container .height-tip{position:absolute;width:3px;background-color:#ccc;left:0;transition:top .2s}.wangEditor-container .txt-toolbar{position:absolute;background-color:#fff;padding:3px 5px;border-top:2px solid #666;box-shadow:1px 3px 3px #999;border-left:1px\9 solid\9 #ccc\9;border-bottom:1px\9 solid\9 #999\9;border-right:1px\9 solid\9 #999\9}.wangEditor-container .txt-toolbar .tip-triangle{display:block;position:absolute;width:0;height:0;border:5px solid;border-color:transparent transparent #666;top:-12px;left:50%;margin-left:-5px}.wangEditor-container .txt-toolbar a{color:#666;display:inline-block;margin:0 3px;padding:5px;text-decoration:none;border-radius:3px}.wangEditor-container .txt-toolbar a:hover{background-color:#f1f1f1}.wangEditor-container .img-drag-point{display:block;position:absolute;width:12px;height:12px;border-radius:50%;cursor:se-resize;background-color:#666;margin-left:-6px;margin-top:-6px;box-shadow:1px 1px 5px #999}.wangEditor-container .wangEditor-upload-progress{position:absolute;height:1px;background:#1e88e5;width:0;display:none;-webkit-transition:width .5s;-o-transition:width .5s;transition:width .5s}.wangEditor-fullscreen{position:fixed;top:0;bottom:0;left:0;right:0}.wangEditor-container .code-textarea{resize:none;width:100%;font-size:14px;line-height:1.5;font-family:Verdana;color:#333;padding:0 15px}.wangEditor-menu-container{width:100%;border-bottom:1px solid #f1f1f1;background-color:#fff}.wangEditor-menu-container .menu-item .active,.wangEditor-menu-container .menu-item:hover{background-color:#f1f1f1}.wangEditor-menu-container .menu-group{float:left;padding:0 8px;border-right:1px solid #f1f1f1}.wangEditor-menu-container .menu-item{float:left;position:relative;text-align:center;height:31px;width:35px}.wangEditor-menu-container .menu-item a{display:block;text-align:center;color:#666;width:100%;padding:8px 0;font-size:.9em}.wangEditor-menu-container .menu-item .selected{color:#1e88e5}.wangEditor-menu-container .menu-item .disable{opacity:.5;filter:alpha(opacity=50)}.wangEditor-menu-container .menu-tip{position:absolute;z-index:20;width:60px;text-align:center;background-color:#666;color:#fff;padding:7px 0;font-size:12px;top:100%;left:50%;margin-left:-30px;border-radius:2px;box-shadow:1px 1px 5px #999;display:none}.wangEditor-menu-container .menu-tip-40{width:40px;margin-left:-20px}.wangEditor-menu-container .menu-tip-50{width:50px;margin-left:-25px}.wangEditor-menu-shadow{border-bottom:1px\9 solid\9 #f1f1f1\9;box-shadow:0 1px 3px #999}.wangEditor-container .wangEditor-txt{width:100%;text-align:left;padding:0 15px 15px;margin-top:5px;overflow-y:auto}.wangEditor-container .wangEditor-txt h1,.wangEditor-container .wangEditor-txt h2,.wangEditor-container .wangEditor-txt h3,.wangEditor-container .wangEditor-txt h4,.wangEditor-container .wangEditor-txt h5,.wangEditor-container .wangEditor-txt p{margin:10px 0;line-height:1.8}.wangEditor-container .wangEditor-txt h1 *,.wangEditor-container .wangEditor-txt h2 *,.wangEditor-container .wangEditor-txt h3 *,.wangEditor-container .wangEditor-txt h4 *,.wangEditor-container .wangEditor-txt h5 *,.wangEditor-container .wangEditor-txt p *{line-height:1.8}.wangEditor-container .wangEditor-txt ol,.wangEditor-container .wangEditor-txt ul{padding-left:20px}.wangEditor-container .wangEditor-txt img{cursor:pointer}.wangEditor-container .wangEditor-txt img.clicked,.wangEditor-container .wangEditor-txt table.clicked{box-shadow:1px 1px 10px #999}.wangEditor-container .wangEditor-txt pre code{line-height:1.5}.wangEditor-container .wangEditor-txt:focus{outline:0}.wangEditor-container .wangEditor-txt blockquote{display:block;border-left:8px solid #d0e5f2;padding:5px 10px;margin:10px 0;line-height:1.4;font-size:100%;background-color:#f1f1f1}.wangEditor-container .wangEditor-txt table{border:none;border-collapse:collapse}.wangEditor-container .wangEditor-txt table td,.wangEditor-container .wangEditor-txt table th{border:1px solid #999;padding:3px 5px;min-width:50px;height:20px}.wangEditor-container .wangEditor-txt pre{border:1px solid #ccc;background-color:#f8f8f8;padding:10px;margin:5px 0;font-size:.8em;border-radius:3px}.txt-toolbar,.wangEditor-drop-list,.wangEditor-drop-panel{z-index:10;border-left:1px\9 solid\9 #ccc\9;border-bottom:1px\9 solid\9 #999\9;border-right:1px\9 solid\9 #999\9;box-shadow:1px 3px 3px #999;position:absolute}.wangEditor-drop-list{display:none;background-color:#fff;overflow:hidden;transition:height .7s;border-top:1px solid #f1f1f1}.wangEditor-drop-list a{display:block;color:#666;padding:3px 5px}.wangEditor-drop-list a:hover{background-color:#f1f1f1}.txt-toolbar,.wangEditor-drop-panel{display:none;padding:10px;font-size:14px;background-color:#fff;border-top:2px solid #666}.txt-toolbar .tip-triangle,.wangEditor-drop-panel .tip-triangle{display:block;position:absolute;width:0;height:0;border:5px solid;border-color:transparent transparent #666;top:-12px;left:50%;margin-left:-5px}.txt-toolbar input[type=text],.wangEditor-drop-panel input[type=text]{border:none;border-bottom:1px solid #ccc;font-size:14px;height:20px;color:#333;padding:3px 0}.txt-toolbar input[type=text]:focus,.wangEditor-drop-panel input[type=text]:focus{outline:0;border-bottom:2px solid #1e88e5}.txt-toolbar input[type=text].block,.wangEditor-drop-panel input[type=text].block{display:block;width:100%}.txt-toolbar textarea,.wangEditor-drop-panel textarea{border:1px solid #ccc}.txt-toolbar textarea:focus,.wangEditor-drop-panel textarea:focus{outline:0;border-color:#1e88e5}.txt-toolbar button,.wangEditor-drop-panel button{font-size:14px;color:#1e88e5;border:none;padding:10px;background-color:#fff;cursor:pointer;border-radius:3px}.txt-toolbar button:hover,.wangEditor-drop-panel button:hover{background-color:#f1f1f1}.txt-toolbar button:focus,.wangEditor-drop-panel button:focus{outline:0}.txt-toolbar button.right,.wangEditor-drop-panel button.right{float:right;margin-left:10px}.txt-toolbar button.gray,.wangEditor-drop-panel button.gray{color:#999}.txt-toolbar button.link,.wangEditor-drop-panel button.link{padding:5px 10px}.txt-toolbar button.link:hover,.wangEditor-drop-panel button.link:hover{background-color:#fff;text-decoration:underline}.txt-toolbar .color-item:hover,.txt-toolbar .list-menu-item:hover,.wangEditor-drop-panel .color-item:hover,.wangEditor-drop-panel .list-menu-item:hover{background-color:#f1f1f1}.txt-toolbar .color-item,.wangEditor-drop-panel .color-item{display:block;float:left;width:25px;height:25px;text-align:center;padding:2px;border-radius:2px;text-decoration:underline}.txt-toolbar .list-menu-item,.wangEditor-drop-panel .list-menu-item{display:block;float:left;color:#333;padding:5px;border-radius:2px}.txt-toolbar table.choose-table,.wangEditor-drop-panel table.choose-table{border:none;border-collapse:collapse}.txt-toolbar table.choose-table td,.wangEditor-drop-panel table.choose-table td{border:1px solid #ccc;width:16px;height:12px}.txt-toolbar table.choose-table td.active,.wangEditor-drop-panel table.choose-table td.active{background-color:#ccc;opacity:.5;filter:alpha(opacity=50)}.txt-toolbar .panel-tab .tab-container,.wangEditor-drop-panel .panel-tab .tab-container{margin-bottom:5px}.txt-toolbar .panel-tab .tab-container a,.wangEditor-drop-panel .panel-tab .tab-container a{display:inline-block;color:#999;text-align:center;margin:0 5px;padding:5px}.txt-toolbar .panel-tab .tab-container a.selected,.wangEditor-drop-panel .panel-tab .tab-container a.selected{color:#1e88e5;border-bottom:2px solid #1e88e5}.txt-toolbar .panel-tab .content-container .content,.wangEditor-drop-panel .panel-tab .content-container .content{display:none}.txt-toolbar .panel-tab .content-container .content a,.wangEditor-drop-panel .panel-tab .content-container .content a{display:inline-block;margin:2px;padding:2px;border-radius:2px}.txt-toolbar .panel-tab .content-container .content a:hover,.wangEditor-drop-panel .panel-tab .content-container .content a:hover{background-color:#f1f1f1}.txt-toolbar .panel-tab .content-container .selected,.wangEditor-drop-panel .panel-tab .content-container .selected{display:block}.txt-toolbar .panel-tab .emotion-content-container,.wangEditor-drop-panel .panel-tab .emotion-content-container{height:200px;overflow-y:auto}.txt-toolbar .upload-icon-container,.wangEditor-drop-panel .upload-icon-container{color:#ccc;text-align:center;margin:20px 20px 15px!important;padding:5px!important;font-size:65px;cursor:pointer;border:2px dotted #f1f1f1;display:block!important}.txt-toolbar .upload-icon-container:hover,.wangEditor-drop-panel .upload-icon-container:hover{color:#666;border-color:#ccc}.wangEditor-modal{position:absolute;top:50%;left:50%;background-color:#fff;border-top:1px solid #f1f1f1;box-shadow:1px 3px 3px #999;border-top:1px\9 solid\9 #ccc\9;border-left:1px\9 solid\9 #ccc\9;border-bottom:1px\9 solid\9 #999\9;border-right:1px\9 solid\9 #999\9}.wangEditor-modal .wangEditor-modal-close{position:absolute;top:0;right:0;margin-top:-25px;margin-right:-25px;font-size:1.5em;color:#666;cursor:pointer}@font-face{font-family:icomoon;src:url(../fonts/icomoon.eot?-qdfu1s);src:url(../fonts/icomoon.eot?#iefix-qdfu1s) format('embedded-opentype'),url(../fonts/icomoon.ttf?-qdfu1s) format('truetype'),url(../fonts/icomoon.woff?-qdfu1s) format('woff'),url(../fonts/icomoon.svg?-qdfu1s#icomoon) format('svg');font-weight:400;font-style:normal}[class*=" wangeditor-menu-img-"],[class^=wangeditor-menu-img-]{font-family:icomoon;speak:none;font-style:normal;font-weight:400;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.wangeditor-menu-img-link:before{content:"\e800"}.wangeditor-menu-img-unlink:before{content:"\e801"}.wangeditor-menu-img-code:before{content:"\e802"}.wangeditor-menu-img-cancel:before{content:"\e803"}.wangeditor-menu-img-terminal:before{content:"\e804"}.wangeditor-menu-img-angle-down:before{content:"\e805"}.wangeditor-menu-img-font:before{content:"\e806"}.wangeditor-menu-img-bold:before{content:"\e807"}.wangeditor-menu-img-italic:before{content:"\e808"}.wangeditor-menu-img-header:before{content:"\e809"}.wangeditor-menu-img-align-left:before{content:"\e80a"}.wangeditor-menu-img-align-center:before{content:"\e80b"}.wangeditor-menu-img-align-right:before{content:"\e80c"}.wangeditor-menu-img-list-bullet:before{content:"\e80d"}.wangeditor-menu-img-indent-left:before{content:"\e80e"}.wangeditor-menu-img-indent-right:before{content:"\e80f"}.wangeditor-menu-img-list-numbered:before{content:"\e810"}.wangeditor-menu-img-underline:before{content:"\e811"}.wangeditor-menu-img-table:before{content:"\e812"}.wangeditor-menu-img-eraser:before{content:"\e813"}.wangeditor-menu-img-text-height:before{content:"\e814"}.wangeditor-menu-img-brush:before{content:"\e815"}.wangeditor-menu-img-pencil:before{content:"\e816"}.wangeditor-menu-img-minus:before{content:"\e817"}.wangeditor-menu-img-picture:before{content:"\e818"}.wangeditor-menu-img-file-image:before{content:"\e819"}.wangeditor-menu-img-cw:before{content:"\e81a"}.wangeditor-menu-img-ccw:before{content:"\e81b"}.wangeditor-menu-img-music:before{content:"\e911"}.wangeditor-menu-img-play:before{content:"\e912"}.wangeditor-menu-img-location:before{content:"\e947"}.wangeditor-menu-img-happy:before{content:"\e9df"}.wangeditor-menu-img-sigma:before{content:"\ea67"}.wangeditor-menu-img-enlarge2:before{content:"\e98b"}.wangeditor-menu-img-shrink2:before{content:"\e98c"}.wangeditor-menu-img-newspaper:before{content:"\e904"}.wangeditor-menu-img-camera:before{content:"\e90f"}.wangeditor-menu-img-video-camera:before{content:"\e914"}.wangeditor-menu-img-file-zip:before{content:"\e92b"}.wangeditor-menu-img-stack:before{content:"\e92e"}.wangeditor-menu-img-credit-card:before{content:"\e93f"}.wangeditor-menu-img-address-book:before{content:"\e944"}.wangeditor-menu-img-envelop:before{content:"\e945"}.wangeditor-menu-img-drawer:before{content:"\e95c"}.wangeditor-menu-img-download:before{content:"\e960"}.wangeditor-menu-img-upload:before{content:"\e961"}.wangeditor-menu-img-lock:before{content:"\e98f"}.wangeditor-menu-img-unlocked:before{content:"\e990"}.wangeditor-menu-img-wrench:before{content:"\e991"}.wangeditor-menu-img-eye:before{content:"\e9ce"}.wangeditor-menu-img-eye-blocked:before{content:"\e9d1"}.wangeditor-menu-img-command:before{content:"\ea4e"}.wangeditor-menu-img-font2:before{content:"\ea5c"}.wangeditor-menu-img-libreoffice:before{content:"\eade"}.wangeditor-menu-img-quotes-left:before{content:"\e977"}.wangeditor-menu-img-strikethrough:before{content:"\ea65"}.wangeditor-menu-img-desktop:before{content:"\f108"}.wangeditor-menu-img-tablet:before{content:"\f10a"}.wangeditor-menu-img-search-plus:before{content:"\f00e"}.wangeditor-menu-img-search-minus:before{content:"\f010"}.wangeditor-menu-img-trash-o:before{content:"\f014"}.wangeditor-menu-img-align-justify:before{content:"\f039"}.wangeditor-menu-img-arrows-v:before{content:"\f07d"}.wangeditor-menu-img-sigma2:before{content:"\ea68"}.wangeditor-menu-img-omega:before{content:"\e900"}.wangeditor-menu-img-cancel-circle:before{content:"\e901"}.hljs{display:block;overflow-x:auto;padding:.5em;color:#333;background:#f8f8f8;-webkit-text-size-adjust:none}.diff .hljs-header,.hljs-comment{color:#998;font-style:italic}.css .rule .hljs-keyword,.hljs-keyword,.hljs-request,.hljs-status,.hljs-subst,.hljs-winutils,.nginx .hljs-title{color:#333;font-weight:700}.hljs-hexcolor,.hljs-number,.ruby .hljs-constant{color:teal}.hljs-doctag,.hljs-string,.hljs-tag .hljs-value,.tex .hljs-formula{color:#d14}.hljs-id,.hljs-title,.scss .hljs-preprocessor{color:#900;font-weight:700}.hljs-list .hljs-keyword,.hljs-subst{font-weight:400}.hljs-class .hljs-title,.hljs-type,.tex .hljs-command,.vhdl .hljs-literal{color:#458;font-weight:700}.django .hljs-tag .hljs-keyword,.hljs-rule .hljs-property,.hljs-tag,.hljs-tag .hljs-title{color:navy;font-weight:400}.hljs-attribute,.hljs-name,.hljs-variable,.lisp .hljs-body{color:teal}.hljs-regexp{color:#009926}.clojure .hljs-keyword,.hljs-prompt,.hljs-symbol,.lisp .hljs-keyword,.ruby .hljs-symbol .hljs-string,.scheme .hljs-keyword,.tex .hljs-special{color:#990073}.hljs-built_in{color:#0086b3}.hljs-cdata,.hljs-doctype,.hljs-pi,.hljs-pragma,.hljs-preprocessor,.hljs-shebang{color:#999;font-weight:700}.hljs-deletion{background:#fdd}.hljs-addition{background:#dfd}.diff .hljs-change{background:#0086b3}.hljs-chunk{color:#aaa}
--------------------------------------------------------------------------------
/deploy/app/static/css/style.css:
--------------------------------------------------------------------------------
1 | .transition-all {
2 | -webkit-transition: all 0.5s;
3 | -moz-transition: all 0.5s;
4 | transition: all 0.5s;
5 | }
6 | .background-cover,
7 | .intro-header {
8 | -webkit-background-size: cover;
9 | -moz-background-size: cover;
10 | background-size: cover;
11 | -o-background-size: cover;
12 | }
13 | .serif,
14 | body,
15 | .intro-header .post-heading .meta {
16 | font-family: 'Lora', 'Times New Roman', serif;
17 | }
18 | .sans-serif,
19 | h1,
20 | h2,
21 | h3,
22 | h4,
23 | h5,
24 | h6,
25 | .navbar-custom,
26 | .btn,
27 | .pager li > a,
28 | .pager li > span {
29 | font-family: 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
30 | }
31 | body {
32 | font-size: 15px;
33 | color: #404040;
34 | }
35 |
36 | p {
37 | line-height: 1.7;
38 | margin: 1.7rem 0;
39 | font-size: 1.7rem;
40 | }
41 | p,
42 | li {
43 | line-height: 1.5;
44 | font-size: 1.5rem;
45 | }
46 | h1,
47 | h2,
48 | h3,
49 | h4,
50 | h5,
51 | h6 {
52 | font-weight: 400;
53 | }
54 | a {
55 | color: #404040;
56 | }
57 | a:hover,
58 | a:focus {
59 | color: #0085a1;
60 | }
61 | a img:hover,
62 | a img:focus {
63 | cursor: zoom-in;
64 | }
65 | hr {
66 | margin-top: 5px;
67 | margin-bottom: 5px;
68 | border: 0;
69 | border-top: 1px solid #b7b7b7;
70 | }
71 |
72 | label {
73 | display: inline-block;
74 | max-width: 100%; // Force IE8 to wrap long content (see https://github.com/twbs/bootstrap/issues/13141)
75 | margin-bottom: 5px;
76 | font-size: 16px;
77 | font-weight: normal;
78 | }
79 |
80 | .navbar-custom {
81 | font-size: 20px;
82 | position: fixed;
83 | top: 0;
84 | left: 0;
85 | width: 100%;
86 | z-index: 3;
87 | }
88 | .navbar-custom .navbar-right {
89 | margin-right: 0px;
90 | }
91 | .navbar-custom .navbar-brand {
92 | font-weight: 400;
93 | }
94 | .navbar-custom .nav li a {
95 | text-transform
96 | font-weight: 400;
97 | text-decoration: none;
98 | }
99 | @media only screen and (min-width: 768px) {
100 | .navbar-custom {
101 | background-color: #fafafb;
102 | border-bottom: 1px solid transparent;
103 | border-color: #e7e7e7;
104 | }
105 | .navbar-custom .container-fluid {
106 | width: 768px;
107 | }
108 | .navbar-custom .navbar-brand {
109 | color: rgba(0, 0, 0, 0.5);
110 | padding: 15px;
111 | font-weight: 700;
112 | font-family: 'PT Sans Caption', Helvetica, Arial, sans-serif;
113 | }
114 | .navbar-custom .navbar-brand:hover,
115 | .navbar-custom .navbar-brand:focus {
116 | color: rgba(0, 0, 0, 0.9);
117 | }
118 | .navbar-custom .nav li a {
119 | color: rgba(0, 0, 0, 0.5);
120 | padding: 10px;
121 | padding-top: 15px;
122 | padding-bottom: 15px;
123 | font-size: 1.7rem;
124 | }
125 | .navbar-custom .nav li a:hover,
126 | .navbar-custom .nav li a:focus {
127 | color: rgba(0, 0, 0, 0.7);
128 | font-weight: 500;
129 | display: inline-block;
130 | }
131 | }
132 | @media only screen and (min-width: 1170px) {
133 | .navbar-custom {
134 | -webkit-transition: background-color 0.3s;
135 | -moz-transition: background-color 0.3s;
136 | transition: background-color 0.3s;
137 | -webkit-transform: translate3d(0, 0, 0);
138 | -moz-transform: translate3d(0, 0, 0);
139 | -ms-transform: translate3d(0, 0, 0);
140 | -o-transform: translate3d(0, 0, 0);
141 | transform: translate3d(0, 0, 0);
142 | -webkit-backface-visibility: hidden;
143 | backface-visibility: hidden;
144 | }
145 | .navbar-custom .container-fluid {
146 | width: 800px;
147 | }
148 | .navbar-custom.is-fixed {
149 | /* when the user scrolls down, we hide the header right above the viewport */
150 | position: fixed;
151 | top: -61px;
152 | background-color: rgba(255,255,255,0.9);
153 | border-bottom: 1px solid #f2f2f2;
154 | -webkit-transition: -webkit-transform 0.3s;
155 | -moz-transition: -moz-transform 0.3s;
156 | transition: transform 0.3s;
157 | }
158 | .navbar-custom.is-fixed .navbar-brand {
159 | color: #404040;
160 | }
161 | .navbar-custom.is-fixed .navbar-brand:hover,
162 | .navbar-custom.is-fixed .navbar-brand:focus {
163 | color: #0085a1;
164 | }
165 | .navbar-custom.is-fixed .nav li a {
166 | color: #404040;
167 | }
168 | .navbar-custom.is-fixed .nav li a:hover,
169 | .navbar-custom.is-fixed .nav li a:focus {
170 | color: #0085a1;
171 | }
172 | .navbar-custom.is-visible {
173 | /* if the user changes the scrolling direction, we show the header */
174 | -webkit-transform: translate3d(0, 100%, 0);
175 | -moz-transform: translate3d(0, 100%, 0);
176 | -ms-transform: translate3d(0, 100%, 0);
177 | -o-transform: translate3d(0, 100%, 0);
178 | transform: translate3d(0, 100%, 0);
179 | }
180 | }
181 | .intro-header {
182 | background-color: #808080;
183 | background-position: center center;
184 | background-attachment: scroll;
185 | margin-bottom: 50px;
186 | }
187 | .intro-header .site-heading,
188 | .intro-header .post-heading,
189 | .intro-header .page-heading {
190 | padding: 100px 0 50px;
191 | color: #fff;
192 | }
193 | @media only screen and (min-width: 768px) {
194 | .intro-header .site-heading,
195 | .intro-header .post-heading,
196 | .intro-header .page-heading {
197 | padding: 150px 0;
198 | }
199 | }
200 | .intro-header .site-heading.archives,
201 | .intro-header .post-heading.archives,
202 | .intro-header .page-heading.archives {
203 | color: #0085a1;
204 | margin-top: 50px;
205 | }
206 | .intro-header .site-heading,
207 | .intro-header .page-heading {
208 | text-align: center;
209 | }
210 | .intro-header .site-heading h1,
211 | .intro-header .page-heading h1 {
212 | margin-top: 0;
213 | font-size: 50px;
214 | }
215 | .intro-header .post-heading h1 {
216 | font-size: 35px;
217 | }
218 | .intro-header .post-heading .meta {
219 | font-style: italic;
220 | font-weight: 300;
221 | font-size: 20px;
222 | }
223 | .intro-header .post-heading .meta a {
224 | color: #fff;
225 | }
226 | @media only screen and (min-width: 768px) {
227 | .intro-header .post-heading h1 {
228 | font-size: 55px;
229 | }
230 | }
231 | .post-preview > a {
232 | color: #404040;
233 | }
234 | .post-preview > a:hover,
235 | .post-preview > a:focus {
236 | text-decoration: none;
237 | color: #404040;
238 | }
239 | .post-preview > a > .post-title {
240 | font-size: 30px;
241 | margin-top: 10px;
242 | margin-bottom: 5px;
243 | }
244 | .post-preview > a > .post-title.archive {
245 | font-size: 22px;
246 | }
247 | .post-preview > a > .post-subtitle {
248 | margin: 0;
249 | font-weight: 300;
250 | margin-bottom: 10px;
251 | }
252 | .post-preview > .post-tag {
253 | display: inline-block;
254 | padding: 0.4rem 0.4rem;
255 | font-size: 1.5rem;
256 | line-height: 0.9;
257 | color: #ffffff;
258 | text-align: center;
259 | white-space: nowrap;
260 | vertical-align: baseline;
261 | border-radius: 0.25em;
262 | background-color: #31b0d5;
263 | border: 1.5px solid #1bb3e9;
264 | }
265 |
266 | .panel-body > .post-tag {
267 | display: inline-block;
268 | padding: 0.4rem 0.4rem;
269 | font-size: 1.5rem;
270 | line-height: 0.9;
271 | color: #ffffff;
272 | text-align: center;
273 | white-space: nowrap;
274 | vertical-align: baseline;
275 | border-radius: 0.25em;
276 | background-color: #31b0d5;
277 | border: 1.5px solid #1bb3e9;
278 | }
279 | .panel-body> .post-red-tag {
280 | display: inline-block;
281 | padding: 0.4rem 0.4rem;
282 | font-size: 1.5rem;
283 | line-height: 0.9;
284 | color: #ffffff;
285 | text-align: center;
286 | white-space: nowrap;
287 | vertical-align: baseline;
288 | border-radius: 0.25em;
289 | background-color: #a94442;
290 | border: 1.5px solid #a94442;
291 | }
292 | .post-preview > .post-edit-tag {
293 | display: inline-block;
294 | padding: 0.4rem 0.4rem;
295 | font-size: 1.5rem;
296 | line-height: 0.9;
297 | color: #ffffff;
298 | text-align: center;
299 | white-space: nowrap;
300 | vertical-align: baseline;
301 | border-radius: 0.25em;
302 | background-color: #a94442;
303 | border: 1.5px solid #a94442;
304 | float: right;
305 | margin: -3rem 0;
306 | }
307 | .post-preview > .post-red-tag {
308 | display: inline-block;
309 | padding: 0.4rem 0.4rem;
310 | font-size: 1.5rem;
311 | line-height: 0.9;
312 | color: #ffffff;
313 | text-align: center;
314 | white-space: nowrap;
315 | vertical-align: baseline;
316 | border-radius: 0.25em;
317 | background-color: #a94442;
318 | border: 1.5px solid #a94442;
319 | }
320 |
321 | .post-preview > .post-meta {
322 | color: #808080;
323 | font-size: 15px;
324 | margin-top: 0;
325 | margin-bottom: 0;
326 | direction: rtl;
327 | }
328 | .post-preview > .post-meta.archive {
329 | font-size: 16px;
330 | }
331 | .post-preview > .post-meta > a {
332 | text-decoration: none;
333 | color: #404040;
334 | }
335 | .post-preview > .post-meta > a:hover,
336 | .post-preview > .post-meta > a:focus {
337 | color: #0085a1;
338 | }
339 | @media only screen and (min-width: 768px) {
340 | .post-preview > a > .post-title {
341 | font-size: 36px;
342 | }
343 | }
344 | .section-heading {
345 | font-size: 36px;
346 | margin-top: 60px;
347 | font-weight: 700;
348 | }
349 | .caption {
350 | text-align: center;
351 | font-size: 14px;
352 | padding: 10px;
353 | font-style: italic;
354 | margin: 0;
355 | display: block;
356 | border-bottom-right-radius: 5px;
357 | border-bottom-left-radius: 5px;
358 | }
359 | .post-tags,
360 | .post-categories {
361 | font-size: 15px;
362 | margin-top: 30px;
363 | margin-bottom: 30px;
364 | }
365 | .post-tags a,
366 | .post-categories a {
367 | border-bottom: 1px dotted #ababab;
368 | text-transform: uppercase;
369 | margin-right: 10px;
370 | }
371 | .post-tags a:hover,
372 | .post-categories a:hover,
373 | .post-tags a:visited,
374 | .post-categories a:visited,
375 | .post-tags a:active,
376 | .post-categories a:active {
377 | text-decoration: none;
378 | border-bottom: none;
379 | }
380 | .archive-before-pagination {
381 | height: 30px;
382 | }
383 | footer {
384 | padding: 50px 0 65px;
385 | }
386 | footer .list-inline {
387 | margin: 0;
388 | padding: 0;
389 | }
390 | footer .copyright {
391 | font-size: 14px;
392 | text-align: center;
393 | margin-bottom: 0;
394 | }
395 | .floating-label-form-group {
396 | font-size: 14px;
397 | position: relative;
398 | margin-bottom: 0;
399 | padding-bottom: 0.5em;
400 | border-bottom: 1px solid #eee;
401 | }
402 | .floating-label-form-group input,
403 | .floating-label-form-group textarea {
404 | z-index: 1;
405 | position: relative;
406 | padding-right: 0;
407 | padding-left: 0;
408 | border: none;
409 | border-radius: 0;
410 | font-size: 1.5em;
411 | background: none;
412 | box-shadow: none !important;
413 | resize: none;
414 | }
415 | .floating-label-form-group label {
416 | display: block;
417 | z-index: 0;
418 | position: relative;
419 | top: 2em;
420 | margin: 0;
421 | font-size: 0.85em;
422 | line-height: 1.764705882em;
423 | vertical-align: middle;
424 | vertical-align: baseline;
425 | opacity: 0;
426 | -webkit-transition: top 0.3s ease, opacity 0.3s ease;
427 | -moz-transition: top 0.3s ease, opacity 0.3s ease;
428 | -ms-transition: top 0.3s ease, opacity 0.3s ease;
429 | transition: top 0.3s ease, opacity 0.3s ease;
430 | }
431 | .floating-label-form-group::not(:first-child) {
432 | padding-left: 14px;
433 | border-left: 1px solid #eee;
434 | }
435 | .floating-label-form-group-with-value label {
436 | top: 0;
437 | opacity: 1;
438 | }
439 | .floating-label-form-group-with-focus label {
440 | color: #0085a1;
441 | }
442 | form .row:first-child .floating-label-form-group {
443 | border-top: 1px solid #eee;
444 | }
445 | .btn {
446 | text-transform: uppercase;
447 | font-size: 16px;
448 | font-weight: normal;
449 | letter-spacing: 1px;
450 | border-radius: 0;
451 | }
452 | .btn-lg {
453 | font-size: 16px;
454 | padding: 25px 35px;
455 | }
456 | .btn-default:hover,
457 | .btn-default:focus {
458 | background-color: #0085a1;
459 | border: 1px solid #0085a1;
460 | color: #fff;
461 | }
462 | .pager {
463 | margin: 20px 0 0;
464 | }
465 | .pager li > a,
466 | .pager li > span {
467 | text-transform: uppercase;
468 | font-size: 14px;
469 | font-weight: 800;
470 | letter-spacing: 1px;
471 | padding: 15px 25px;
472 | background-color: #fff;
473 | border-radius: 0;
474 | }
475 | .pager li > a:hover,
476 | .pager li > a:focus {
477 | color: #fff;
478 | background-color: #0085a1;
479 | border: 1px solid #0085a1;
480 | }
481 | .pager .disabled > a,
482 | .pager .disabled > a:hover,
483 | .pager .disabled > a:focus,
484 | .pager .disabled > span {
485 | color: #808080;
486 | background-color: #404040;
487 | cursor: not-allowed;
488 | }
489 | ::-moz-selection {
490 | color: #fff;
491 | text-shadow: none;
492 | background: #0085a1;
493 | }
494 | ::selection {
495 | color: #fff;
496 | text-shadow: none;
497 | background: #0085a1;
498 | }
499 | img::selection {
500 | color: #fff;
501 | background: transparent;
502 | }
503 | img::-moz-selection {
504 | color: #fff;
505 | background: transparent;
506 | }
507 | body {
508 | webkit-tap-highlight-color: #0085a1;
509 | }
510 | article .container .row img {
511 | max-width: 100% !important;
512 | height: auto;
513 | display: inline-block;
514 | }
515 | article .container .row .video-container {
516 | position: relative;
517 | padding-bottom: 56.25%;
518 | padding-top: 30px;
519 | height: 0;
520 | overflow: hidden;
521 | }
522 | article .container .row .video-container iframe,
523 | article .container .row .video-container object,
524 | article .container .row .video-container embed {
525 | position: absolute;
526 | top: 0;
527 | left: 0;
528 | width: 100%;
529 | height: 100%;
530 | margin-top: 0;
531 | }
532 | article .container .row blockquote {
533 | font-style: italic;
534 | font-family: "Georgia", serif;
535 | font-size: 1.2em;
536 | padding: 0 30px 15px;
537 | text-align: center;
538 | }
539 | article .container .row blockquote footer {
540 | border-top: none;
541 | font-size: 0.8em;
542 | line-height: 1;
543 | margin: 20px 0 0;
544 | padding-top: 0;
545 | }
546 | article .container .row blockquote footer cite:before {
547 | content: '—';
548 | color: #ccc;
549 | padding: 0 0.5em;
550 | }
551 | article .container .row .pullquote {
552 | float: right;
553 | border: none;
554 | padding: 0;
555 | margin: 1em 0 0.5em 1.5em;
556 | text-align: left;
557 | width: 45%;
558 | font-size: 1.5em;
559 | }
560 | article .container .row .pullquote.left {
561 | float: left;
562 | }
563 | figure.highlight {
564 | background: #eee;
565 | border: 1px solid #ddd;
566 | margin-top: 15px;
567 | padding: 7px 15px;
568 | border-radius: 2px;
569 | text-shadow: 0 0 1px #fff;
570 | line-height: 1.6;
571 | overflow: auto;
572 | position: relative;
573 | font-size: 0.9em;
574 | }
575 | figure.highlight figcaption {
576 | color: #999;
577 | font-family: Monaco, Menlo, Consolas, Courier New, monospace;
578 | margin-bottom: 5px;
579 | text-shadow: 0 0 1px #fff;
580 | }
581 | figure.highlight figcaption a {
582 | position: absolute;
583 | right: 15px;
584 | }
585 | figure.highlight pre {
586 | border: none;
587 | padding: 0;
588 | margin: 0;
589 | background: none;
590 | }
591 | figure.highlight table {
592 | margin-top: 0;
593 | border-spacing: 0;
594 | }
595 | figure.highlight table .gutter {
596 | color: #999;
597 | padding-right: 15px;
598 | border-right: 1px solid #ddd;
599 | text-align: right;
600 | }
601 | figure.highlight table .code {
602 | padding-left: 15px;
603 | border-left: 1px solid #fff;
604 | color: #666;
605 | }
606 | figure.highlight table .line {
607 | height: 20px;
608 | }
609 | figure.highlight table .line.marked {
610 | background: #d6d6d6;
611 | }
612 | pre .comment,
613 | pre .template_comment,
614 | pre .diff .header,
615 | pre .doctype,
616 | pre .pi,
617 | pre .lisp .string,
618 | pre .javadoc {
619 | color: #93a1a1;
620 | font-style: italic;
621 | }
622 | pre .keyword,
623 | pre .winutils,
624 | pre .method,
625 | pre .addition,
626 | pre .css .tag,
627 | pre .request,
628 | pre .status,
629 | pre .nginx .title {
630 | color: #859900;
631 | }
632 | pre .number,
633 | pre .command,
634 | pre .string,
635 | pre .tag .value,
636 | pre .phpdoc,
637 | pre .tex .formula,
638 | pre .regexp,
639 | pre .hexcolor {
640 | color: #2aa198;
641 | }
642 | pre .title,
643 | pre .localvars,
644 | pre .chunk,
645 | pre .decorator,
646 | pre .built_in,
647 | pre .identifier,
648 | pre .vhdl,
649 | pre .literal,
650 | pre .id {
651 | color: #268bd2;
652 | }
653 | pre .attribute,
654 | pre .variable,
655 | pre .lisp .body,
656 | pre .smalltalk .number,
657 | pre .constant,
658 | pre .class .title,
659 | pre .parent,
660 | pre .haskell .type {
661 | color: #b58900;
662 | }
663 | pre .preprocessor,
664 | pre .preprocessor .keyword,
665 | pre .shebang,
666 | pre .symbol,
667 | pre .symbol .string,
668 | pre .diff .change,
669 | pre .special,
670 | pre .attr_selector,
671 | pre .important,
672 | pre .subst,
673 | pre .cdata,
674 | pre .clojure .title {
675 | color: #cb4b16;
676 | }
677 | pre .deletion {
678 | color: #dc322f;
679 | }
680 |
--------------------------------------------------------------------------------
/deploy/app/report/views.py:
--------------------------------------------------------------------------------
1 | #coding:utf-8
2 | from flask import render_template, redirect,request, url_for, \
3 | current_app, flash, Markup
4 | from flask_babelex import lazy_gettext as _
5 | from flask_login import current_user
6 | from datetime import datetime, timedelta, date
7 | from . import report
8 | from .forms import WriteForm, ReadDepartmentForm, \
9 | ReadCrewForm, EmailReminderForm
10 | from .. import db
11 | from ..email import send_email
12 | from ..models import Permission, User, Report, Department
13 | from ..utils import get_week_count, permission_required, get_this_monday, \
14 | get_last_week, get_last_week_start_at, get_last_week_end_at, get_last_week_content
15 |
16 |
17 | @report.route('/write/', methods=['GET', 'POST'])
18 | @permission_required(Permission.WRITE_REPORT)
19 | def write():
20 | form = WriteForm()
21 | last_content_display = ""
22 | report = Report.query.filter_by(
23 | author_id=current_user.id,
24 | week_count=get_week_count(),
25 | year=datetime.today().year
26 | ).first()
27 |
28 | last_report = Report.query.filter_by(
29 | author_id=current_user.id,
30 | week_count=get_week_count() - 1,
31 | year=datetime.today().year
32 | ).first()
33 |
34 | if form.submit.data and form.validate_on_submit():
35 | if report:
36 | report.content = form.body.data.replace('
', '')
37 | report.last_content = form.last_content.data.replace('
', '')
38 | db.session.add(report)
39 | else:
40 | report = Report(
41 | content=form.body.data.replace('
', ''),
42 | last_content=form.last_content.data.replace('
', ''),
43 | author_id=current_user.id,
44 | week_count=get_week_count(),
45 | year=datetime.today().year)
46 | db.session.add(report)
47 | db.session.commit()
48 | flash(_('Successfully submitted report'))
49 |
50 | current_app.logger.info(
51 | '{} submitted report'.format(current_user.email))
52 |
53 | return redirect(url_for('report.write'))
54 |
55 | if report:
56 | form.body.data = report.content
57 | else:
58 | form.body.data = current_app.config['DEFAULT_CONTENT']
59 |
60 | if last_report:
61 | form.last_content.data = last_report.content
62 | last_content_display = get_last_week_content(last_report.content)
63 |
64 | return render_template('report/write.html',
65 | form=form,
66 | week_count=get_week_count(),
67 | start_at=get_this_monday(),
68 | end_at=get_this_monday()+timedelta(days=6),
69 | last_content_display=last_content_display)
70 |
71 |
72 | @report.route('/write/last_week', methods=['GET', 'POST'])
73 | @permission_required(Permission.WRITE_REPORT)
74 | def write_last_week():
75 | form = WriteForm()
76 | last_content_display = ""
77 |
78 | report = Report.query.filter_by(
79 | author_id=current_user.id,
80 | week_count=get_week_count(get_last_week()),
81 | year=get_last_week().year).first()
82 |
83 | last_report = Report.query.filter_by(
84 | author_id=current_user.id,
85 | week_count=get_week_count(get_last_week()) - 1,
86 | year=get_last_week().year).first()
87 |
88 | if form.submit.data and form.validate_on_submit():
89 | if report:
90 | report.content = form.body.data.replace('
', '')
91 | report.last_content = form.last_content.data.replace('
', '')
92 | else:
93 | report = Report(
94 | content=form.body.data.replace('
', ''),
95 | author_id=current_user.id,
96 | week_count=get_week_count(get_last_week()),
97 | year=get_last_week().year)
98 | db.session.add(report)
99 | db.session.commit()
100 | flash(_('Successfully submitted report'))
101 |
102 | current_app.logger.info(
103 | "{} edited last week's report".format(current_user.email))
104 |
105 | return redirect(url_for('report.write_last_week'))
106 |
107 | if report:
108 | form.body.data = report.content
109 | else:
110 | form.body.data = current_app.config['DEFAULT_CONTENT']
111 |
112 | if last_report:
113 | form.last_content.data = last_report.content
114 | last_content_display = get_last_week_content(last_report.content)
115 |
116 | return render_template('report/write.html',
117 | form=form,
118 | week_count=get_week_count(get_last_week()),
119 | start_at=get_last_week_start_at(),
120 | end_at=get_last_week_end_at() - timedelta(days=1),
121 | last_content_display=last_content_display)
122 |
123 |
124 | @report.route('/read/', methods=['GET'])
125 | @report.route('/read/', methods=['GET'])
126 | @permission_required(Permission.WRITE_REPORT)
127 | def read(page_count=1):
128 | if not Report.query.filter_by(
129 | author_id=current_user.id,
130 | week_count=get_week_count(get_last_week()),
131 | year=get_last_week().year).first():
132 | flash(Markup(_("Do you want to "
133 | "edit last week's report?")))
134 |
135 | pagination = Report.query.filter_by(author_id=current_user.id).order_by(
136 | Report.year.desc()).order_by(Report.week_count.desc()).paginate(
137 | page=page_count, per_page=current_app.config['PER_PAGE'])
138 | if not Report.query.filter_by(
139 | author_id=current_user.id,
140 | week_count=get_week_count(),
141 | year=datetime.today().year):
142 | flash(_("You haven't submitted your weekly report"))
143 | return render_template('report/read.html', pagination=pagination)
144 |
145 |
146 | @report.route('/read/department/', methods=['GET', 'POST'])
147 | @permission_required(Permission.READ_DEPARTMENT_REPORT)
148 | def read_department():
149 | form = ReadDepartmentForm()
150 |
151 | user_choices = [('0', '*')]
152 | user_choices.extend([(
153 | str(user.id), user.username) for user in User.query.all()])
154 | form.user.choices = user_choices
155 |
156 | page = request.args.get('page', 1, type=int)
157 | user_id = request.args.get('user', 0, type=int)
158 | start_at = request.args.get('start_at', '', type=str)
159 | end_at = request.args.get('end_at', '', type=str)
160 |
161 | start_at = get_last_week_start_at() if not start_at \
162 | else datetime.strptime(start_at[:10], '%Y-%m-%d')
163 | end_at = date.today()+timedelta(hours=24) if not end_at \
164 | else datetime.strptime(end_at[:10], '%Y-%m-%d')
165 |
166 | form.start_at.data = start_at
167 | form.end_at.data = end_at
168 | form.user.data = str(user_id)
169 |
170 | ids = [user.id for user in User.query.filter_by(
171 | department_id=current_user.department_id)]
172 |
173 | qst = Report.query.filter_by().filter(
174 | Report.created_at.between(start_at, end_at)).filter(
175 | Report.author_id.in_(ids))
176 |
177 | if user_id:
178 | qst = qst.filter_by(author_id=user_id)
179 |
180 | if form.validate_on_submit():
181 | pass
182 |
183 | pagination = qst.filter_by().order_by(Report.year.desc()).order_by(
184 | Report.week_count.desc()).order_by(Report.created_at.desc()).paginate(
185 | page=page, per_page=current_app.config['PER_PAGE'])
186 |
187 | return render_template('report/read_department.html',
188 | form=form,
189 | pagination=pagination)
190 |
191 |
192 | @report.route('/read/crew/', methods=['GET', 'POST'])
193 | @permission_required(Permission.READ_ALL_REPORT)
194 | def read_crew():
195 | form = ReadCrewForm()
196 | user_choices = [('0', '*')]
197 | department_choices = user_choices[:]
198 |
199 | for dept in Department.query.all():
200 | department_choices.extend([(str(dept.id), dept.name)])
201 | user_choices.extend([(str(user.id), user.username) for user in
202 | User.query.filter_by(department_id=dept.id)])
203 |
204 | form.user.choices = user_choices
205 | form.department.choices = department_choices
206 |
207 | page = request.args.get('page', 1, type=int)
208 | department_id = request.args.get('department', 0, type=int)
209 | user_id = request.args.get('user', 0, type=int)
210 | start_at = request.args.get('start_at', '', type=str)
211 | end_at = request.args.get('end_at', '', type=str)
212 |
213 | start_at = get_last_week_start_at() if not start_at \
214 | else datetime.strptime(start_at[:10], '%Y-%m-%d')
215 | end_at = date.today()+timedelta(hours=24) if not end_at \
216 | else datetime.strptime(end_at[:10], '%Y-%m-%d')
217 |
218 | form.start_at.data = start_at
219 | form.end_at.data = end_at
220 | form.user.data = str(user_id)
221 | form.department.data = str(department_id)
222 |
223 | qst = Report.query.filter_by().filter(
224 | Report.created_at.between(start_at, end_at))
225 |
226 | if department_id:
227 | ids = [user.id for user in User.query.filter_by(
228 | department_id=department_id)]
229 | qst = qst.filter(Report.author_id.in_(ids))
230 |
231 | if user_id:
232 | qst = qst.filter_by(author_id=user_id)
233 |
234 | if form.validate_on_submit():
235 | pass
236 |
237 | pagination = qst.filter_by().order_by(Report.year.desc()).order_by(
238 | Report.week_count.desc()).order_by(Report.created_at.desc()).paginate(
239 | page=page, per_page=current_app.config['PER_PAGE'])
240 |
241 | return render_template('report/read_crew.html',
242 | form=form,
243 | pagination=pagination)
244 |
245 |
246 | @report.route('/statistics/department/', methods=['GET'])
247 | @permission_required(Permission.READ_DEPARTMENT_REPORT)
248 | def statistics_department():
249 | qst = Report.query.filter_by()
250 | dept_users = [user for user in User.query.filter_by(
251 | department_id=current_user.department_id) if not user.is_ignored]
252 | ids = [user.id for user in dept_users]
253 | if ids:
254 | qst = qst.filter(Report.author_id.in_(ids))
255 | else:
256 | qst = qst.filter(False)
257 |
258 | submitted_users = [
259 | report.author for report in qst.filter_by(
260 | week_count=get_week_count(),
261 | year=datetime.today().year)]
262 | unsubmitted_users = set(dept_users) - set(submitted_users)
263 |
264 | data = {'已交': len(submitted_users),
265 | '未交': len(unsubmitted_users)}
266 | names = {'has_submitted': [user.username for user in submitted_users],
267 | 'not_yet': [user.username for user in unsubmitted_users]}
268 |
269 | return render_template('report/statistics_department.html',
270 | data=data,
271 | names=names,
272 | week_count=get_week_count(),
273 | start_at=get_this_monday(),
274 | end_at=get_this_monday() + timedelta(days=6))
275 |
276 |
277 | @report.route('/statistics/department/last_week', methods=['GET'])
278 | @permission_required(Permission.READ_DEPARTMENT_REPORT)
279 | def statistics_department_last_week():
280 | qst = Report.query.filter_by()
281 | dept_users = [user for user in User.query.filter_by(
282 | department_id=current_user.department_id) if not user.is_ignored]
283 | ids = [user.id for user in dept_users]
284 | if ids:
285 | qst = qst.filter(Report.author_id.in_(ids))
286 | else:
287 | qst = qst.filter(False)
288 |
289 | submitted_users = [
290 | report.author for report in qst.filter_by(
291 | week_count=get_week_count(get_last_week()),
292 | year=get_last_week().year)]
293 | unsubmitted_users = set(dept_users) - set(submitted_users)
294 |
295 | data = {'已交': len(submitted_users),
296 | '未交': len(unsubmitted_users)}
297 | names = {'has_submitted': [user.username for user in submitted_users],
298 | 'not_yet': [user.username for user in unsubmitted_users]}
299 |
300 | return render_template('report/statistics_department.html',
301 | data=data,
302 | names=names,
303 | week_count=get_week_count(get_last_week()),
304 | start_at=get_last_week_start_at(),
305 | end_at=get_last_week_end_at() - timedelta(days=1))
306 |
307 |
308 | @report.route('/statistics/crew/', methods=['GET', 'POST'])
309 | @permission_required(Permission.READ_ALL_REPORT)
310 | def statistics_crew():
311 | stash = []
312 | contrast = {}
313 | reminder_emails = set()
314 | form = EmailReminderForm()
315 | for dept in Department.query.filter_by():
316 | qst = Report.query.filter_by()
317 | dept_users = [user for user in User.query.filter_by(
318 | department_id=dept.id) if not user.is_ignored]
319 | ids = [user.id for user in dept_users]
320 | if ids:
321 | qst = qst.filter(Report.author_id.in_(ids))
322 | else:
323 | qst = qst.filter(False)
324 |
325 | submitted_users = [
326 | report.author for report in qst.filter_by(
327 | week_count=get_week_count(),
328 | year=datetime.today().year)]
329 |
330 | unsubmitted_users = set(dept_users)-set(submitted_users)
331 | reminder_emails |= set([user.email for user in unsubmitted_users])
332 |
333 | names = {'has_submitted': [user.username for user in submitted_users],
334 | 'not_yet': [user.username for user in unsubmitted_users]}
335 |
336 | stash.append({'names': names,
337 | 'dept_name': dept.name})
338 |
339 | contrast[dept.name] = len(dept_users) - len(submitted_users)
340 |
341 | if form.validate_on_submit():
342 | subject = 'Reminder of Report of week' + str(get_week_count()) + \
343 | ' From:' + str(get_this_monday()) + \
344 | ' To:' + str(get_this_monday() + timedelta(days=6))
345 | send_email(reminder_emails, subject,
346 | 'email/reminder',
347 | user=current_user,
348 | week_count=get_week_count(),
349 | start_at=get_this_monday(),
350 | end_at=get_this_monday() + timedelta(days=6))
351 | flash(_('Email has been sent to:') + '\n{}'.format(reminder_emails))
352 |
353 | return render_template('report/statistics_crew.html',
354 | contrast=contrast,
355 | stash=stash,
356 | week_count=get_week_count(),
357 | form=form,
358 | start_at=get_this_monday(),
359 | end_at=get_this_monday() + timedelta(days=6))
360 |
361 |
362 | @report.route('/statistics/crew/last_week', methods=['GET', 'POST'])
363 | @permission_required(Permission.READ_ALL_REPORT)
364 | def statistics_crew_last_week():
365 | stash = []
366 | contrast = {}
367 | reminder_emails = set()
368 | form = EmailReminderForm()
369 | for dept in Department.query.filter_by():
370 | qst = Report.query.filter_by()
371 | dept_users = [user for user in User.query.filter_by(
372 | department_id=dept.id) if not user.is_ignored]
373 | ids = [user.id for user in dept_users]
374 | if ids:
375 | qst = qst.filter(Report.author_id.in_(ids))
376 | else:
377 | qst = qst.filter(False)
378 |
379 | submitted_users = [
380 | report.author for report in qst.filter_by(
381 | week_count=get_week_count(get_last_week()),
382 | year=get_last_week().year)]
383 |
384 | unsubmitted_users = set(dept_users)-set(submitted_users)
385 | reminder_emails |= set([user.email for user in unsubmitted_users])
386 |
387 | names = {'has_submitted': [user.username for user in submitted_users],
388 | 'not_yet': [user.username for user in unsubmitted_users]}
389 |
390 | stash.append({'names': names,
391 | 'dept_name': dept.name})
392 | contrast[dept.name] = len(dept_users) - len(submitted_users)
393 |
394 | if form.validate_on_submit():
395 | subject = 'Reminder of Report of week' + str(get_week_count(get_last_week())) + \
396 | ' From:' + str(get_last_week_start_at()) + \
397 | ' To:' + str(get_last_week_end_at() - timedelta(days=1))
398 | send_email(reminder_emails, subject,
399 | 'email/reminder',
400 | user=current_user,
401 | week_count=get_week_count(get_last_week()),
402 | start_at=get_last_week_start_at(),
403 | end_at=get_last_week_end_at() - timedelta(days=1))
404 | flash(_('Email has been sent to:') + '\n{}'.format(reminder_emails))
405 | return render_template('report/statistics_crew.html',
406 | contrast=contrast,
407 | stash=stash,
408 | form=form,
409 | week_count=get_week_count(get_last_week()),
410 | start_at=get_last_week_start_at(),
411 | end_at=get_last_week_end_at() - timedelta(days=1))
412 |
--------------------------------------------------------------------------------
/deploy/app/static/css/font-awesome.min.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome
3 | * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License)
4 | */@font-face{font-family:'FontAwesome';src:url('../fonts/fontawesome-webfont.eot?v=4.7.0');src:url('../fonts/fontawesome-webfont.eot?#iefix&v=4.7.0') format('embedded-opentype'),url('../fonts/fontawesome-webfont.woff2?v=4.7.0') format('woff2'),url('../fonts/fontawesome-webfont.woff?v=4.7.0') format('woff'),url('../fonts/fontawesome-webfont.ttf?v=4.7.0') format('truetype'),url('../fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left{margin-right:.3em}.fa.fa-pull-right{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-remove:before,.fa-close:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook-f:before,.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-feed:before,.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before,.fa-gratipay:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-resistance:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-y-combinator-square:before,.fa-yc-square:before,.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"}.fa-buysellads:before{content:"\f20d"}.fa-connectdevelop:before{content:"\f20e"}.fa-dashcube:before{content:"\f210"}.fa-forumbee:before{content:"\f211"}.fa-leanpub:before{content:"\f212"}.fa-sellsy:before{content:"\f213"}.fa-shirtsinbulk:before{content:"\f214"}.fa-simplybuilt:before{content:"\f215"}.fa-skyatlas:before{content:"\f216"}.fa-cart-plus:before{content:"\f217"}.fa-cart-arrow-down:before{content:"\f218"}.fa-diamond:before{content:"\f219"}.fa-ship:before{content:"\f21a"}.fa-user-secret:before{content:"\f21b"}.fa-motorcycle:before{content:"\f21c"}.fa-street-view:before{content:"\f21d"}.fa-heartbeat:before{content:"\f21e"}.fa-venus:before{content:"\f221"}.fa-mars:before{content:"\f222"}.fa-mercury:before{content:"\f223"}.fa-intersex:before,.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-venus-double:before{content:"\f226"}.fa-mars-double:before{content:"\f227"}.fa-venus-mars:before{content:"\f228"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-neuter:before{content:"\f22c"}.fa-genderless:before{content:"\f22d"}.fa-facebook-official:before{content:"\f230"}.fa-pinterest-p:before{content:"\f231"}.fa-whatsapp:before{content:"\f232"}.fa-server:before{content:"\f233"}.fa-user-plus:before{content:"\f234"}.fa-user-times:before{content:"\f235"}.fa-hotel:before,.fa-bed:before{content:"\f236"}.fa-viacoin:before{content:"\f237"}.fa-train:before{content:"\f238"}.fa-subway:before{content:"\f239"}.fa-medium:before{content:"\f23a"}.fa-yc:before,.fa-y-combinator:before{content:"\f23b"}.fa-optin-monster:before{content:"\f23c"}.fa-opencart:before{content:"\f23d"}.fa-expeditedssl:before{content:"\f23e"}.fa-battery-4:before,.fa-battery:before,.fa-battery-full:before{content:"\f240"}.fa-battery-3:before,.fa-battery-three-quarters:before{content:"\f241"}.fa-battery-2:before,.fa-battery-half:before{content:"\f242"}.fa-battery-1:before,.fa-battery-quarter:before{content:"\f243"}.fa-battery-0:before,.fa-battery-empty:before{content:"\f244"}.fa-mouse-pointer:before{content:"\f245"}.fa-i-cursor:before{content:"\f246"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-sticky-note:before{content:"\f249"}.fa-sticky-note-o:before{content:"\f24a"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-diners-club:before{content:"\f24c"}.fa-clone:before{content:"\f24d"}.fa-balance-scale:before{content:"\f24e"}.fa-hourglass-o:before{content:"\f250"}.fa-hourglass-1:before,.fa-hourglass-start:before{content:"\f251"}.fa-hourglass-2:before,.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-3:before,.fa-hourglass-end:before{content:"\f253"}.fa-hourglass:before{content:"\f254"}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:"\f255"}.fa-hand-stop-o:before,.fa-hand-paper-o:before{content:"\f256"}.fa-hand-scissors-o:before{content:"\f257"}.fa-hand-lizard-o:before{content:"\f258"}.fa-hand-spock-o:before{content:"\f259"}.fa-hand-pointer-o:before{content:"\f25a"}.fa-hand-peace-o:before{content:"\f25b"}.fa-trademark:before{content:"\f25c"}.fa-registered:before{content:"\f25d"}.fa-creative-commons:before{content:"\f25e"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-tripadvisor:before{content:"\f262"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-get-pocket:before{content:"\f265"}.fa-wikipedia-w:before{content:"\f266"}.fa-safari:before{content:"\f267"}.fa-chrome:before{content:"\f268"}.fa-firefox:before{content:"\f269"}.fa-opera:before{content:"\f26a"}.fa-internet-explorer:before{content:"\f26b"}.fa-tv:before,.fa-television:before{content:"\f26c"}.fa-contao:before{content:"\f26d"}.fa-500px:before{content:"\f26e"}.fa-amazon:before{content:"\f270"}.fa-calendar-plus-o:before{content:"\f271"}.fa-calendar-minus-o:before{content:"\f272"}.fa-calendar-times-o:before{content:"\f273"}.fa-calendar-check-o:before{content:"\f274"}.fa-industry:before{content:"\f275"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-map-o:before{content:"\f278"}.fa-map:before{content:"\f279"}.fa-commenting:before{content:"\f27a"}.fa-commenting-o:before{content:"\f27b"}.fa-houzz:before{content:"\f27c"}.fa-vimeo:before{content:"\f27d"}.fa-black-tie:before{content:"\f27e"}.fa-fonticons:before{content:"\f280"}.fa-reddit-alien:before{content:"\f281"}.fa-edge:before{content:"\f282"}.fa-credit-card-alt:before{content:"\f283"}.fa-codiepie:before{content:"\f284"}.fa-modx:before{content:"\f285"}.fa-fort-awesome:before{content:"\f286"}.fa-usb:before{content:"\f287"}.fa-product-hunt:before{content:"\f288"}.fa-mixcloud:before{content:"\f289"}.fa-scribd:before{content:"\f28a"}.fa-pause-circle:before{content:"\f28b"}.fa-pause-circle-o:before{content:"\f28c"}.fa-stop-circle:before{content:"\f28d"}.fa-stop-circle-o:before{content:"\f28e"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-hashtag:before{content:"\f292"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-percent:before{content:"\f295"}.fa-gitlab:before{content:"\f296"}.fa-wpbeginner:before{content:"\f297"}.fa-wpforms:before{content:"\f298"}.fa-envira:before{content:"\f299"}.fa-universal-access:before{content:"\f29a"}.fa-wheelchair-alt:before{content:"\f29b"}.fa-question-circle-o:before{content:"\f29c"}.fa-blind:before{content:"\f29d"}.fa-audio-description:before{content:"\f29e"}.fa-volume-control-phone:before{content:"\f2a0"}.fa-braille:before{content:"\f2a1"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asl-interpreting:before,.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-deafness:before,.fa-hard-of-hearing:before,.fa-deaf:before{content:"\f2a4"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-signing:before,.fa-sign-language:before{content:"\f2a7"}.fa-low-vision:before{content:"\f2a8"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-pied-piper:before{content:"\f2ae"}.fa-first-order:before{content:"\f2b0"}.fa-yoast:before{content:"\f2b1"}.fa-themeisle:before{content:"\f2b2"}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:"\f2b3"}.fa-fa:before,.fa-font-awesome:before{content:"\f2b4"}.fa-handshake-o:before{content:"\f2b5"}.fa-envelope-open:before{content:"\f2b6"}.fa-envelope-open-o:before{content:"\f2b7"}.fa-linode:before{content:"\f2b8"}.fa-address-book:before{content:"\f2b9"}.fa-address-book-o:before{content:"\f2ba"}.fa-vcard:before,.fa-address-card:before{content:"\f2bb"}.fa-vcard-o:before,.fa-address-card-o:before{content:"\f2bc"}.fa-user-circle:before{content:"\f2bd"}.fa-user-circle-o:before{content:"\f2be"}.fa-user-o:before{content:"\f2c0"}.fa-id-badge:before{content:"\f2c1"}.fa-drivers-license:before,.fa-id-card:before{content:"\f2c2"}.fa-drivers-license-o:before,.fa-id-card-o:before{content:"\f2c3"}.fa-quora:before{content:"\f2c4"}.fa-free-code-camp:before{content:"\f2c5"}.fa-telegram:before{content:"\f2c6"}.fa-thermometer-4:before,.fa-thermometer:before,.fa-thermometer-full:before{content:"\f2c7"}.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-thermometer-2:before,.fa-thermometer-half:before{content:"\f2c9"}.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:"\f2ca"}.fa-thermometer-0:before,.fa-thermometer-empty:before{content:"\f2cb"}.fa-shower:before{content:"\f2cc"}.fa-bathtub:before,.fa-s15:before,.fa-bath:before{content:"\f2cd"}.fa-podcast:before{content:"\f2ce"}.fa-window-maximize:before{content:"\f2d0"}.fa-window-minimize:before{content:"\f2d1"}.fa-window-restore:before{content:"\f2d2"}.fa-times-rectangle:before,.fa-window-close:before{content:"\f2d3"}.fa-times-rectangle-o:before,.fa-window-close-o:before{content:"\f2d4"}.fa-bandcamp:before{content:"\f2d5"}.fa-grav:before{content:"\f2d6"}.fa-etsy:before{content:"\f2d7"}.fa-imdb:before{content:"\f2d8"}.fa-ravelry:before{content:"\f2d9"}.fa-eercast:before{content:"\f2da"}.fa-microchip:before{content:"\f2db"}.fa-snowflake-o:before{content:"\f2dc"}.fa-superpowers:before{content:"\f2dd"}.fa-wpexplorer:before{content:"\f2de"}.fa-meetup:before{content:"\f2e0"}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}
5 |
--------------------------------------------------------------------------------