├── .gitignore ├── README.md ├── app ├── __init__.py ├── api │ ├── __init__.py │ ├── alarm.py │ └── dsn.py ├── decorators.py ├── email.py ├── exceptions.py ├── ext │ ├── __init__.py │ └── openid2_ext.py ├── main │ ├── __init__.py │ ├── errors.py │ ├── forms.py │ └── views.py ├── models.py ├── static │ ├── favicon.ico │ └── styles.css └── templates │ ├── 403.html │ ├── 404.html │ ├── 500.html │ ├── _macros.html │ ├── base.html │ ├── edit_profile.html │ ├── error_page.html │ ├── index.html │ ├── mail │ ├── alarm.html │ └── alarm.txt │ ├── new_project.html │ ├── new_to_email.html │ ├── project.html │ ├── to_emails.html │ └── user.html ├── config.py.example ├── gun.conf ├── manage.py ├── requirement.txt └── tests ├── __init__.py ├── conftest.py ├── test_api.py ├── test_basics.py ├── test_main.py ├── test_product_model.py └── test_user_model.py /.gitignore: -------------------------------------------------------------------------------- 1 | .coverage 2 | .tox 3 | .DS_Store 4 | *.sqlite 5 | *.egg-info 6 | *.pyc 7 | *.log 8 | *.egg 9 | *.db 10 | *.pid 11 | *pycache* 12 | config.py 13 | MANIFEST 14 | test.conf 15 | pip-log.txt 16 | celerybeat-schedule 17 | /htmlcov 18 | /cover 19 | /build 20 | /dist 21 | example/db.sqlite 22 | /dist 23 | /venv 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 系统监控自动报警 2 | 3 | ## 部署方法 4 | 5 | ### 安装依赖 6 | 7 | pip install -r requirement.txt 8 | 9 | 复制 `config.py.example` 为 `config.py`, 修改配置此文件中对应的项. 10 | 11 | 准备部署环境 12 | 13 | python manager.py deploy 14 | 15 | ### 运行 16 | 17 | 以 gunicorn 运行 18 | 19 | gunicorn -c gun.conf manage:app 20 | 21 | ### HTTP API 22 | 23 | 发送警报 24 | 25 | URI: /api/alarm/ 26 | 27 | 方式: POST JSON body 28 | 29 | HTTP Header 需要加上 X-CSRFToken 30 | 31 | Body 参数形如 32 | 33 | { 34 | "token": "ffffffffffffffffffffffffffffffff", 35 | "title": "Something Wrong", 36 | "text": "This is just another alarm", 37 | "emails": ["somebody@example.org", "anothor@example.orz"] 38 | } 39 | 40 | 其中 X-CSRFToken 的取值与 token 是登陆创建 app 后, 该 app 的 token; 成功返回状态码是 201 41 | 42 | 获取 DSN 43 | 44 | URI: /api/dsn/// 45 | 46 | 方式: GET 47 | 48 | URI 参数: 49 | 50 | user_name : 用户名 51 | project_name : 项目名 52 | 53 | 返回值为 JSON 54 | 55 | { 56 | "dsn": DSN 地址 57 | } 58 | -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from flask import Flask, request, g 4 | from flask.ext.bootstrap import Bootstrap 5 | from flask.ext.mail import Mail 6 | from flask.ext.moment import Moment 7 | from flask.ext.sqlalchemy import SQLAlchemy 8 | from config import config 9 | 10 | from .ext import openid2 11 | 12 | bootstrap = Bootstrap() 13 | mail = Mail() 14 | moment = Moment() 15 | db = SQLAlchemy() 16 | 17 | 18 | def create_app(config_name): 19 | app = Flask(__name__) 20 | app.config.from_object(config[config_name]) 21 | config[config_name].init_app(app) 22 | 23 | bootstrap.init_app(app) 24 | mail.init_app(app) 25 | moment.init_app(app) 26 | db.init_app(app) 27 | openid2.init_app(app) 28 | 29 | from .main import main as main_blueprint 30 | app.register_blueprint(main_blueprint) 31 | 32 | from .api import api as api_blueprint 33 | app.register_blueprint(api_blueprint, url_prefix='/api') 34 | 35 | from .models import User 36 | 37 | @app.before_request 38 | def init_global_vars(): 39 | user_dict = json.loads(request.cookies.get(app.config['OPENID2_PROFILE_COOKIE_NAME'], '{}')) 40 | g.user = user_dict and User.get_or_create(user_dict['username'], user_dict['email']) or None 41 | return app 42 | -------------------------------------------------------------------------------- /app/api/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | 3 | api = Blueprint('api', __name__) 4 | 5 | from . import alarm 6 | from . import dsn 7 | -------------------------------------------------------------------------------- /app/api/alarm.py: -------------------------------------------------------------------------------- 1 | from flask import request, Response 2 | from ..email import send_email 3 | from .. import db 4 | from ..models import Project, Alarm 5 | from . import api 6 | import json 7 | import re 8 | 9 | EMAIL_PATTERN = re.compile( 10 | r'^[_a-z0-9-]+(\.[_a-z0-9-]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,4})$') 11 | 12 | 13 | @api.route('/alarm/', methods=['POST']) 14 | def alarm(): 15 | data = request.get_json() or json.loads(request.data) 16 | if not data: 17 | return Response(status=400) 18 | 19 | token = data.get('token', '') 20 | text = data.get('text', '') 21 | title = data.get('title', '') 22 | emails = data.get('emails', None) 23 | if emails is None: 24 | emails = [] 25 | additional_to_email = [x for x in emails if EMAIL_PATTERN.match(x)] 26 | 27 | project = Project.query.filter_by(token=token).first_or_404() 28 | to_email = map(lambda x: x.address, project.to_emails.all()) 29 | to = list(set(to_email + additional_to_email)) 30 | 31 | send_email(to, (project.name + ': ' + title).title(), 'mail/alarm', 32 | message=text) 33 | 34 | alarm = Alarm(text=text, title=title, recipients=', '.join(to)) 35 | project.alarms.append(alarm) 36 | db.session.add(project) 37 | db.session.commit() 38 | return Response(status=201) 39 | -------------------------------------------------------------------------------- /app/api/dsn.py: -------------------------------------------------------------------------------- 1 | from urlparse import urlparse 2 | from flask import request, Response, jsonify, current_app 3 | from ..models import User, Project 4 | from .. import db 5 | from . import api 6 | 7 | 8 | @api.route('/dsn///') 9 | def register_dsn(username, project_name): 10 | user = User.query.filter_by(username=username).first_or_404() 11 | project = user.projects.filter_by(name=project_name).first() 12 | if project: 13 | token = project.token 14 | else: 15 | project = Project(name=project_name) 16 | user.projects.append(project) 17 | db.session.add(user) 18 | db.session.commit() 19 | token = project.token 20 | app = current_app 21 | url = app.config['ALGALON_SERVER_HOST'] 22 | urlparts = urlparse(url) 23 | port = app.config['ALGALON_SERVER_PORT'] 24 | token = project.token 25 | dsn = 'http://{token}@{host}:{port}{path}'.format( 26 | token=token, 27 | host=urlparts.hostname, 28 | port=port, 29 | path=urlparts.path) 30 | response = jsonify({'dsn': dsn}) 31 | response.status_code = 201 32 | return response 33 | -------------------------------------------------------------------------------- /app/decorators.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | from flask import abort, g, flash, url_for, redirect 3 | 4 | 5 | def login_required(f): 6 | 7 | @wraps(f) 8 | def decorated_function(*args, **kwargs): 9 | if g.user and g.user.username == kwargs['username']: 10 | return f(*args, **kwargs) 11 | if g.user and g.user.username != kwargs['username']: 12 | return abort(403) 13 | flash('please login') 14 | return redirect(url_for('main.index')) 15 | 16 | return decorated_function 17 | -------------------------------------------------------------------------------- /app/email.py: -------------------------------------------------------------------------------- 1 | from threading import Thread 2 | from flask import current_app, render_template 3 | from flask.ext.mail import Message 4 | from . import mail 5 | 6 | 7 | def send_async_email(app, msg): 8 | with app.app_context(): 9 | mail.send(msg) 10 | 11 | 12 | def send_email(to, subject, template, **kwargs): 13 | app = current_app._get_current_object() 14 | msg = Message(app.config['ALGALON_MAIL_SUBJECT_PREFIX'] + subject, 15 | sender=app.config['ALGALON_MAIL_SENDER'], recipients=to) 16 | msg.body = render_template(template + '.txt', **kwargs) 17 | msg.html = render_template(template + '.html', **kwargs) 18 | thr = Thread(target=send_async_email, args=[app, msg]) 19 | thr.start() 20 | return thr 21 | -------------------------------------------------------------------------------- /app/exceptions.py: -------------------------------------------------------------------------------- 1 | class ValidationError(ValueError): 2 | pass 3 | -------------------------------------------------------------------------------- /app/ext/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from .openid2_ext import OpenID2 4 | 5 | openid2 = OpenID2(file_store_path=os.getenv('NBE_PERMDIR', '')) 6 | -------------------------------------------------------------------------------- /app/ext/openid2_ext.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import os 4 | import json 5 | import hmac 6 | import tempfile 7 | from urllib import urlencode 8 | from urlparse import urljoin 9 | 10 | from flask import request, redirect, current_app, _app_ctx_stack 11 | from openid.extensions import sreg 12 | from openid.consumer.consumer import Consumer, SUCCESS 13 | from openid.store.filestore import FileOpenIDStore 14 | 15 | sreg.data_fields = { 16 | 'username': 'test', 17 | 'email': 'test@xxx.com', 18 | 'groups': '[]', 19 | 'uid': '0' 20 | } 21 | 22 | 23 | def sign(user_cookie_name, user): 24 | s = user_cookie_name + user 25 | return hmac.new('123', s).hexdigest() 26 | 27 | 28 | class OpenIDRouteNotFoundError(Exception): 29 | pass 30 | 31 | 32 | class OpenID2User(dict): 33 | 34 | def __getattr__(self, name): 35 | return self.get(name, None) 36 | 37 | 38 | class OpenID2Client(object): 39 | 40 | def __init__(self, verify_url, store): 41 | self.verify_url = verify_url 42 | self.store = store 43 | 44 | def login(self, req): 45 | continue_url = req.values.get('continue') or req.headers.get('Referer', '/') 46 | consumer = Consumer({}, self.store) 47 | authreq = consumer.begin(current_app.config['OPENID2_YADIS']) 48 | 49 | sregreq = sreg.SRegRequest(optional=['username', 'uid'], 50 | required=['email', 'groups']) 51 | authreq.addExtension(sregreq) 52 | 53 | verify_url = urljoin(req.host_url, self.verify_url) + '?' + urlencode({'continue': continue_url}) 54 | urlencode({'continue': continue_url}) 55 | url = authreq.redirectURL(return_to=verify_url, realm=req.host_url) 56 | return redirect(location=url) 57 | 58 | def verify(self, req): 59 | consumer = Consumer({}, self.store) 60 | return_to = req.values.get('continue', '/') 61 | authres = consumer.complete(req.args, urljoin(req.host_url, self.verify_url)) 62 | res = redirect(location=return_to) 63 | if authres.status is SUCCESS: 64 | sregres = sreg.SRegResponse.fromSuccessResponse(authres) 65 | user = json.dumps({'openid': authres.identity_url}) 66 | sig = sign(current_app.config['OPENID2_USER_COOKIE_NAME'], user=user) 67 | one_year = 3600 * 24 * 365 68 | res.set_cookie(current_app.config['OPENID2_USER_COOKIE_NAME'], user, max_age=one_year) 69 | res.set_cookie(current_app.config['OPENID2_SIG_COOKIE_NAME'], sig, max_age=one_year) 70 | profile = sregres and {s[0]: s[1] for s in sregres.items()} or {} 71 | res.set_cookie(current_app.config['OPENID2_PROFILE_COOKIE_NAME'], json.dumps(profile), max_age=one_year) 72 | return res 73 | 74 | def logout(self, req): 75 | continue_url = req.values.get('continue') or req.headers.get('Referer', '/') 76 | this_url = urljoin(req.host_url, continue_url) 77 | openid2_logout_url = current_app.config['OPENID2_LOGOUT'] + '?next=%s' % this_url 78 | res = redirect(location=openid2_logout_url) 79 | res.delete_cookie(current_app.config['OPENID2_USER_COOKIE_NAME']) 80 | res.delete_cookie(current_app.config['OPENID2_PROFILE_COOKIE_NAME']) 81 | res.delete_cookie(current_app.config['OPENID2_SIG_COOKIE_NAME']) 82 | return res 83 | 84 | 85 | class OpenID2(object): 86 | 87 | def __init__(self, name='openid2', 88 | file_store_path='', 89 | app=None, 90 | login_url='/login/', 91 | verify_url='/login/verify/', 92 | logout_url='/login/logout/', 93 | openid2_yadis_url='', 94 | openid2_logout_url='', 95 | openid2_user_cookie_name='USER_COOKIE_NAME', 96 | openid2_sig_cookie_name='SIG_COOKIE_NAME', 97 | openid2_profile_cookie_name='PROFILE_COOKIE_NAME'): 98 | 99 | self.name = name 100 | if not file_store_path: 101 | file_store_path = os.path.join(tempfile.gettempdir(), 'algalon-openid2') 102 | self.file_store_path = file_store_path 103 | self.app = app 104 | 105 | self.login_url = login_url 106 | self.verify_url = verify_url 107 | self.logout_url = logout_url 108 | 109 | self.openid2_yadis_url = openid2_yadis_url 110 | self.openid2_logout_url = openid2_logout_url 111 | self.openid2_user_cookie_name = openid2_user_cookie_name 112 | self.openid2_sig_cookie_name = openid2_sig_cookie_name 113 | self.openid2_profile_cookie_name = openid2_profile_cookie_name 114 | 115 | self.store = FileOpenIDStore(self.file_store_path) 116 | 117 | if app is not None: 118 | self.init_app(app) 119 | 120 | def init_app(self, app): 121 | app.config.setdefault('OPENID2_YADIS', self.openid2_yadis_url) 122 | app.config.setdefault('OPENID2_LOGOUT', self.openid2_logout_url) 123 | app.config.setdefault('OPENID2_USER_COOKIE_NAME', self.openid2_user_cookie_name) 124 | app.config.setdefault('OPENID2_SIG_COOKIE_NAME', self.openid2_sig_cookie_name) 125 | app.config.setdefault('OPENID2_PROFILE_COOKIE_NAME', self.openid2_profile_cookie_name) 126 | self.register_openid_route(app) 127 | app.add_template_global(self, self.name) 128 | 129 | def register_openid_route(self, app): 130 | if not (self.login_url and self.logout_url and self.verify_url): 131 | raise OpenIDRouteNotFoundError() 132 | 133 | # @app.before_request 134 | # def get_login_info(): 135 | # g.user = OpenID2User(json.loads(request.cookies.get(self.openid2_profile_cookie_name, '{}'))) 136 | 137 | @app.route(self.login_url) 138 | def openid2_login(): 139 | return self.openid2.login(request) 140 | 141 | @app.route(self.verify_url) 142 | def openid2_verify(): 143 | return self.openid2.verify(request) 144 | 145 | @app.route(self.logout_url) 146 | def openid2_logout(): 147 | return self.openid2.logout(request) 148 | 149 | def init_openid2(self): 150 | return OpenID2Client(self.verify_url, self.store) 151 | 152 | @property 153 | def openid2(self): 154 | ctx = _app_ctx_stack.top 155 | if ctx is not None: 156 | if not hasattr(ctx, 'openid2'): 157 | ctx.openid2 = self.init_openid2() 158 | return ctx.openid2 159 | 160 | def __getattr__(self, name): 161 | return getattr(self.openid2, name) 162 | -------------------------------------------------------------------------------- /app/main/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | 3 | main = Blueprint('main', __name__) 4 | 5 | from . import views, errors 6 | -------------------------------------------------------------------------------- /app/main/errors.py: -------------------------------------------------------------------------------- 1 | from flask import render_template, request, jsonify 2 | from . import main 3 | 4 | 5 | @main.app_errorhandler(403) 6 | def forbidden(e): 7 | if request.accept_mimetypes.accept_json and \ 8 | not request.accept_mimetypes.accept_html: 9 | response = jsonify({'error': 'forbidden'}) 10 | response.status_code = 403 11 | return response 12 | return render_template('403.html'), 403 13 | 14 | 15 | @main.app_errorhandler(404) 16 | def page_not_found(e): 17 | if request.accept_mimetypes.accept_json and \ 18 | not request.accept_mimetypes.accept_html: 19 | response = jsonify({'error': 'not found'}) 20 | response.status_code = 404 21 | return response 22 | return render_template('404.html'), 404 23 | 24 | 25 | @main.app_errorhandler(500) 26 | def internal_server_error(e): 27 | if request.accept_mimetypes.accept_json and \ 28 | not request.accept_mimetypes.accept_html: 29 | response = jsonify({'error': 'internal server error'}) 30 | response.status_code = 500 31 | return response 32 | return render_template('500.html'), 500 33 | -------------------------------------------------------------------------------- /app/main/forms.py: -------------------------------------------------------------------------------- 1 | from flask.ext.wtf import Form 2 | from wtforms import StringField, SubmitField 3 | from wtforms.validators import Required, Length 4 | 5 | 6 | class EditProfileForm(Form): 7 | name = StringField('Real name', validators=[Length(0, 64)]) 8 | submit = SubmitField('Submit') 9 | 10 | 11 | class NewProjectForm(Form): 12 | name = StringField('Project name', validators=[Required(), Length(0, 64)]) 13 | submit = SubmitField('Submit') 14 | 15 | 16 | class NewToEmailForm(Form): 17 | addresses = StringField('Email Addresses', validators=[Required()]) 18 | submit = SubmitField('Submit') 19 | -------------------------------------------------------------------------------- /app/main/views.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding=utf-8 3 | 4 | from flask import render_template, redirect, url_for, flash, g, current_app, request 5 | from urlparse import urlparse 6 | from . import main 7 | from .forms import EditProfileForm, NewProjectForm, NewToEmailForm 8 | from .. import db 9 | from ..models import User, Project, ToEmail, Alarm 10 | from ..decorators import login_required 11 | 12 | import re 13 | 14 | @main.route('/') 15 | def index(): 16 | if g.user: 17 | return redirect(url_for('.user', username=g.user.username)) 18 | return render_template('index.html') 19 | 20 | 21 | @main.route('/user/') 22 | @login_required 23 | def user(username): 24 | user = User.query.filter_by(username=username).first_or_404() 25 | url = current_app.config['ALGALON_SERVER_HOST'] 26 | urlparts = urlparse(url) 27 | port = current_app.config['ALGALON_SERVER_PORT'] 28 | return render_template('user.html', user=user, host=urlparts.netloc, port=port) 29 | 30 | 31 | @main.route('/user//edit', methods=['GET', 'POST']) 32 | @login_required 33 | def edit_profile(username): 34 | form = EditProfileForm() 35 | if form.validate_on_submit(): 36 | user = User.query.filter_by(username=username).first_or_404() 37 | user.name = form.name.data 38 | user.email = form.email.data 39 | db.session.add(user) 40 | db.session.commit() 41 | return redirect(url_for('.user', username=username)) 42 | return render_template('edit_profile.html', form=form) 43 | 44 | 45 | @main.route('/user//project/') 46 | @login_required 47 | def user_project(username, project_name): 48 | user = User.query.filter_by(username=username).first_or_404() 49 | project = user.projects.filter_by(name=project_name).first_or_404() 50 | page = request.args.get('page', 1, type=int) 51 | pagination = project.alarms.order_by(Alarm.date_added.desc()).paginate( 52 | page,5, 53 | error_out=False) 54 | alarms = pagination.items 55 | return render_template('project.html', username=user.username, 56 | alarms=alarms, project=project, 57 | pagination=pagination) 58 | 59 | 60 | @main.route('/user//project//reset_token') 61 | @login_required 62 | def reset_token(username, project_name): 63 | user = User.query.filter_by(username=username).first_or_404() 64 | project = user.projects.filter_by(name=project_name).first_or_404() 65 | project.reset_token() 66 | db.session.add(project) 67 | db.session.commit() 68 | return redirect(url_for('.user', username=username)) 69 | 70 | 71 | @main.route('/user//project/new', methods=['GET', 'POST']) 72 | @login_required 73 | def create_user_project(username): 74 | form = NewProjectForm() 75 | if form.validate_on_submit(): 76 | project_name = form.name.data 77 | if g.user.projects.filter_by(name=project_name).first(): 78 | flash('failure, project name already exist') 79 | return redirect(url_for('.user', username=username)) 80 | project = Project(name=project_name) 81 | g.user.projects.append(project) 82 | db.session.add(g.user) 83 | db.session.commit() 84 | flash('new project created') 85 | return redirect(url_for('.user', username=username)) 86 | return render_template('new_project.html', form=form) 87 | 88 | 89 | @main.route('/user//project//delete') 90 | @login_required 91 | def delete_project(username, project_name): 92 | user = User.query.filter_by(username=username).first_or_404() 93 | project = user.projects.filter_by(name=project_name).first_or_404() 94 | user.projects.remove(project) 95 | db.session.add(user) 96 | db.session.commit() 97 | flash('project deleted') 98 | return redirect(url_for('.user', username=username)) 99 | 100 | 101 | @main.route('///to_email/new', methods=['GET', 'POST']) 102 | @login_required 103 | def create_to_email(username, project_name): 104 | p = g.user.projects.filter_by(name=project_name).first_or_404() 105 | form = NewToEmailForm() 106 | if form.validate_on_submit(): 107 | pt = re.compile('^[_a-z0-9-]+(\.[_a-z0-9-]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,4})$') 108 | addresses = form.addresses.data 109 | 110 | for address in set([x.strip() for x in addresses.split(',')]): 111 | m = pt.match(address) 112 | if m: 113 | te = ToEmail(address=m.group()) 114 | p.to_emails.append(te) 115 | 116 | db.session.add(p) 117 | db.session.commit() 118 | flash('emails added') 119 | return redirect(url_for('.user_project', username=username, project_name=project_name)) 120 | return render_template('new_to_email.html', form=form) 121 | 122 | 123 | @main.route('////delete', methods=['GET']) 124 | @login_required 125 | def delete_to_email(username, project_name, to_email_id): 126 | p = g.user.projects.filter_by(name=project_name).first_or_404() 127 | te = p.to_emails.filter_by(id=to_email_id).first_or_404() 128 | p.to_emails.remove(te) 129 | db.session.add(p) 130 | db.session.commit() 131 | return redirect(url_for('.show_to_emails', username=username, project_name=project_name)) 132 | 133 | 134 | @main.route('///to_emails', methods=['GET']) 135 | @login_required 136 | def show_to_emails(username, project_name): 137 | user = User.query.filter_by(username=username).first_or_404() 138 | project = user.projects.filter_by(name=project_name).first_or_404() 139 | return render_template('to_emails.html', username=username, project=project) 140 | -------------------------------------------------------------------------------- /app/models.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import uuid 3 | from datetime import datetime 4 | from itsdangerous import TimedJSONWebSignatureSerializer as Serializer 5 | from flask import current_app, request 6 | from . import db 7 | 8 | 9 | class User(db.Model): 10 | __tablename__ = 'users' 11 | id = db.Column(db.Integer, primary_key=True) 12 | email = db.Column(db.String(64), unique=True, index=True) 13 | username = db.Column(db.String(64), unique=True, index=True) 14 | name = db.Column(db.String(64)) 15 | last_seen = db.Column(db.DateTime(), default=datetime.utcnow) 16 | avatar_hash = db.Column(db.String(32)) 17 | is_admin = db.Column(db.Boolean(), default=False) 18 | projects = db.relationship('Project', 19 | backref=db.backref('owner', lazy='joined'), 20 | cascade='all, delete, delete-orphan', 21 | lazy='dynamic') 22 | 23 | def __init__(self, **kwargs): 24 | super(User, self).__init__(**kwargs) 25 | if self.email == current_app.config.get('ALGALON_ADMIN', ''): 26 | self.is_admin = True 27 | if self.email is not None and self.avatar_hash is None: 28 | self.avatar_hash = hashlib.md5( 29 | self.email.encode('utf-8')).hexdigest() 30 | 31 | @classmethod 32 | def get_or_create(cls, username, email): 33 | u = db.session.query(cls).filter(cls.username == username).first() 34 | if u: 35 | return u 36 | u = cls(username=username, email=email) 37 | db.session.add(u) 38 | db.session.commit() 39 | return u 40 | 41 | def is_administrator(self): 42 | return self.is_admin 43 | 44 | def gravatar(self, size=100, default='identicon', rating='g'): 45 | if request.is_secure: 46 | url = 'https://secure.gravatar.com/avatar' 47 | else: 48 | url = 'http://www.gravatar.com/avatar' 49 | hash = self.avatar_hash or hashlib.md5( 50 | self.email.encode('utf-8')).hexdigest() 51 | return '{url}/{hash}?s={size}&d={default}&r={rating}'.format( 52 | url=url, hash=hash, size=size, default=default, rating=rating) 53 | 54 | def __repr__(self): 55 | return '' % self.username 56 | 57 | 58 | class Project(db.Model): 59 | __tablename__ = 'projects' 60 | id = db.Column(db.Integer, primary_key=True) 61 | name = db.Column(db.String(64), index=True) 62 | date_added = db.Column(db.DateTime, default=datetime.utcnow()) 63 | token = db.Column(db.String(64), unique=True, index=True, default=lambda: uuid.uuid4().hex) 64 | owner_id = db.Column(db.Integer, db.ForeignKey('users.id')) 65 | alarms = db.relationship('Alarm', backref='app', lazy='dynamic') 66 | to_emails = db.relationship('ToEmail', 67 | backref=db.backref('project', lazy='joined'), 68 | cascade='all, delete, delete-orphan', 69 | lazy='dynamic') 70 | 71 | def reset_token(self): 72 | self.token = uuid.uuid4().hex 73 | db.session.add(self) 74 | return True 75 | 76 | 77 | class Alarm(db.Model): 78 | __tablename__ = 'alarms' 79 | id = db.Column(db.Integer, primary_key=True) 80 | title = db.Column(db.String(32)) 81 | text = db.Column(db.Text) 82 | recipients = db.Column(db.Text) 83 | date_added = db.Column(db.DateTime, default=datetime.utcnow()) 84 | app_id = db.Column(db.Integer, db.ForeignKey('projects.id')) 85 | 86 | 87 | class ToEmail(db.Model): 88 | __tablename__ = 'to_emails' 89 | id = db.Column(db.Integer, primary_key=True) 90 | address = db.Column(db.String(64), nullable=False) 91 | project_id = db.Column(db.Integer, db.ForeignKey('projects.id')) 92 | date_added = db.Column(db.DateTime, default=datetime.utcnow()) 93 | -------------------------------------------------------------------------------- /app/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HunanTV/algalon/5d136a4a2df71673e1c05dc344721f6c46b312c5/app/static/favicon.ico -------------------------------------------------------------------------------- /app/static/styles.css: -------------------------------------------------------------------------------- 1 | .profile-thumbnail { 2 | position: absolute; 3 | } 4 | .profile-header { 5 | min-height: 260px; 6 | margin-left: 280px; 7 | } 8 | div.project-tabs { 9 | margin-top: 16px; 10 | } 11 | ul.projects { 12 | list-style-type: none; 13 | padding: 0px; 14 | margin: 16px 0px 0px 0px; 15 | border-bottom: 1px solid #e0e0e0; 16 | } 17 | div.project-tabs ul.projects { 18 | margin: 0px; 19 | border-top: none; 20 | } 21 | ul.projects li.project { 22 | padding: 8px; 23 | 24 | } 25 | ul.projects li.project:hover { 26 | background-color: #f0f0f0; 27 | } 28 | div.project-date { 29 | float: right; 30 | } 31 | div.project-author { 32 | font-weight: bold; 33 | } 34 | div.project-content { 35 | margin-left: 48px; 36 | min-height: 48px; 37 | } 38 | div.project-footer { 39 | text-align: right; 40 | } 41 | ul.alarms { 42 | list-style-type: none; 43 | padding: 0px; 44 | margin: 16px 0px 0px 0px; 45 | bborder-top: 1px solid #e0e0e0; 46 | } 47 | ul.alarms li.alarm { 48 | margin-left: 32px; 49 | padding: 8px; 50 | border-bottom: 1px solid #e0e0e0; 51 | } 52 | ul.alarms li.alarm:nth-child(1) { 53 | border-top: 1px solid #e0e0e0; 54 | } 55 | ul.alarms li.alarm:hover { 56 | background-color: #f0f0f0; 57 | } 58 | div.alarm-date { 59 | float: right; 60 | } 61 | div.alarm-content { 62 | margin-left: 48px; 63 | min-height: 48px; 64 | } 65 | div.pagination { 66 | width: 100%; 67 | text-align: right; 68 | padding: 0px; 69 | margin: 0px; 70 | } 71 | .project-body h1 { 72 | font-size: 140%; 73 | } 74 | .project-body h2 { 75 | font-size: 130%; 76 | } 77 | .project-body h3 { 78 | font-size: 120%; 79 | } -------------------------------------------------------------------------------- /app/templates/403.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Flasky - Forbidden{% endblock %} 4 | 5 | {% block page_content %} 6 | 9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /app/templates/404.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Flasky - Page Not Found{% endblock %} 4 | 5 | {% block page_content %} 6 | 9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /app/templates/500.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Flasky - Internal Server Error{% endblock %} 4 | 5 | {% block page_content %} 6 | 9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /app/templates/_macros.html: -------------------------------------------------------------------------------- 1 | {% macro pagination_widget(pagination, endpoint, fragment='') %} 2 |
    3 | 4 | 5 | « 6 | 7 | 8 | {% for p in pagination.iter_pages() %} 9 | {% if p %} 10 | {% if p == pagination.page %} 11 |
  • 12 | {{ p }} 13 |
  • 14 | {% else %} 15 |
  • 16 | {{ p }} 17 |
  • 18 | {% endif %} 19 | {% else %} 20 |
  • 21 | {% endif %} 22 | {% endfor %} 23 | 24 | 25 | » 26 | 27 | 28 |
