├── .gitattributes ├── .gitignore ├── FDataBase.py ├── UserLogin.py ├── flsite.db ├── flsite.py ├── framework-flask-intro.htm ├── sq_db.sql ├── static ├── css │ └── styles.css ├── images │ └── ava.png ├── images_html │ └── framework-flask-intro.files │ │ ├── image001.jpg │ │ ├── image002.jpg │ │ ├── image003.jpg │ │ └── image004.jpg └── js │ └── jquery-1.9.0.min.js └── templates ├── about.html ├── add_post.html ├── base.html ├── contact.html ├── index.html ├── login.html ├── page404.html ├── post.html └── register.html /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # celery beat schedule file 95 | celerybeat-schedule 96 | 97 | # SageMath parsed files 98 | *.sage.py 99 | 100 | # Environments 101 | .env 102 | .venv 103 | env/ 104 | venv/ 105 | ENV/ 106 | env.bak/ 107 | venv.bak/ 108 | 109 | # Spyder project settings 110 | .spyderproject 111 | .spyproject 112 | 113 | # Rope project settings 114 | .ropeproject 115 | 116 | # mkdocs documentation 117 | /site 118 | 119 | # mypy 120 | .mypy_cache/ 121 | .dmypy.json 122 | dmypy.json 123 | 124 | # Pyre type checker 125 | .pyre/ 126 | -------------------------------------------------------------------------------- /FDataBase.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | import time 3 | import math 4 | import re 5 | from flask import url_for 6 | 7 | class FDataBase: 8 | def __init__(self, db): 9 | self.__db = db 10 | self.__cur = db.cursor() 11 | 12 | def getMenu(self): 13 | sql = '''SELECT * FROM mainmenu''' 14 | try: 15 | self.__cur.execute(sql) 16 | res = self.__cur.fetchall() 17 | if res: return res 18 | except: 19 | print("Ошибка чтения из БД") 20 | return [] 21 | 22 | def addPost(self, title, text, url): 23 | try: 24 | self.__cur.execute(f"SELECT COUNT() as `count` FROM posts WHERE url LIKE '{url}'") 25 | res = self.__cur.fetchone() 26 | if res['count'] > 0: 27 | print("Статья с таким url уже существует") 28 | return False 29 | 30 | base = url_for('static', filename='images_html') 31 | 32 | text = re.sub(r"(?P]*src=)(?P[\"'])(?P.+?)(?P=quote)>", 33 | "\\g" + base + "/\\g>", 34 | text) 35 | 36 | tm = math.floor(time.time()) 37 | self.__cur.execute("INSERT INTO posts VALUES(NULL, ?, ?, ?, ?)", (title, text, url, tm)) 38 | self.__db.commit() 39 | except sqlite3.Error as e: 40 | print("Ошибка добавления статьи в БД "+str(e)) 41 | return False 42 | 43 | return True 44 | 45 | def getPost(self, alias): 46 | try: 47 | self.__cur.execute(f"SELECT title, text FROM posts WHERE url LIKE '{alias}' LIMIT 1") 48 | res = self.__cur.fetchone() 49 | if res: 50 | return res 51 | except sqlite3.Error as e: 52 | print("Ошибка получения статьи из БД "+str(e)) 53 | 54 | return (False, False) 55 | 56 | def getPostsAnonce(self): 57 | try: 58 | self.__cur.execute(f"SELECT id, title, text, url FROM posts ORDER BY time DESC") 59 | res = self.__cur.fetchall() 60 | if res: return res 61 | except sqlite3.Error as e: 62 | print("Ошибка получения статьи из БД "+str(e)) 63 | 64 | return [] 65 | 66 | def addUser(self, name, email, hpsw): 67 | try: 68 | self.__cur.execute(f"SELECT COUNT() as `count` FROM users WHERE email LIKE '{email}'") 69 | res = self.__cur.fetchone() 70 | if res['count'] > 0: 71 | print("Пользователь с таким email уже существует") 72 | return False 73 | 74 | tm = math.floor(time.time()) 75 | self.__cur.execute("INSERT INTO users VALUES(NULL, ?, ?, ?, ?)", (name, email, hpsw, tm)) 76 | self.__db.commit() 77 | except sqlite3.Error as e: 78 | print("Ошибка добавления пользователя в БД "+str(e)) 79 | return False 80 | 81 | return True 82 | 83 | def getUser(self, user_id): 84 | try: 85 | self.__cur.execute(f"SELECT * FROM users WHERE id = {user_id} LIMIT 1") 86 | res = self.__cur.fetchone() 87 | if not res: 88 | print("Пользователь не найден") 89 | return False 90 | 91 | return res 92 | except sqlite3.Error as e: 93 | print("Ошибка получения данных из БД "+str(e)) 94 | 95 | return False 96 | 97 | def getUserByEmail(self, email): 98 | try: 99 | self.__cur.execute(f"SELECT * FROM users WHERE email = '{email}' LIMIT 1") 100 | res = self.__cur.fetchone() 101 | if not res: 102 | print("Пользователь не найден") 103 | return False 104 | 105 | return res 106 | except sqlite3.Error as e: 107 | print("Ошибка получения данных из БД "+str(e)) 108 | 109 | return False -------------------------------------------------------------------------------- /UserLogin.py: -------------------------------------------------------------------------------- 1 | from flask_login import UserMixin 2 | 3 | class UserLogin(UserMixin): 4 | def fromDB(self, user_id, db): 5 | self.__user = db.getUser(user_id) 6 | return self 7 | 8 | def create(self, user): 9 | self.__user = user 10 | return self 11 | 12 | def get_id(self): 13 | return str(self.__user['id']) 14 | -------------------------------------------------------------------------------- /flsite.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/selfedu-rus/flasksite-16/f2f5f7db4f9ce9e25c027af40e0b996905e85e14/flsite.db -------------------------------------------------------------------------------- /flsite.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | import os 3 | from flask import Flask, render_template, request, g, flash, abort, redirect, url_for 4 | from FDataBase import FDataBase 5 | from werkzeug.security import generate_password_hash, check_password_hash 6 | from flask_login import LoginManager, login_user, login_required, logout_user, current_user 7 | from UserLogin import UserLogin 8 | 9 | # конфигурация 10 | DATABASE = '/tmp/flsite.db' 11 | DEBUG = True 12 | SECRET_KEY = 'fdgfh78@#5?>gfhf89dx,v06k' 13 | 14 | app = Flask(__name__) 15 | app.config.from_object(__name__) 16 | app.config.update(dict(DATABASE=os.path.join(app.root_path,'flsite.db'))) 17 | 18 | login_manager = LoginManager(app) 19 | login_manager.login_view = 'login' 20 | login_manager.login_message = "Авторизуйтесь для доступа к закрытым страницам" 21 | login_manager.login_message_category = "success" 22 | 23 | 24 | @login_manager.user_loader 25 | def load_user(user_id): 26 | print("load_user") 27 | return UserLogin().fromDB(user_id, dbase) 28 | 29 | 30 | def connect_db(): 31 | conn = sqlite3.connect(app.config['DATABASE']) 32 | conn.row_factory = sqlite3.Row 33 | return conn 34 | 35 | def create_db(): 36 | """Вспомогательная функция для создания таблиц БД""" 37 | db = connect_db() 38 | with app.open_resource('sq_db.sql', mode='r') as f: 39 | db.cursor().executescript(f.read()) 40 | db.commit() 41 | db.close() 42 | 43 | def get_db(): 44 | '''Соединение с БД, если оно еще не установлено''' 45 | if not hasattr(g, 'link_db'): 46 | g.link_db = connect_db() 47 | return g.link_db 48 | 49 | 50 | dbase = None 51 | @app.before_request 52 | def before_request(): 53 | """Установление соединения с БД перед выполнением запроса""" 54 | global dbase 55 | db = get_db() 56 | dbase = FDataBase(db) 57 | 58 | 59 | @app.teardown_appcontext 60 | def close_db(error): 61 | '''Закрываем соединение с БД, если оно было установлено''' 62 | if hasattr(g, 'link_db'): 63 | g.link_db.close() 64 | 65 | 66 | @app.route("/") 67 | def index(): 68 | return render_template('index.html', menu=dbase.getMenu(), posts=dbase.getPostsAnonce()) 69 | 70 | @app.route("/add_post", methods=["POST", "GET"]) 71 | def addPost(): 72 | if request.method == "POST": 73 | if len(request.form['name']) > 4 and len(request.form['post']) > 10: 74 | res = dbase.addPost(request.form['name'], request.form['post'], request.form['url']) 75 | if not res: 76 | flash('Ошибка добавления статьи', category = 'error') 77 | else: 78 | flash('Статья добавлена успешно', category='success') 79 | else: 80 | flash('Ошибка добавления статьи', category='error') 81 | 82 | return render_template('add_post.html', menu = dbase.getMenu(), title="Добавление статьи") 83 | 84 | 85 | @app.route("/post/") 86 | @login_required 87 | def showPost(alias): 88 | title, post = dbase.getPost(alias) 89 | if not title: 90 | abort(404) 91 | 92 | return render_template('post.html', menu=dbase.getMenu(), title=title, post=post) 93 | 94 | @app.route("/login", methods=["POST", "GET"]) 95 | def login(): 96 | if current_user.is_authenticated: 97 | return redirect(url_for('profile')) 98 | 99 | if request.method == "POST": 100 | user = dbase.getUserByEmail(request.form['email']) 101 | if user and check_password_hash(user['psw'], request.form['psw']): 102 | userlogin = UserLogin().create(user) 103 | rm = True if request.form.get('remainme') else False 104 | login_user(userlogin, remember=rm) 105 | return redirect(request.args.get("next") or url_for("profile")) 106 | 107 | flash("Неверная пара логин/пароль", "error") 108 | 109 | return render_template("login.html", menu=dbase.getMenu(), title="Авторизация") 110 | 111 | 112 | @app.route("/register", methods=["POST", "GET"]) 113 | def register(): 114 | if request.method == "POST": 115 | if len(request.form['name']) > 4 and len(request.form['email']) > 4 \ 116 | and len(request.form['psw']) > 4 and request.form['psw'] == request.form['psw2']: 117 | hash = generate_password_hash(request.form['psw']) 118 | res = dbase.addUser(request.form['name'], request.form['email'], hash) 119 | if res: 120 | flash("Вы успешно зарегистрированы", "success") 121 | return redirect(url_for('login')) 122 | else: 123 | flash("Ошибка при добавлении в БД", "error") 124 | else: 125 | flash("Неверно заполнены поля", "error") 126 | 127 | return render_template("register.html", menu=dbase.getMenu(), title="Регистрация") 128 | 129 | 130 | @app.route('/logout') 131 | @login_required 132 | def logout(): 133 | logout_user() 134 | flash("Вы вышли из аккаунта", "success") 135 | return redirect(url_for('login')) 136 | 137 | 138 | @app.route('/profile') 139 | @login_required 140 | def profile(): 141 | return f"""