29 | {% endmacro %} 30 | -------------------------------------------------------------------------------- /app/templates/base.html: -------------------------------------------------------------------------------- 1 | {% extends "bootstrap/base.html" %} 2 | 3 | {% block title %}Algalon - 自动报警系统{% endblock %} 4 | 5 | {% block head %} 6 | {{ super() }} 7 | 8 | 9 | 10 | {% endblock %} 11 | 12 | {% block navbar %} 13 | 35 | {% endblock %} 36 | 37 | {% block content %} 38 |
39 | {% for message in get_flashed_messages() %} 40 |
41 | 42 | {{ message }} 43 |
44 | {% endfor %} 45 | 46 | {% block page_content %}{% endblock %} 47 |
48 | {% endblock %} 49 | 50 | {% block scripts %} 51 | {{ super() }} 52 | {{ moment.include_moment() }} 53 | {% endblock %} 54 | -------------------------------------------------------------------------------- /app/templates/edit_profile.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% import "bootstrap/wtf.html" as wtf %} 3 | 4 | {% block title %}Flasky - Edit Infomation{% endblock %} 5 | 6 | {% block page_content %} 7 | 10 |
11 | {{ wtf.quick_form(form) }} 12 |
13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /app/templates/error_page.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Flasky - {{ code }}: {{ name }}{% endblock %} 4 | 5 | {% block page_content %} 6 | 10 | {% endblock %} 11 | -------------------------------------------------------------------------------- /app/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% import "bootstrap/wtf.html" as wtf %} 3 | {% import "_macros.html" as macros %} 4 | 5 | {% block title %}Flasky{% endblock %} 6 | 7 | {% block page_content %} 8 | 11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /app/templates/mail/alarm.html: -------------------------------------------------------------------------------- 1 | {{ message }} 2 | -------------------------------------------------------------------------------- /app/templates/mail/alarm.txt: -------------------------------------------------------------------------------- 1 | {{ message }} 2 | -------------------------------------------------------------------------------- /app/templates/new_project.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% import "bootstrap/wtf.html" as wtf %} 3 | 4 | {% block title %}Algalon New Project{% endblock %} 5 | 6 | {% block page_content %} 7 | 10 |
11 | {{ wtf.quick_form(form) }} 12 |
13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /app/templates/new_to_email.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% import "bootstrap/wtf.html" as wtf %} 3 | 4 | {% block title %}Add New Email to Project{% endblock %} 5 | 6 | {% block page_content %} 7 | 10 |
11 | {{ wtf.quick_form(form) }} 12 |
13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /app/templates/project.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% import "_macros.html" as macros %} 3 | 4 | {% block title %}Algalon - {{ username }} - {{ project.name }}{% endblock %} 5 | 6 | {% block page_content %} 7 | 12 |