Выйти из профиля 142 |

user info: {current_user.get_id()}""" 143 | 144 | 145 | if __name__ == "__main__": 146 | app.run(debug=True) 147 | -------------------------------------------------------------------------------- /framework-flask-intro.htm: -------------------------------------------------------------------------------- 1 |

Для тех кто не 2 | совсем знаком с принципом взаимодействия между клиентом (браузером) и 3 | фреймворком, установленном на сервере, опишу в двух словах этот процесс. 4 | 5 |

Когда 6 | пользователь вводит в браузер строку запроса, например, vk.com, то от браузера 7 | отправляется запрос к серверу, где расположен и работает этот сайт. Здесь мы 8 | отложим в сторону вопрос маршрутизации и DNS-серверов, 9 | сейчас это неважно, главное, что сеть Интернет так устроена, что маршрутизаторы 10 | «знают» куда направлять запросы, если они относятся к работающим сайтам. 11 | 12 |

13 | 14 |

Сервер постоянно 15 | находится в режиме ожидания очередного запроса и как только он приходит, 16 | формирует ответ клиенту, как правило, в виде HTML-документа. Этот 17 | документ возвращается в браузер и пользователь видит на экране устройства 18 | заветную страницу. 19 | 20 |

Но где же во 21 | всей этой схеме фреймворк? В действительности он установлен на сервере. Так как 22 | это обычный компьютер (ну может не совсем обычный, но принцип тот же), то на 23 | нем установлено соответствующее программное обеспечение. Мы, опять же, не будем 24 | здесь глубоко вдаваться в подробности, скажу лишь, что на них часто 25 | устанавливают Linux-подобные ОС 26 | (благодаря их надежности), затем программу под названием веб-сервер (часто это Apache или Nginx) и уже он 27 | отдает обработку запроса конкретному фреймворку: 28 | 29 |

30 | 31 |

Здесь WSGI (Web Server Gateway Interface) — стандарт 32 | взаимодействия между Python-программой, выполняющейся на 33 | стороне сервера, и самим веб-сервером, например Apache. Фактически, 34 | это интерпретатор Python, который запускает WSGI-приложение, 35 | написанное на Flask. 36 | 37 |

При поступлении 38 | запроса активизируется WSGI-приложение, выполняется определенный 39 | обработчик, который еще называется «Представление» и реализованный в виде 40 | функции на языке Python. Соответственно, если приходит сразу 41 | несколько запросов, то одна и та же функция-обработчик может быть запущена в 42 | параллельных потоках. Многопоточность – это норма для фреймворков, поэтому, 43 | работая с представлениями во Flask, всегда следует это учитывать. 44 | 45 |

Конечно, в 46 | рамках наших занятий мы не будем использовать удаленный сервер и устанавливать 47 | на него данный фреймворк – это отдельная задача. Кстати, современные хостеры 48 | предоставляют инструментарий для простой установки и настройки Flask. Поэтому этот 49 | процесс не представляет больших сложностей. А для изучения данного пакета на 50 | домашнем ПК от вас потребуется только его установить, используя установщик pip: 51 | 52 |

pip install Flask 53 | 54 |

Теперь мы можем 55 | написать свое первое WSGI-приложение. В самом простом варианте 56 | оно выглядит так: 57 | 58 |

from flask import Flask 59 |
60 |
app = Flask(__name__) 61 |
62 |
if __name__ == "__main__": 63 |
    app.run(debug=True) 64 | 65 |

Вначале идет 66 | импорт класса Flask, который, 67 | фактически и формирует это приложение. Далее, мы создаем экземпляр этого класса 68 | и первым аргументом должны указать имя нашего приложения. Если вся программа 69 | пишется в одном файле, то следует передавать директиву __name__, которая в 70 | случае импорта будет содержать имя текущего файла, а в случае самостоятельного 71 | запуска – значение «__main__». Для Flask это имеет 72 | принципиальное значение, в частности, от этого зависит где искать подкаталоги с 73 | шаблонами и статичными документами. 74 | 75 |

После этого 76 | выполняется запуск фреймворка методом run и в качестве параметра 77 | указывается debug=True, чтобы мы в браузере 78 | видели все ошибки, которые будут возникать при разработке сайта-приложения. Конечно, 79 | после его создания, здесь следует прописать debug=False, чтобы 80 | случайные ошибки реальный пользователь уже не видел. 81 | 82 |

И, наконец, 83 | условие. Зачем оно? Смотрите, когда мы непосредственно запускаем наш модуль, то 84 | директива __name__ будет 85 | принимать значение «__main__» и будет запущен локальный веб-сервер 86 | для отладки текущего приложения. Если же модуль запускается, например, на 87 | удаленном сервере, то нам не нужно запускать еще один сервер. В этом случае 88 | директива __name__ будет 89 | принимать имя данного модуля и строчка app.run выполнена не 90 | будет. То есть, мы это условие прописали с целью запуска приложения 91 | непосредственно на локальном устройстве. 92 | 93 |

Соответственно, как 94 | только фреймворк запущен, у нас активизируется локальный веб-сервер и мы можем 95 | в браузере создавать запрос, используя вот такой начальный адрес: 96 | 97 |

http://127.0.0.1:5000/ 98 | 99 |

Давайте сделаем 100 | это и посмотрим, что получится. Наберем в браузере указанный запрос и видим, 101 | что запрашиваемая страница не найдена: 102 | 103 |

104 | 105 |

Все верно, так и 106 | должно быть, так как мы в программе не создали еще ни одного представления. 107 | Сделаем и это, добавим его: 108 | 109 |

@app.route("/") 110 |
def index(): 111 |
    return "index" 112 | 113 |

Здесь 114 | используется специальный декоратор route, который 115 | создает обертку вокруг нашей функции index, которая будет 116 | активизироваться при обращении к главной странице сайта, то есть, по запросу http://127.0.0.1:5000/ 117 | 118 |

Запустим 119 | программу, обновим страницу и теперь в браузере видим то, что возвратила 120 | функция index: 121 | 122 |

123 | 124 |

Часто к главной 125 | странице обращаются еще по index, то есть: 126 | 127 |

domain/index 128 | 129 |

например, 130 | 131 |

proproprogs.ru/index 132 | 133 |

Чтобы одну и ту 134 | же страницу отобразить по нескольким URL-адресам, следует добавить 135 | несколько конструкций route: 136 | 137 |

@app.route("/index") 138 |
@app.route("/") 139 |
def index(): 140 |
    return "index" 141 | 142 |

Соответственно, 143 | для любого другого адреса мы также можем добавить свой отдельный обработчик, 144 | прописав еще один декоратор route: 145 | 146 |

@app.route("/about") 147 |
def about(): 148 |
    return "<h1>О сайте</h1>" 149 | 150 |

Теперь на нашем 151 | сайте как бы две страницы: главная и /about – о сайте. Причем, 152 | наши обработчики возвращают HTML-документ и все теги будут 153 | соответственно отображаться на странице в браузере. 154 | 155 | -------------------------------------------------------------------------------- /sq_db.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS mainmenu ( 2 | id integer PRIMARY KEY AUTOINCREMENT, 3 | title text NOT NULL, 4 | url text NOT NULL 5 | ); 6 | 7 | CREATE TABLE IF NOT EXISTS posts ( 8 | id integer PRIMARY KEY AUTOINCREMENT, 9 | title text NOT NULL, 10 | text text NOT NULL, 11 | url text NOT NULL, 12 | time integer NOT NULL 13 | ); 14 | 15 | CREATE TABLE IF NOT EXISTS users ( 16 | id integer PRIMARY KEY AUTOINCREMENT, 17 | name text NOT NULL, 18 | email text NOT NULL, 19 | psw text NOT NULL, 20 | time integer NOT NULL 21 | ); 22 | -------------------------------------------------------------------------------- /static/css/styles.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | font-family: 'Arial'; 3 | margin: 0; 4 | padding: 0; 5 | height: 100%; 6 | width: 100%; 7 | } 8 | 9 | a { 10 | color: #0059b2; 11 | text-decoration: none; 12 | } 13 | a:hover { 14 | color: #CC0000; 15 | text-decoration: underline; 16 | } 17 | 18 | .clear {clear: both;} 19 | 20 | ul.mainmenu { 21 | list-style: none; 22 | background: #3F4137; 23 | height: 60px; 24 | margin: 0; 25 | padding: 0; 26 | color: #fdc073; 27 | font-size: 20px; 28 | overflow: hidden; 29 | } 30 | ul.mainmenu li { 31 | float: left; 32 | margin: 18px 40px 0 50px; 33 | } 34 | ul.mainmenu li.last {float: right;} 35 | 36 | ul.mainmenu li a { 37 | color: #fdc073; 38 | text-decoration: none; 39 | } 40 | ul.mainmenu li a:hover { 41 | color: #FDA83D; 42 | } 43 | 44 | div.content { 45 | margin: 10px; 46 | } 47 | 48 | .form-contact label { 49 | display: inline-block; 50 | min-width: 80px; 51 | } 52 | .form-contact p {margin: 10px 0 10px 0;} 53 | .form-contact input[type=submit], textarea { 54 | font-size: 16px; 55 | } 56 | 57 | .flash {padding: 10px;} 58 | .flash.success { 59 | border: 1px solid #21DB56; 60 | background: #AEFFC5; 61 | } 62 | .flash.error { 63 | border: 1px solid #FF4343; 64 | background: #FF9C9C; 65 | } 66 | 67 | ul.list-posts { 68 | list-style: none; 69 | margin: 0; 70 | padding: 0; 71 | max-width: 600px; 72 | } 73 | ul.list-posts li { 74 | margin: 20px 0 0 0; 75 | border: 1px solid #eee; 76 | } 77 | ul.list-posts .title { 78 | margin: 0; 79 | padding: 5px; 80 | background: #eee; 81 | } 82 | ul.list-posts .annonce { 83 | margin: 0; 84 | padding: 10px 5px 10px 5px; 85 | } -------------------------------------------------------------------------------- /static/images/ava.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/selfedu-rus/flasksite-16/f2f5f7db4f9ce9e25c027af40e0b996905e85e14/static/images/ava.png -------------------------------------------------------------------------------- /static/images_html/framework-flask-intro.files/image001.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/selfedu-rus/flasksite-16/f2f5f7db4f9ce9e25c027af40e0b996905e85e14/static/images_html/framework-flask-intro.files/image001.jpg -------------------------------------------------------------------------------- /static/images_html/framework-flask-intro.files/image002.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/selfedu-rus/flasksite-16/f2f5f7db4f9ce9e25c027af40e0b996905e85e14/static/images_html/framework-flask-intro.files/image002.jpg -------------------------------------------------------------------------------- /static/images_html/framework-flask-intro.files/image003.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/selfedu-rus/flasksite-16/f2f5f7db4f9ce9e25c027af40e0b996905e85e14/static/images_html/framework-flask-intro.files/image003.jpg -------------------------------------------------------------------------------- /static/images_html/framework-flask-intro.files/image004.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/selfedu-rus/flasksite-16/f2f5f7db4f9ce9e25c027af40e0b996905e85e14/static/images_html/framework-flask-intro.files/image004.jpg -------------------------------------------------------------------------------- /static/js/jquery-1.9.0.min.js: -------------------------------------------------------------------------------- 1 | /*! jQuery v1.9.0 | (c) 2005, 2012 jQuery Foundation, Inc. | jquery.org/license */(function(e,t){"use strict";function n(e){var t=e.length,n=st.type(e);return st.isWindow(e)?!1:1===e.nodeType&&t?!0:"array"===n||"function"!==n&&(0===t||"number"==typeof t&&t>0&&t-1 in e)}function r(e){var t=Tt[e]={};return st.each(e.match(lt)||[],function(e,n){t[n]=!0}),t}function i(e,n,r,i){if(st.acceptData(e)){var o,a,s=st.expando,u="string"==typeof n,l=e.nodeType,c=l?st.cache:e,f=l?e[s]:e[s]&&s;if(f&&c[f]&&(i||c[f].data)||!u||r!==t)return f||(l?e[s]=f=K.pop()||st.guid++:f=s),c[f]||(c[f]={},l||(c[f].toJSON=st.noop)),("object"==typeof n||"function"==typeof n)&&(i?c[f]=st.extend(c[f],n):c[f].data=st.extend(c[f].data,n)),o=c[f],i||(o.data||(o.data={}),o=o.data),r!==t&&(o[st.camelCase(n)]=r),u?(a=o[n],null==a&&(a=o[st.camelCase(n)])):a=o,a}}function o(e,t,n){if(st.acceptData(e)){var r,i,o,a=e.nodeType,u=a?st.cache:e,l=a?e[st.expando]:st.expando;if(u[l]){if(t&&(r=n?u[l]:u[l].data)){st.isArray(t)?t=t.concat(st.map(t,st.camelCase)):t in r?t=[t]:(t=st.camelCase(t),t=t in r?[t]:t.split(" "));for(i=0,o=t.length;o>i;i++)delete r[t[i]];if(!(n?s:st.isEmptyObject)(r))return}(n||(delete u[l].data,s(u[l])))&&(a?st.cleanData([e],!0):st.support.deleteExpando||u!=u.window?delete u[l]:u[l]=null)}}}function a(e,n,r){if(r===t&&1===e.nodeType){var i="data-"+n.replace(Nt,"-$1").toLowerCase();if(r=e.getAttribute(i),"string"==typeof r){try{r="true"===r?!0:"false"===r?!1:"null"===r?null:+r+""===r?+r:wt.test(r)?st.parseJSON(r):r}catch(o){}st.data(e,n,r)}else r=t}return r}function s(e){var t;for(t in e)if(("data"!==t||!st.isEmptyObject(e[t]))&&"toJSON"!==t)return!1;return!0}function u(){return!0}function l(){return!1}function c(e,t){do e=e[t];while(e&&1!==e.nodeType);return e}function f(e,t,n){if(t=t||0,st.isFunction(t))return st.grep(e,function(e,r){var i=!!t.call(e,r,e);return i===n});if(t.nodeType)return st.grep(e,function(e){return e===t===n});if("string"==typeof t){var r=st.grep(e,function(e){return 1===e.nodeType});if(Wt.test(t))return st.filter(t,r,!n);t=st.filter(t,r)}return st.grep(e,function(e){return st.inArray(e,t)>=0===n})}function p(e){var t=zt.split("|"),n=e.createDocumentFragment();if(n.createElement)for(;t.length;)n.createElement(t.pop());return n}function d(e,t){return e.getElementsByTagName(t)[0]||e.appendChild(e.ownerDocument.createElement(t))}function h(e){var t=e.getAttributeNode("type");return e.type=(t&&t.specified)+"/"+e.type,e}function g(e){var t=nn.exec(e.type);return t?e.type=t[1]:e.removeAttribute("type"),e}function m(e,t){for(var n,r=0;null!=(n=e[r]);r++)st._data(n,"globalEval",!t||st._data(t[r],"globalEval"))}function y(e,t){if(1===t.nodeType&&st.hasData(e)){var n,r,i,o=st._data(e),a=st._data(t,o),s=o.events;if(s){delete a.handle,a.events={};for(n in s)for(r=0,i=s[n].length;i>r;r++)st.event.add(t,n,s[n][r])}a.data&&(a.data=st.extend({},a.data))}}function v(e,t){var n,r,i;if(1===t.nodeType){if(n=t.nodeName.toLowerCase(),!st.support.noCloneEvent&&t[st.expando]){r=st._data(t);for(i in r.events)st.removeEvent(t,i,r.handle);t.removeAttribute(st.expando)}"script"===n&&t.text!==e.text?(h(t).text=e.text,g(t)):"object"===n?(t.parentNode&&(t.outerHTML=e.outerHTML),st.support.html5Clone&&e.innerHTML&&!st.trim(t.innerHTML)&&(t.innerHTML=e.innerHTML)):"input"===n&&Zt.test(e.type)?(t.defaultChecked=t.checked=e.checked,t.value!==e.value&&(t.value=e.value)):"option"===n?t.defaultSelected=t.selected=e.defaultSelected:("input"===n||"textarea"===n)&&(t.defaultValue=e.defaultValue)}}function b(e,n){var r,i,o=0,a=e.getElementsByTagName!==t?e.getElementsByTagName(n||"*"):e.querySelectorAll!==t?e.querySelectorAll(n||"*"):t;if(!a)for(a=[],r=e.childNodes||e;null!=(i=r[o]);o++)!n||st.nodeName(i,n)?a.push(i):st.merge(a,b(i,n));return n===t||n&&st.nodeName(e,n)?st.merge([e],a):a}function x(e){Zt.test(e.type)&&(e.defaultChecked=e.checked)}function T(e,t){if(t in e)return t;for(var n=t.charAt(0).toUpperCase()+t.slice(1),r=t,i=Nn.length;i--;)if(t=Nn[i]+n,t in e)return t;return r}function w(e,t){return e=t||e,"none"===st.css(e,"display")||!st.contains(e.ownerDocument,e)}function N(e,t){for(var n,r=[],i=0,o=e.length;o>i;i++)n=e[i],n.style&&(r[i]=st._data(n,"olddisplay"),t?(r[i]||"none"!==n.style.display||(n.style.display=""),""===n.style.display&&w(n)&&(r[i]=st._data(n,"olddisplay",S(n.nodeName)))):r[i]||w(n)||st._data(n,"olddisplay",st.css(n,"display")));for(i=0;o>i;i++)n=e[i],n.style&&(t&&"none"!==n.style.display&&""!==n.style.display||(n.style.display=t?r[i]||"":"none"));return e}function C(e,t,n){var r=mn.exec(t);return r?Math.max(0,r[1]-(n||0))+(r[2]||"px"):t}function k(e,t,n,r,i){for(var o=n===(r?"border":"content")?4:"width"===t?1:0,a=0;4>o;o+=2)"margin"===n&&(a+=st.css(e,n+wn[o],!0,i)),r?("content"===n&&(a-=st.css(e,"padding"+wn[o],!0,i)),"margin"!==n&&(a-=st.css(e,"border"+wn[o]+"Width",!0,i))):(a+=st.css(e,"padding"+wn[o],!0,i),"padding"!==n&&(a+=st.css(e,"border"+wn[o]+"Width",!0,i)));return a}function E(e,t,n){var r=!0,i="width"===t?e.offsetWidth:e.offsetHeight,o=ln(e),a=st.support.boxSizing&&"border-box"===st.css(e,"boxSizing",!1,o);if(0>=i||null==i){if(i=un(e,t,o),(0>i||null==i)&&(i=e.style[t]),yn.test(i))return i;r=a&&(st.support.boxSizingReliable||i===e.style[t]),i=parseFloat(i)||0}return i+k(e,t,n||(a?"border":"content"),r,o)+"px"}function S(e){var t=V,n=bn[e];return n||(n=A(e,t),"none"!==n&&n||(cn=(cn||st("