报警状态

13 |
14 | 接收邮箱: 15 | {% for x in project.to_emails.all() %} 16 | {{ x.address }}, 17 | {% endfor%} 18 | 23 | 24 |
25 |
26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | {% for alarm in alarms%} 37 | 38 | 41 | 44 | 47 | 50 | 51 | {% endfor %} 52 | 53 | 54 | 55 | {% if pagination %} 56 | 59 | {% endif %} 60 | {% endblock %} 61 | -------------------------------------------------------------------------------- /app/templates/to_emails.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% import "_macros.html" as macros %} 3 | 4 | {% block title %}Algalon - {{ username }} - {{ project.name }}{% endblock %} 5 | 6 | {% block page_content %} 7 | 12 |

接收报警邮件列表

添加 13 |
14 |
TitleMessageRecipient添加时间
39 | {{ alarm.title}} 40 | 42 | {{ alarm.text }} 43 | 45 | {{ alarm.recipients}} 46 | 48 | {{ moment(alarm.date_added).format('LLL') }} 49 |
15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | {% for email in project.to_emails.all() %} 25 | 26 | 29 | 32 | 37 | 38 | {% endfor %} 39 | 40 | 41 | {% endblock %} 42 | -------------------------------------------------------------------------------- /app/templates/user.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% import "_macros.html" as macros %} 3 | 4 | {% block title %}Algalon - {{ user.username }}{% endblock %} 5 | 6 | {% block page_content %} 7 | 23 | 24 | 55 | {% endblock %} 56 | -------------------------------------------------------------------------------- /config.py.example: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #coding=utf-8 3 | 4 | import os 5 | basedir = os.path.abspath(os.path.dirname(__file__)) 6 | 7 | 8 | class Config: 9 | SECRET_KEY = '' 10 | SSL_DISABLE = False 11 | SQLALCHEMY_COMMIT_ON_TEARDOWN = True 12 | 13 | # 这个是服务器所在的地址,不要加上尾斜线 14 | ALGALON_SERVER_HOST = 'http://localhost' 15 | ALGALON_SERVER_PORT = 6969 16 | 17 | # 这里是邮件服务器配置,改为相应的邮件服务器 18 | MAIL_SERVER = '' 19 | MAIL_PORT = 25 20 | MAIL_USE_TLS = True 21 | MAIL_USERNAME = '' 22 | MAIL_PASSWORD = '' 23 | ALGALON_ADMIN = '' 24 | ALGALON_MAIL_SUBJECT_PREFIX = '' 25 | ALGALON_MAIL_SENDER = '' 26 | 27 | #这里是OpenID服务器的地址 28 | OPENID2_YADIS = '' 29 | OPENID2_LOGOUT = '' 30 | 31 | @staticmethod 32 | def init_app(app): 33 | pass 34 | 35 | 36 | class DevelopmentConfig(Config): 37 | DEBUG = True 38 | SQLALCHEMY_DATABASE_URI = 'mysql://user:pass@localhost/algalon_dev?charset=utf8' 39 | 40 | 41 | class TestingConfig(Config): 42 | TESTING = True 43 | SQLALCHEMY_DATABASE_URI = 'mysql://user:pass@localhost/algalon_test?charset=utf8' 44 | WTF_CSRF_ENABLED = False 45 | 46 | 47 | class ProductionConfig(Config): 48 | # 这里配置mysql服务器地址 49 | SQLALCHEMY_DATABASE_URI = 'mysql://user:pass@localhost/algalon?charset=utf8' 50 | 51 | @classmethod 52 | def init_app(cls, app): 53 | Config.init_app(app) 54 | 55 | # log to syslog 56 | import logging 57 | from logging.handlers import SysLogHandler 58 | syslog_handler = SysLogHandler() 59 | syslog_handler.setLevel(logging.WARNING) 60 | app.logger.addHandler(syslog_handler) 61 | 62 | # email errors to the administrators 63 | import logging 64 | from logging.handlers import SMTPHandler 65 | credentials = None 66 | secure = None 67 | if getattr(cls, 'MAIL_USERNAME', None) is not None: 68 | credentials = (cls.MAIL_USERNAME, cls.MAIL_PASSWORD) 69 | if getattr(cls, 'MAIL_USE_TLS', None): 70 | secure = () 71 | mail_handler = SMTPHandler( 72 | mailhost=(cls.MAIL_SERVER, cls.MAIL_PORT), 73 | fromaddr=cls.ALGALON_MAIL_SENDER, 74 | toaddrs=[cls.ALGALON_ADMIN], 75 | subject=cls.ALGALON_MAIL_SUBJECT_PREFIX + ' Application Error', 76 | credentials=credentials, 77 | secure=secure) 78 | mail_handler.setLevel(logging.ERROR) 79 | app.logger.addHandler(mail_handler) 80 | 81 | 82 | config = { 83 | 'development': DevelopmentConfig, 84 | 'testing': TestingConfig, 85 | 'production': ProductionConfig, 86 | 'default': ProductionConfig 87 | } 88 | -------------------------------------------------------------------------------- /gun.conf: -------------------------------------------------------------------------------- 1 | import os 2 | bind = '0.0.0.0:6969' 3 | workers = 4 4 | worker_class = "sync" # sync, gevent,meinheld 5 | worker_connections = 1000 6 | timeout = 30 7 | keepalive = 2 8 | debug = False -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | 4 | if os.path.exists('.env'): 5 | print('Importing environment from .env...') 6 | for line in open('.env'): 7 | var = line.strip().split('=') 8 | if len(var) == 2: 9 | os.environ[var[0]] = var[1] 10 | 11 | from app import create_app, db 12 | from app.models import User, Project, Alarm, ToEmail 13 | from flask.ext.script import Manager, Shell 14 | 15 | app = create_app(os.getenv('ALGALON_CONFIG') or 'default') 16 | manager = Manager(app) 17 | 18 | 19 | def make_shell_context(): 20 | return dict(app=app, db=db, User=User, Project=Project, Alarm=Alarm, 21 | ToEmail=ToEmail) 22 | 23 | manager.add_command("shell", Shell(make_context=make_shell_context)) 24 | 25 | 26 | @manager.command 27 | def profile(length=25, profile_dir=None): 28 | """Start the application under the code profiler.""" 29 | from werkzeug.contrib.profiler import ProfilerMiddleware 30 | app.wsgi_app = ProfilerMiddleware(app.wsgi_app, restrictions=[length], 31 | profile_dir=profile_dir) 32 | app.run() 33 | 34 | 35 | @manager.command 36 | def deploy(): 37 | """Run deployment tasks.""" 38 | db.create_all() 39 | 40 | 41 | if __name__ == '__main__': 42 | manager.run() 43 | -------------------------------------------------------------------------------- /requirement.txt: -------------------------------------------------------------------------------- 1 | Flask==0.10.1 2 | Flask-Bootstrap==3.3.0.1 3 | Flask-HTTPAuth==2.3.0 4 | Flask-Login==0.2.11 5 | Flask-Mail==0.9.1 6 | Flask-Moment==0.4.0 7 | Flask-SQLAlchemy==2.0 8 | Flask-SSLify==0.1.4 9 | Flask-Script==2.0.5 10 | Flask-WTF==0.10.3 11 | ForgeryPy==0.1 12 | Jinja2==2.7.3 13 | Mako==1.0.0 14 | MarkupSafe==0.23 15 | MySQL-python==1.2.5 16 | Pygments==2.0.2 17 | SQLAlchemy==0.9.8 18 | WTForms==2.0.2 19 | Werkzeug==0.9.6 20 | alembic==0.7.4 21 | argparse==1.2.1 22 | bleach==1.4.1 23 | blinker==1.3 24 | colorama==0.3.3 25 | gevent==1.0.1 26 | greenlet==0.4.5 27 | gunicorn==18.0 28 | html5lib==0.999 29 | httpie==0.8.0 30 | ipython==2.3.1 31 | itsdangerous==0.24 32 | meld3==1.0.0 33 | python-openid==2.2.5 34 | requests==2.5.1 35 | selenium==2.44.0 36 | six==1.9.0 37 | supervisor==3.1.3 38 | wsgiref==0.1.2 39 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HunanTV/algalon/5d136a4a2df71673e1c05dc344721f6c46b312c5/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | from app import create_app 2 | from app import db as _db 3 | from app.models import User, Project 4 | import pytest 5 | 6 | 7 | @pytest.fixture(scope='session') 8 | def app(request): 9 | app = create_app('testing') 10 | return app 11 | 12 | 13 | @pytest.fixture(scope='module') 14 | def db(app, request): 15 | _db.app = app 16 | _db.create_all() 17 | 18 | def teardown(): 19 | _db.session.remove() 20 | _db.drop_all() 21 | 22 | request.addfinalizer(teardown) 23 | return _db 24 | 25 | 26 | @pytest.fixture(scope='function') 27 | def session(db, request): 28 | connection = db.engine.connect() 29 | 30 | options = dict(bind=connection) 31 | session = db.create_scoped_session(options=options) 32 | transaction = connection.begin() 33 | 34 | db.session = session 35 | 36 | def teardown(): 37 | transaction.rollback() 38 | connection.close() 39 | session.remove() 40 | 41 | request.addfinalizer(teardown) 42 | return session 43 | -------------------------------------------------------------------------------- /tests/test_api.py: -------------------------------------------------------------------------------- 1 | from flask import url_for 2 | from urlparse import urlparse 3 | from app.models import User, Project 4 | import pytest 5 | import json 6 | 7 | 8 | def get_api_headers(): 9 | return { 10 | 'Accept': 'application/json', 11 | 'Content-Type': 'application/json' 12 | } 13 | 14 | 15 | @pytest.fixture(scope="function") 16 | def project(db): 17 | u = User.query.filter_by(username='test').first() 18 | if u is None: 19 | u = User(username='test', email='test@test.com') 20 | p = Project(name='test_project') 21 | u.projects.append(p) 22 | db.session.add(u) 23 | db.session.commit() 24 | return u.projects.first() 25 | 26 | 27 | def test_register_dsn(client, config, project): 28 | username = project.owner.username 29 | project_name = project.name 30 | token = project.token 31 | host = config['ALGALON_SERVER_HOST'] 32 | port = config['ALGALON_SERVER_PORT'] 33 | hostname = urlparse(host).hostname 34 | path = urlparse(host).path 35 | dsn = 'http://{token}@{host}:{port}{path}'.format( 36 | token=token, 37 | host=hostname, 38 | port=port, 39 | path=path) 40 | 41 | resoponse = client.get(url_for('api.register_dsn', username=username, project_name=project_name)) 42 | assert resoponse.status_code == 201 43 | assert dsn == resoponse.json['dsn'] 44 | 45 | 46 | def test_alarm(client, db, project): 47 | data = {} 48 | data['token'] = project.token 49 | data['text'] = 'test' 50 | data['title'] = 'testtitle' 51 | data['emails'] = ['test1@test.com', 'test2@test.com'] 52 | 53 | response = client.post(url_for('api.alarm'), 54 | headers=get_api_headers(), 55 | data=json.dumps(data)) 56 | assert response.status_code == 201 57 | -------------------------------------------------------------------------------- /tests/test_basics.py: -------------------------------------------------------------------------------- 1 | from flask import current_app 2 | import pytest 3 | 4 | 5 | @pytest.mark.app(debug=False) 6 | def test_app(app): 7 | assert not app.debug, 'Ensure the app not in debug mode' 8 | 9 | 10 | def test_app_exist(app): 11 | assert current_app is not None 12 | 13 | 14 | def test_app_is_testing(app): 15 | assert current_app.config['TESTING'] 16 | -------------------------------------------------------------------------------- /tests/test_main.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | from flask import url_for 4 | from app.models import User, Project, ToEmail 5 | import pytest 6 | import json 7 | 8 | 9 | @pytest.fixture(scope="function") 10 | def user(db): 11 | u = User.query.filter_by(username='test').first() 12 | if u is None: 13 | u = User(username='test', email='test@test.com') 14 | p = Project(name='test_project') 15 | u.projects.append(p) 16 | db.session.add(u) 17 | db.session.commit() 18 | return u 19 | 20 | 21 | def set_user_cookie(client, config, user): 22 | user_dict = {'username': user.username, 'email': user.email} 23 | client.set_cookie('localhost', config['OPENID2_PROFILE_COOKIE_NAME'], json.dumps(user_dict)) 24 | 25 | 26 | def test_index(client): 27 | response = client.get(url_for('main.index')) 28 | assert response.status_code == 200 29 | assert 'Stranger' in response.get_data(as_text=True) 30 | 31 | 32 | def test_user(client, config, user): 33 | set_user_cookie(client, config, user) 34 | 35 | response = client.get(url_for('main.user', username='wrong_test'), follow_redirects=True) 36 | assert response.status_code == 403 37 | 38 | response = client.get(url_for('main.user', username='test'), follow_redirects=True) 39 | assert response.status_code == 200 40 | assert 'test' in response.get_data(as_text=True) 41 | assert 'test_project' in response.get_data(as_text=True) 42 | 43 | p = user.projects.first() 44 | token = p.token 45 | assert token in response.get_data(as_text=True) 46 | response = client.get(url_for('main.reset_token', username=user.username, project_name=p.name)) 47 | assert token not in response.get_data(as_text=True) 48 | 49 | new_project_name = 'test2' 50 | assert new_project_name not in response.get_data(as_text=True) 51 | response = client.post(url_for('main.create_user_project', username=user.username), 52 | data={'name': new_project_name}, 53 | follow_redirects=True) 54 | assert new_project_name in response.get_data(as_text=True) 55 | 56 | response = client.get(url_for('main.delete_project', username=user.username, project_name=new_project_name)) 57 | assert new_project_name not in response.get_data(as_text=True) 58 | 59 | 60 | def test_user_project(client, config, user): 61 | set_user_cookie(client, config, user) 62 | username = user.username 63 | project_name = user.projects.first().name 64 | response = client.get(url_for('main.user_project', username=username, project_name=project_name)) 65 | assert u'应用程序: {0} 的报警信息'.format(project_name) in response.get_data(as_text=True) 66 | 67 | 68 | def test_to_emails(client, config, user): 69 | set_user_cookie(client, config, user) 70 | username = user.username 71 | project_name = user.projects.first().name 72 | response = client.get(url_for('main.show_to_emails', username=username, project_name=project_name)) 73 | assert u'接收报警邮件列表' in response.get_data(as_text=True) 74 | 75 | response = client.post(url_for('main.create_to_email', username=user.username, project_name=project_name), 76 | data={'addresses': 'test@email.com, test1@email.com'}, 77 | follow_redirects=True) 78 | assert 'test@email.com' in response.get_data(as_text=True) 79 | assert 'test1@email.com' in response.get_data(as_text=True) 80 | 81 | to_email = ToEmail.query.filter_by(address='test1@email.com').first() 82 | response = client.get(url_for('main.delete_to_email', username=user.username, project_name=project_name, to_email_id=to_email.id)) 83 | assert 'test1@email.com' not in response.get_data(as_text=True) 84 | -------------------------------------------------------------------------------- /tests/test_product_model.py: -------------------------------------------------------------------------------- 1 | from app.models import User, Project 2 | import pytest 3 | 4 | 5 | @pytest.fixture(scope="function") 6 | def user(db): 7 | u = User.query.filter_by(username='test').first() 8 | if u is None: 9 | u = User(username='test', email='test@test.com') 10 | db.session.add(u) 11 | db.session.commit() 12 | return u 13 | 14 | 15 | def test_create_project(db, user): 16 | p = Project(name='test_project') 17 | assert user.projects.count() == 0 18 | user.projects.append(p) 19 | db.session.add(user) 20 | db.session.commit() 21 | assert user.projects.count() == 1 22 | 23 | 24 | def test_reset_token(db, user): 25 | p = user.projects.first() 26 | old_token = p.token 27 | p.reset_token() 28 | new_token = p.token 29 | assert old_token != new_token 30 | 31 | 32 | def test_delete_project(db, user): 33 | assert user.projects.count() == 1 34 | p = user.projects.first() 35 | user.projects.remove(p) 36 | db.session.add(user) 37 | db.session.commit() 38 | assert user.projects.count() == 0 39 | -------------------------------------------------------------------------------- /tests/test_user_model.py: -------------------------------------------------------------------------------- 1 | from app.models import User 2 | import pytest 3 | 4 | 5 | @pytest.fixture(scope='module') 6 | def user(): 7 | user = User(username='dq', email='dq@example.com') 8 | return user 9 | 10 | 11 | def test_create_user(db, user): 12 | assert User.query.count() == 0 13 | db.session.add(user) 14 | db.session.commit() 15 | assert User.query.count() == 1 16 | 17 | 18 | def test_delete_user(db, user): 19 | assert User.query.count() == 1 20 | db.session.delete(user) 21 | db.session.commit() 22 | assert User.query.count() == 0 23 | --------------------------------------------------------------------------------
Email Address添加时间
27 | {{ email.address }} 28 | 30 | {{ moment(email.date_added).format('LLL') }} 31 | 33 | 34 | 删除 35 | 36 |