├── app ├── __init__.py ├── local_settings.py.sample ├── static │ ├── css │ │ ├── index.css │ │ ├── login.css │ │ ├── base.css │ │ └── jquery.dataTables.css │ └── js │ │ ├── popper.min.js │ │ ├── bootstrap.min.js │ │ └── jquery-3.4.1.slim.min.js ├── templates │ ├── course_student.html │ ├── user_list.html │ ├── new_content.html │ ├── cluster.html │ ├── login.html │ ├── course.html │ ├── course_list.html │ ├── register.html │ ├── index.html │ ├── profile.html │ ├── base.html │ └── macro.html ├── fake_db.py ├── main.py └── model.py ├── env-var.sh ├── requirements.txt ├── LICENSE ├── README.md └── .gitignore /app/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /env-var.sh: -------------------------------------------------------------------------------- 1 | # For development only 2 | export FLASK_APP=main.py 3 | export FLASK_DEBUG=1 4 | 5 | -------------------------------------------------------------------------------- /app/local_settings.py.sample: -------------------------------------------------------------------------------- 1 | MYSQL_HOST = 'localhost' 2 | MYSQL_USER = 'root' 3 | MYSQL_PASSWORD = '1234' 4 | MYSQL_DB = 'lms' 5 | -------------------------------------------------------------------------------- /app/static/css/index.css: -------------------------------------------------------------------------------- 1 | .course-link { 2 | text-decoration: none; 3 | } 4 | 5 | .course-link:hover { 6 | text-decoration: none; 7 | } -------------------------------------------------------------------------------- /app/static/css/login.css: -------------------------------------------------------------------------------- 1 | .form-signin { 2 | width: 100%; 3 | max-width: 430px; 4 | padding: 15px; 5 | margin: 100px auto; 6 | text-align: center; 7 | } -------------------------------------------------------------------------------- /app/static/css/base.css: -------------------------------------------------------------------------------- 1 | .navbar-right-ul { 2 | text-align: right; 3 | } 4 | 5 | .flash { 6 | margin-top: 20px; 7 | } 8 | 9 | .my-container { 10 | margin-bottom: 100px; 11 | } -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | click==7.1.2 2 | Faker==5.6.1 3 | Flask==1.1.2 4 | Flask-Login==0.5.0 5 | itsdangerous==1.1.0 6 | Jinja2==2.11.2 7 | MarkupSafe==1.1.1 8 | mysql-connector-python==8.0.23 9 | protobuf==3.14.0 10 | python-dateutil==2.8.1 11 | six==1.15.0 12 | text-unidecode==1.3 13 | Werkzeug==1.0.1 14 | -------------------------------------------------------------------------------- /app/templates/course_student.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% from 'macro.html' import table, page_header %} 4 | 5 | {% block title %} 6 | LMS - Course Students 7 | {% endblock title %} 8 | 9 | {% block content %} 10 | {{ page_header("Student List") }} 11 | {{ table(li=student_list, button=false) }} 12 | {% endblock content %} 13 | 14 | {% block script %} 15 | {{ super() }} 16 | 21 | {% endblock script %} -------------------------------------------------------------------------------- /app/templates/user_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% from 'macro.html' import table, page_header %} 4 | 5 | {% block title %} 6 | LMS - Users 7 | {% endblock title %} 8 | 9 | {% block content %} 10 | {{ page_header("User List") }} 11 | {{ table(li=user_list, button_url="profile", button_text="Edit", button_color="primary") }} 12 | {% endblock content %} 13 | 14 | {% block script %} 15 | {{ super() }} 16 | 21 | {% endblock script %} -------------------------------------------------------------------------------- /app/fake_db.py: -------------------------------------------------------------------------------- 1 | from faker import Faker 2 | import model 3 | 4 | fake = Faker() 5 | 6 | for i in range(10000): 7 | profile = fake.profile() 8 | success, message = model.create_user( 9 | Username=profile["name"].replace(" ", "_"), 10 | Password="rootroot", 11 | FirstName=''.join(profile["name"].split(" ")[:-1]), 12 | LastName=profile["name"].split(" ")[-1], 13 | PhoneNumber=fake.phone_number(), 14 | Email=profile["mail"], 15 | Faculty=profile['job'], 16 | Institution=profile['company'], 17 | Address=profile['address'] 18 | ) 19 | if not success: 20 | print(message) 21 | -------------------------------------------------------------------------------- /app/templates/new_content.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% from 'macro.html' import page_header, input, text_area %} 4 | 5 | {% block title %} 6 | LMS - New Content 7 | {% endblock title %} 8 | 9 | {% block content %} 10 | 11 | {{ page_header("New Content") }} 12 | 13 |
14 | {{ input(name="Title", label="Title", required=true) }} 15 | {{ text_area(name="TextContent", label="Content", required=false, rows="12") }} 16 | 17 |
18 | 19 | 20 | {% endblock content %} -------------------------------------------------------------------------------- /app/templates/cluster.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% from 'macro.html' import page_header, table %} 4 | 5 | {% block title %} 6 | LMS - Clusters 7 | {% endblock title %} 8 | 9 | {% block content %} 10 | {{ page_header("Cluster List") }} 11 |
12 |
13 |
14 | 15 |
16 |
17 | 18 |
19 |
20 |
21 | {{ table(li=cluster_list, button=false) }} 22 | {% endblock content %} 23 | 24 | {% block script %} 25 | {{ super() }} 26 | 31 | {% endblock script %} -------------------------------------------------------------------------------- /app/templates/login.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block navbar %} 4 | {% endblock navbar %} 5 | 6 | {% block stylesheet %} 7 | {{ super() }} 8 | 9 | {% endblock stylesheet %} 10 | 11 | {% block title %} 12 | LMS - Login 13 | {% endblock title %} 14 | 15 | {% block content %} 16 |
17 |

LMS

18 |

Please sign in

19 |
20 | 21 |
22 |
23 | 24 |
25 | 26 | Register 27 |
28 | {% endblock content %} -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | Copyright (c) 2021 Seyed Mohammad Mousavi 3 | 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 9 | of the Software, and to permit persons to whom the Software is furnished to do 10 | so, subject to the following conditions: 11 | 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | -------------------------------------------------------------------------------- /app/templates/course.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% from 'macro.html' import page_header %} 4 | 5 | {% block title %} 6 | LMS - {{ course_data["Course Name"] }} 7 | {% endblock title %} 8 | 9 | {% block content %} 10 | {% if is_teacher %} 11 |
12 |
13 | Students 14 |
15 |
16 | New Content 17 |
18 |
19 | {% endif %} 20 | 21 | {{ page_header(course_data["Course Name"]) }} 22 | 23 | {% for content in content_list %} 24 |
25 |
26 |

27 | {{ content["Title"] }} 28 |

29 |
30 | {{ content["TextContent"] }} 31 |
32 |
33 | Date: {{ content["CreatedAt"] }} 34 |
35 |
36 |
37 | {% endfor %} 38 | 39 | {% endblock content %} -------------------------------------------------------------------------------- /app/templates/course_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% from 'macro.html' import page_header, table, input, input_select %} 4 | 5 | {% block title %} 6 | LMS - Courses 7 | {% endblock title %} 8 | 9 | {% block content %} 10 | {{ page_header("Course List") }} 11 |
12 |
13 |
14 | 15 |
16 |
17 | {{ input_select(name="TeacherID", li=user_list, value="ID", value_name="Username", label="Teacher", required=true, default_value_name="Select Teacher", have_label=false) }} 18 |
19 |
20 | {{ input_select(name="ClusterID", li=cluster_list, value="Cluster ID", value_name="Cluster Name", label="Cluster", required=true, default_value_name="Select Cluster", have_label=false) }} 21 |
22 |
23 | 24 |
25 |
26 |
27 | {{ table(li=course_list, button=false) }} 28 | {% endblock content %} 29 | 30 | {% block script %} 31 | {{ super() }} 32 | 37 | {% endblock script %} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Learning Management System 2 | This project was developed with flask as the Database and Internet Engineering 3 | final project, because of the limited time and the purpose of this project 4 | which was to work with databases in a website, I did not consider some security 5 | features which should be considered in the production phase. 6 | 7 | For running this on your computer first make sure you have `python3.6` or later 8 | , then install `virtualenv` package. 9 | ``` 10 | pip install virtualenv 11 | ``` 12 | 13 | Create a virtual environment in main directory of the project (same folder as 14 | this file) preferably with a name like `venv`, `env`, `.venv` or `.env` so 15 | `.gitignore` file can ignore it without any modification, I assumed you're 16 | gonna use `.venv`. 17 | ``` 18 | virtualenv .venv 19 | ``` 20 | 21 | Activate your virtual environment: 22 | ``` 23 | source .venv/bin/activate 24 | ``` 25 | 26 | Or if you're still using windows: 27 | ``` 28 | .\venv\Scripts\activate 29 | ``` 30 | 31 | Then install all of the project's dependencies without affecting anything on 32 | your computer. 33 | ``` 34 | pip install -r requirements.txt 35 | ``` 36 | 37 | You should set 2 environment variable, `FLASK_APP` and `FLASK_DEBUG`, 38 | in GNU/Linux or macOS: 39 | ``` 40 | export FLASK_APP=main.py 41 | export FLASK_DEBUG=1 42 | ``` 43 | In windows: 44 | ``` 45 | set FLASK_APP=main.py 46 | set FLASK_DEBUG=1 47 | ``` 48 | `FLASK_APP` is the name of the flask app file, and `FLASK_DEBUG` should be 0 or 49 | 1, if it's 1 we have access to hot reload and some more features in development 50 | phase. 51 | 52 | In the app directory copy `local_settings.py.sample` to `local_settings.py` and 53 | provide required variables for connecting to the database, keep in mind that 54 | for security reasons this file is ignored by `.gitignore`. 55 | 56 | In the app directory run the server with this command: 57 | ``` 58 | flask run 59 | ``` 60 | -------------------------------------------------------------------------------- /app/templates/register.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block navbar %} 4 | {% endblock navbar %} 5 | 6 | {% block stylesheet %} 7 | {{ super() }} 8 | 9 | {% endblock stylesheet %} 10 | 11 | {% block title %} 12 | LMS - Register 13 | {% endblock title %} 14 | 15 | {% block content %} 16 |
17 |

LMS

18 |

Registration Form

19 |
20 | 21 |
22 |
23 | 24 |
25 |
26 | 27 |
28 |
29 | 30 |
31 |
32 | 33 |
34 |
35 | 36 |
37 |
38 | 39 |
40 |
41 | 42 |
43 |
44 | 45 |
46 | 47 | Login 48 |
49 | {% endblock content %} -------------------------------------------------------------------------------- /app/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block stylesheet %} 4 | {{ super() }} 5 | 6 | {% endblock stylesheet %} 7 | 8 | {% block title %} 9 | LMS - Home 10 | {% endblock title %} 11 | 12 | {% block content %} 13 | {% if session.User.is_admin or session.User.is_manager %} 14 |
15 |
16 | Users 17 |
18 | {% if session.User.is_admin %} 19 |
20 | Clusters 21 |
22 | {% endif %} 23 |
24 | Courses 25 |
26 |
27 | {% endif %} 28 | 29 | {% for course in student_course_list %} 30 |
31 |
32 | 33 |
34 | {{ course["Course Name"] }} 35 | 36 | Student 37 | 38 |
39 |
40 |
Teacher: {{ course["Teacher First Name"] }} {{ course["Teacher Last Name"] }}
41 |
42 |
43 | {% endfor %} 44 | 45 | {% for course in teacher_course_list %} 46 |
47 |
48 | 49 |
50 | {{ course["Course Name"] }} 51 | 52 | Teacher 53 | 54 |
55 |
56 |
Teacher: {{ course["Teacher First Name"] }} {{ course["Teacher Last Name"] }}
57 |
58 |
59 | {% endfor %} 60 | 61 | {% endblock content %} -------------------------------------------------------------------------------- /app/templates/profile.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% from 'macro.html' import input, page_header, input_select, table %} 4 | 5 | {% block title %} 6 | LMS - {{ user['Username'] }} Profile 7 | {% endblock title %} 8 | 9 | {% block content %} 10 | {{ page_header("Edit Profile") }} 11 |
12 | {{ input(name="Username", label="Username", value=user["Username"], required=true) }} 13 | {{ input(name="FirstName", label="First Name", value=user["FirstName"]) }} 14 | {{ input(name="LastName", label="Last Name", value=user["LastName"]) }} 15 | {{ input(name="PhoneNumber", label="Phone Number", value=user["PhoneNumber"]) }} 16 | {{ input(name="Email", label="Email", value=user["Email"]) }} 17 | {{ input(name="Faculty", label="Faculty", value=user["Faculty"]) }} 18 | {{ input(name="Institution", label="Institution", value=user["Institution"]) }} 19 | {{ input(name="Address", label="Address", value=user["Address"]) }} 20 | 21 |
22 | 23 | {{ page_header("Add as Student to Course") }} 24 |
25 |
26 |
27 | {{ input_select(name="CourseID", li=course_list, value="ID", value_name="Name", label="Course", required=true, default_value_name="Select Course", have_label=false) }} 28 |
29 |
30 | 31 |
32 |
33 |
34 | {% if student_course_list %} 35 | {{ table(li=student_course_list, button=false) }} 36 | {% endif %} 37 | 38 | {{ page_header("Add as Manager to Cluster") }} 39 |
40 |
41 |
42 | {{ input_select(name="ClusterID", li=cluster_list, value="Cluster ID", value_name="Cluster Name", label="Cluster", required=true, default_value_name="Select Cluster", have_label=false) }} 43 |
44 |
45 | 46 |
47 |
48 |
49 | {% if user_cluster_list %} 50 | {{ table(li=user_cluster_list, button=false) }} 51 | {% endif %} 52 | 53 | {% endblock content %} 54 | 55 | {% block script %} 56 | {{ super() }} 57 | 62 | {% endblock script %} -------------------------------------------------------------------------------- /app/templates/base.html: -------------------------------------------------------------------------------- 1 | {% from 'macro.html' import flash %} 2 | 3 | 4 | 5 | 6 | 7 | {% block stylesheet %} 8 | 9 | 10 | 11 | {% endblock stylesheet %} 12 | {% block title %}{% endblock title %} 13 | 14 | 15 | {% block navbar %} 16 | 44 | {% endblock navbar %} 45 |
46 |
47 | {% block flash %} 48 | {% with messages = get_flashed_messages(with_categories=true) %} 49 | {% if messages %} 50 | {% for category, message in messages %} 51 | {{ flash(message, category) }} 52 | {% endfor %} 53 | {% endif %} 54 | {% endwith %} 55 | {% endblock flash %} 56 |
57 | {% block content %} 58 | {% endblock content %} 59 |
60 | {% block script %} 61 | 62 | 63 | 64 | 65 | {% endblock script %} 66 | 67 | -------------------------------------------------------------------------------- /app/templates/macro.html: -------------------------------------------------------------------------------- 1 | {% macro flash(message, category='primary') -%} 2 | 8 | {%- endmacro %} 9 | 10 | 11 | {% macro page_header(text) %} 12 |

13 | {{ text }} 14 |

15 | {% endmacro %} 16 | 17 | 18 | {% macro table(li, button=true, button_text='Edit', button_url="", button_color='primary', button_head='Action') %} 19 | {% if li %} 20 | 21 | 22 | 23 | {% for key, value in li[0].items() %} 24 | {% if button and (key == 'ID' or key == 'Id' or key == 'id') %} 25 | 28 | {% else %} 29 | 30 | {% endif %} 31 | {% endfor %} 32 | 33 | 34 | 35 | {% for dict in li %} 36 | 37 | {% for key, value in dict.items() %} 38 | {% if button and (key == 'ID' or key == 'Id' or key == 'id') %} 39 | 42 | {% else %} 43 | 44 | {% endif %} 45 | {% endfor %} 46 | 47 | {% endfor %} 48 | 49 |
26 | {{ button_head }} 27 | {{ key }}
40 | {{ button_text }} 41 | {{ value }}
50 | {% endif %} 51 | {% endmacro %} 52 | 53 | {% macro input(name, label, have_label=true, placeholder="", required=false, type="text", value="") %} 54 | {% set unique_id = "id" + range(0, 1000000) | random | string %} 55 | {% if have_label %} 56 |
57 | 58 | {% if required %} 59 | 60 | {% else %} 61 | 62 | {% endif %} 63 |
64 | {% else %} 65 | {% if required %} 66 | 67 | {% else %} 68 | 69 | {% endif %} 70 | {% endif %} 71 | {% endmacro %} 72 | 73 | {% macro text_area(name, label, have_label=true, placeholder="", required=false, type="text", value="", rows="5") %} 74 | {% set unique_id = "id" + range(0, 1000000) | random | string %} 75 | {% if have_label %} 76 |
77 | 78 | {% if required %} 79 | 80 | {% else %} 81 | 82 | {% endif %} 83 |
84 | {% else %} 85 | {% if required %} 86 | 87 | {% else %} 88 | 89 | {% endif %} 90 | {% endif %} 91 | {% endmacro %} 92 | 93 | {% macro input_select(name, li, value, value_name, label, have_label=true, required=false, default_value_name="") %} 94 | {% set unique_id = "id" + range(0, 1000000) | random | string %} 95 | {% if have_label %} 96 |
97 | 98 | 104 |
105 | {% else %} 106 | 112 | {% endif %} 113 | {% endmacro %} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/vim,linux,macos,emacs,windows,visualstudiocode,python,flask 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=vim,linux,macos,emacs,windows,visualstudiocode,python,flask 4 | 5 | ### Emacs ### 6 | # -*- mode: gitignore; -*- 7 | *~ 8 | \#*\# 9 | /.emacs.desktop 10 | /.emacs.desktop.lock 11 | *.elc 12 | auto-save-list 13 | tramp 14 | .\#* 15 | 16 | # Org-mode 17 | .org-id-locations 18 | *_archive 19 | 20 | # flymake-mode 21 | *_flymake.* 22 | 23 | # eshell files 24 | /eshell/history 25 | /eshell/lastdir 26 | 27 | # elpa packages 28 | /elpa/ 29 | 30 | # reftex files 31 | *.rel 32 | 33 | # AUCTeX auto folder 34 | /auto/ 35 | 36 | # cask packages 37 | .cask/ 38 | dist/ 39 | 40 | # Flycheck 41 | flycheck_*.el 42 | 43 | # server auth directory 44 | /server/ 45 | 46 | # projectiles files 47 | .projectile 48 | 49 | # directory configuration 50 | .dir-locals.el 51 | 52 | # network security 53 | /network-security.data 54 | 55 | 56 | ### Flask ### 57 | instance/* 58 | !instance/.gitignore 59 | .webassets-cache 60 | 61 | ### Flask.Python Stack ### 62 | # Byte-compiled / optimized / DLL files 63 | __pycache__/ 64 | *.py[cod] 65 | *$py.class 66 | 67 | # C extensions 68 | *.so 69 | 70 | # Distribution / packaging 71 | .Python 72 | build/ 73 | develop-eggs/ 74 | downloads/ 75 | eggs/ 76 | .eggs/ 77 | lib/ 78 | lib64/ 79 | parts/ 80 | sdist/ 81 | var/ 82 | wheels/ 83 | pip-wheel-metadata/ 84 | share/python-wheels/ 85 | *.egg-info/ 86 | .installed.cfg 87 | *.egg 88 | MANIFEST 89 | 90 | # PyInstaller 91 | # Usually these files are written by a python script from a template 92 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 93 | *.manifest 94 | *.spec 95 | 96 | # Installer logs 97 | pip-log.txt 98 | pip-delete-this-directory.txt 99 | 100 | # Unit test / coverage reports 101 | htmlcov/ 102 | .tox/ 103 | .nox/ 104 | .coverage 105 | .coverage.* 106 | .cache 107 | nosetests.xml 108 | coverage.xml 109 | *.cover 110 | *.py,cover 111 | .hypothesis/ 112 | .pytest_cache/ 113 | pytestdebug.log 114 | 115 | # Translations 116 | *.mo 117 | *.pot 118 | 119 | # Django stuff: 120 | *.log 121 | local_settings.py 122 | db.sqlite3 123 | db.sqlite3-journal 124 | 125 | # Flask stuff: 126 | instance/ 127 | 128 | # Scrapy stuff: 129 | .scrapy 130 | 131 | # Sphinx documentation 132 | docs/_build/ 133 | doc/_build/ 134 | 135 | # PyBuilder 136 | target/ 137 | 138 | # Jupyter Notebook 139 | .ipynb_checkpoints 140 | 141 | # IPython 142 | profile_default/ 143 | ipython_config.py 144 | 145 | # pyenv 146 | .python-version 147 | 148 | # pipenv 149 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 150 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 151 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 152 | # install all needed dependencies. 153 | #Pipfile.lock 154 | 155 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 156 | __pypackages__/ 157 | 158 | # Celery stuff 159 | celerybeat-schedule 160 | celerybeat.pid 161 | 162 | # SageMath parsed files 163 | *.sage.py 164 | 165 | # Environments 166 | .env 167 | .venv 168 | env/ 169 | venv/ 170 | ENV/ 171 | env.bak/ 172 | venv.bak/ 173 | pythonenv* 174 | 175 | # Spyder project settings 176 | .spyderproject 177 | .spyproject 178 | 179 | # Rope project settings 180 | .ropeproject 181 | 182 | # mkdocs documentation 183 | /site 184 | 185 | # mypy 186 | .mypy_cache/ 187 | .dmypy.json 188 | dmypy.json 189 | 190 | # Pyre type checker 191 | .pyre/ 192 | 193 | # pytype static type analyzer 194 | .pytype/ 195 | 196 | # profiling data 197 | .prof 198 | 199 | ### Linux ### 200 | 201 | # temporary files which can be created if a process still has a handle open of a deleted file 202 | .fuse_hidden* 203 | 204 | # KDE directory preferences 205 | .directory 206 | 207 | # Linux trash folder which might appear on any partition or disk 208 | .Trash-* 209 | 210 | # .nfs files are created when an open file is removed but is still being accessed 211 | .nfs* 212 | 213 | ### macOS ### 214 | # General 215 | .DS_Store 216 | .AppleDouble 217 | .LSOverride 218 | 219 | # Icon must end with two \r 220 | Icon 221 | 222 | # Thumbnails 223 | ._* 224 | 225 | # Files that might appear in the root of a volume 226 | .DocumentRevisions-V100 227 | .fseventsd 228 | .Spotlight-V100 229 | .TemporaryItems 230 | .Trashes 231 | .VolumeIcon.icns 232 | .com.apple.timemachine.donotpresent 233 | 234 | # Directories potentially created on remote AFP share 235 | .AppleDB 236 | .AppleDesktop 237 | Network Trash Folder 238 | Temporary Items 239 | .apdisk 240 | 241 | ### Python ### 242 | # Byte-compiled / optimized / DLL files 243 | 244 | # C extensions 245 | 246 | # Distribution / packaging 247 | 248 | # PyInstaller 249 | # Usually these files are written by a python script from a template 250 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 251 | 252 | # Installer logs 253 | 254 | # Unit test / coverage reports 255 | 256 | # Translations 257 | 258 | # Django stuff: 259 | 260 | # Flask stuff: 261 | 262 | # Scrapy stuff: 263 | 264 | # Sphinx documentation 265 | 266 | # PyBuilder 267 | 268 | # Jupyter Notebook 269 | 270 | # IPython 271 | 272 | # pyenv 273 | 274 | # pipenv 275 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 276 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 277 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 278 | # install all needed dependencies. 279 | 280 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 281 | 282 | # Celery stuff 283 | 284 | # SageMath parsed files 285 | 286 | # Environments 287 | 288 | # Spyder project settings 289 | 290 | # Rope project settings 291 | 292 | # mkdocs documentation 293 | 294 | # mypy 295 | 296 | # Pyre type checker 297 | 298 | # pytype static type analyzer 299 | 300 | # profiling data 301 | 302 | ### Vim ### 303 | # Swap 304 | [._]*.s[a-v][a-z] 305 | !*.svg # comment out if you don't need vector files 306 | [._]*.sw[a-p] 307 | [._]s[a-rt-v][a-z] 308 | [._]ss[a-gi-z] 309 | [._]sw[a-p] 310 | 311 | # Session 312 | Session.vim 313 | Sessionx.vim 314 | 315 | # Temporary 316 | .netrwhist 317 | # Auto-generated tag files 318 | tags 319 | # Persistent undo 320 | [._]*.un~ 321 | 322 | ### VisualStudioCode ### 323 | .vscode/* 324 | !.vscode/tasks.json 325 | !.vscode/launch.json 326 | *.code-workspace 327 | 328 | ### VisualStudioCode Patch ### 329 | # Ignore all local history of files 330 | .history 331 | .ionide 332 | 333 | ### Windows ### 334 | # Windows thumbnail cache files 335 | Thumbs.db 336 | Thumbs.db:encryptable 337 | ehthumbs.db 338 | ehthumbs_vista.db 339 | 340 | # Dump file 341 | *.stackdump 342 | 343 | # Folder config file 344 | [Dd]esktop.ini 345 | 346 | # Recycle Bin used on file shares 347 | $RECYCLE.BIN/ 348 | 349 | # Windows Installer files 350 | *.cab 351 | *.msi 352 | *.msix 353 | *.msm 354 | *.msp 355 | 356 | # Windows shortcuts 357 | *.lnk 358 | 359 | # End of https://www.toptal.com/developers/gitignore/api/vim,linux,macos,emacs,windows,visualstudiocode,python,flask 360 | -------------------------------------------------------------------------------- /app/main.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, render_template, request, redirect, flash, url_for, session 2 | from flask_login import ( 3 | LoginManager, 4 | UserMixin, 5 | current_user, 6 | login_required, 7 | login_user, 8 | logout_user, 9 | ) 10 | from . import model 11 | 12 | 13 | app = Flask(__name__) 14 | app.config["SECRET_KEY"] = "Wfd8do6H7d74vdesbuRLlMFiAeXeJ7r" 15 | # Flask login 16 | login_manager = LoginManager() 17 | login_manager.init_app(app) 18 | login_manager.login_view = "login" 19 | login_manager.login_message_category = "danger" 20 | 21 | 22 | class User(UserMixin): 23 | def __init__(self, id): 24 | self.id = id 25 | 26 | def is_admin(self): 27 | return bool(session["User"]["is_admin"]) 28 | 29 | def __repr__(self): 30 | return f"<{self.id}>" 31 | 32 | 33 | @login_manager.user_loader 34 | def load_user(userid): 35 | return User(userid) 36 | 37 | 38 | @app.route("/") 39 | @login_required 40 | def index(): 41 | success, message, student_course_list = model.get_student_course_list( 42 | current_user.id 43 | ) 44 | if not success: 45 | flash(message, "warning") 46 | return redirect(url_for("index")) 47 | success, message, teacher_course_list = model.get_teacher_course_list( 48 | current_user.id 49 | ) 50 | if not success: 51 | flash(message, "warning") 52 | return redirect(url_for("index")) 53 | return render_template( 54 | "index.html", 55 | teacher_course_list=teacher_course_list, 56 | student_course_list=student_course_list, 57 | ) 58 | 59 | 60 | @app.route("/course/") 61 | @login_required 62 | def course(CourseID): 63 | success, message, is_teacher = model.is_teacher(current_user.id, CourseID) 64 | if not success: 65 | flash(message, "warning") 66 | return redirect(url_for("index")) 67 | success, message, course_data = model.get_course(CourseID) 68 | if not success: 69 | flash(message, "warning") 70 | return redirect(url_for("index")) 71 | if not course_data: 72 | flash("This course does not exist or you don't have access to it", "warning") 73 | return redirect(url_for("index")) 74 | success, message, content_list = model.get_content_list(CourseID) 75 | if not success: 76 | flash(message, "warning") 77 | return redirect(url_for("index")) 78 | return render_template( 79 | "course.html", 80 | is_teacher=is_teacher, 81 | course_data=course_data, 82 | content_list=content_list, 83 | ) 84 | 85 | 86 | @app.route("/login/", methods=["GET", "POST"]) 87 | def login(): 88 | if current_user.is_authenticated: 89 | flash("You are currently logged in", "primary") 90 | return redirect(url_for("index")) 91 | if request.method == "POST": 92 | Username = request.form.get("Username", "") 93 | Password = request.form.get("Password", "") 94 | success, message, raw_user = model.check_login(Username, Password) 95 | if success: 96 | login_user(User(raw_user["ID"])) 97 | session["User"] = raw_user 98 | success2, message2, is_manager = model.is_manager(current_user.id) 99 | if success2: 100 | session["User"]["is_manager"] = is_manager 101 | else: 102 | flash(message2, "warning") 103 | return redirect(url_for("login")) 104 | return redirect("/") 105 | else: 106 | flash(message, "warning") 107 | return redirect(url_for("login")) 108 | return render_template("login.html") 109 | 110 | 111 | @app.route("/logout/") 112 | @login_required 113 | def logout(): 114 | logout_user() 115 | flash("You have logged out successfully", "success") 116 | return redirect(url_for("login")) 117 | 118 | 119 | @app.route("/register/", methods=["GET", "POST"]) 120 | def register(): 121 | if request.method == "POST": 122 | Username = request.form.get("Username", "") 123 | Password = request.form.get("Password", "") 124 | LastName = request.form.get("LastName", "") 125 | FirstName = request.form.get("FirstName", "") 126 | PhoneNumber = request.form.get("PhoneNumber", "") 127 | Email = request.form.get("Email", "") 128 | Faculty = request.form.get("Faculty", "") 129 | Institution = request.form.get("Institution", "") 130 | Address = request.form.get("Address", "") 131 | success, message = model.create_user( 132 | Username=Username, 133 | Password=Password, 134 | LastName=LastName, 135 | FirstName=FirstName, 136 | PhoneNumber=PhoneNumber, 137 | Email=Email, 138 | Faculty=Faculty, 139 | Institution=Institution, 140 | Address=Address, 141 | ) 142 | if success: 143 | flash( 144 | "Your account has been created successfully, you can login now.", 145 | "success", 146 | ) 147 | return redirect(url_for("login")) 148 | flash(message, "warning") 149 | return render_template("register.html") 150 | 151 | 152 | @app.route("/users/") 153 | @login_required 154 | def user_list(): 155 | success, message, user_list = model.get_user_list() 156 | if not success: 157 | flash(message, "warning") 158 | return redirect(url_for("index")) 159 | return render_template("user_list.html", user_list=user_list) 160 | 161 | 162 | @app.route("/profile//", methods=["GET", "POST"]) 163 | @login_required 164 | def profile(ID): 165 | if request.method == "POST": 166 | Username = request.form.get("Username", "") 167 | LastName = request.form.get("LastName", "") 168 | FirstName = request.form.get("FirstName", "") 169 | PhoneNumber = request.form.get("PhoneNumber", "") 170 | Email = request.form.get("Email", "") 171 | Faculty = request.form.get("Faculty", "") 172 | Institution = request.form.get("Institution", "") 173 | Address = request.form.get("Address", "") 174 | success, message = model.edit_user_profile( 175 | ID=ID, 176 | Username=Username, 177 | LastName=LastName, 178 | FirstName=FirstName, 179 | PhoneNumber=PhoneNumber, 180 | Email=Email, 181 | Faculty=Faculty, 182 | Institution=Institution, 183 | Address=Address, 184 | ) 185 | if success: 186 | flash( 187 | "Profile updated successfully.", 188 | "success", 189 | ) 190 | return redirect(url_for("profile", ID=ID)) 191 | flash(message, "warning") 192 | success, message, raw_user = model.get_user_profile(ID) 193 | if not success: 194 | flash( 195 | "No such user exists, or you don't have access to it's profile.", "warning" 196 | ) 197 | return redirect(url_for("index")) 198 | success, message, course_list = model.get_all_course_list() 199 | if not success: 200 | flash(message, "warning") 201 | return redirect(url_for("index")) 202 | success, message, student_course_list = model.get_student_course_list(ID) 203 | if not success: 204 | flash(message, "warning") 205 | return redirect(url_for("index")) 206 | success, message, cluster_list = model.get_cluster_list( 207 | current_user.id, current_user.is_admin() 208 | ) 209 | if not success: 210 | flash(message, "warning") 211 | return redirect(url_for("index")) 212 | success, message, user_cluster_list = model.get_cluster_list(ID) 213 | if not success: 214 | flash(message, "warning") 215 | return redirect(url_for("index")) 216 | return render_template( 217 | "profile.html", 218 | user=raw_user, 219 | course_list=course_list, 220 | student_course_list=student_course_list, 221 | cluster_list=cluster_list, 222 | user_cluster_list=user_cluster_list, 223 | ) 224 | 225 | 226 | @app.route("/clusters/", methods=["GET", "POST"]) 227 | @login_required 228 | def cluster_list(): 229 | if request.method == "POST": 230 | Name = request.form.get("Name", "DEFAULT") 231 | success, message = model.create_cluster(Name) 232 | if success: 233 | flash(message, "success") 234 | return redirect(url_for("cluster_list")) 235 | flash(message, "warning") 236 | return redirect(url_for("cluster_list")) 237 | success, message, cluster_list = model.get_cluster_list( 238 | current_user.id, current_user.is_admin() 239 | ) 240 | if not success: 241 | flash(message, "warning") 242 | return redirect(url_for("cluster_list")) 243 | return render_template("cluster.html", cluster_list=cluster_list) 244 | 245 | 246 | @app.route("/make_manager//", methods=["POST"]) 247 | @login_required 248 | def make_manager(ManagerID): 249 | ClusterID = request.form.get("ClusterID", "") 250 | success, message = model.create_manager_cluster(ManagerID, ClusterID) 251 | if success: 252 | flash(message, "success") 253 | else: 254 | flash(message, "warning") 255 | return redirect(url_for("profile", ID=ManagerID)) 256 | 257 | 258 | @app.route("/courses/", methods=["GET", "POST"]) 259 | @login_required 260 | def course_list(): 261 | if request.method == "POST": 262 | Name = request.form.get("Name", "DEFAULT") 263 | ClusterID = request.form.get("ClusterID", "DEFAULT") 264 | TeacherID = request.form.get("TeacherID", "DEFAULT") 265 | success, message = model.create_course(Name, TeacherID, ClusterID) 266 | if success: 267 | flash(message, "success") 268 | return redirect(url_for("course_list")) 269 | flash(message, "warning") 270 | return redirect(url_for("course_list")) 271 | success, message, course_list = model.get_all_course_list() 272 | if not success: 273 | flash(message, "warning") 274 | return redirect(url_for("course_list")) 275 | success, message, cluster_list = model.get_cluster_list( 276 | current_user.id, current_user.is_admin() 277 | ) 278 | if not success: 279 | flash(message, "warning") 280 | return redirect(url_for("index")) 281 | success, message, user_list = model.get_user_list() 282 | if not success: 283 | flash(message, "warning") 284 | return redirect(url_for("index")) 285 | return render_template( 286 | "course_list.html", 287 | course_list=course_list, 288 | cluster_list=cluster_list, 289 | user_list=user_list, 290 | ) 291 | 292 | 293 | @app.route("/participate/", methods=["POST"]) 294 | @login_required 295 | def participate(StudentID): 296 | CourseID = request.form.get("CourseID", "") 297 | success, message = model.create_student_course(StudentID, CourseID) 298 | if success: 299 | flash(message, "success") 300 | else: 301 | flash(message, "warning") 302 | return redirect(url_for("profile", ID=StudentID)) 303 | 304 | 305 | @app.route("/students//") 306 | @login_required 307 | def student_list(CourseID): 308 | success, message, student_list = model.get_course_student_list(CourseID) 309 | if not success: 310 | flash(message, "warning") 311 | return redirect(url_for("course", CourseID=CourseID)) 312 | return render_template("course_student.html", student_list=student_list) 313 | 314 | 315 | @app.route("/newcontent//", methods=["GET", "POST"]) 316 | @login_required 317 | def new_content(CourseID): 318 | if request.method == "POST": 319 | Title = request.form.get("Title", "") 320 | TextContent = request.form.get("TextContent", "") 321 | success, message = model.create_content(CourseID, Title, TextContent) 322 | if success: 323 | flash("Content created successfully.", "success") 324 | return redirect(url_for("course", CourseID=CourseID)) 325 | flash(message, "warning") 326 | return redirect(url_for("course", CourseID=CourseID)) 327 | return render_template("new_content.html", CourseID=CourseID) 328 | -------------------------------------------------------------------------------- /app/model.py: -------------------------------------------------------------------------------- 1 | from mysql.connector import connect, Error 2 | from werkzeug.security import generate_password_hash, check_password_hash 3 | from . import local_settings 4 | 5 | 6 | def get_conn(): 7 | conn = connect( 8 | host=local_settings.MYSQL_HOST, 9 | user=local_settings.MYSQL_USER, 10 | password=local_settings.MYSQL_PASSWORD, 11 | database=local_settings.MYSQL_DB, 12 | ) 13 | return conn 14 | 15 | 16 | def create_user( 17 | Username, 18 | Password, 19 | is_student="DEFAULT", 20 | is_teacher="DEFAULT", 21 | is_manager="DEFAULT", 22 | is_admin="DEFAULT", 23 | LastName="DEFAULT", 24 | FirstName="DEFAULT", 25 | PhoneNumber="DEFAULT", 26 | Email="DEFAULT", 27 | Faculty="DEFAULT", 28 | Institution="DEFAULT", 29 | Address="DEFAULT", 30 | ): 31 | """ 32 | CREATE TABLE Users ( 33 | ID INT NOT NULL AUTO_INCREMENT, 34 | Username VARCHAR(64) NOT NULL UNIQUE, 35 | Password VARCHAR(255) NOT NULL, 36 | is_student BOOLEAN DEFAULT false, 37 | is_teacher BOOLEAN DEFAULT false, 38 | is_manager BOOLEAN DEFAULT false, 39 | is_admin BOOLEAN DEFAULT false, 40 | LastName VARCHAR(255), 41 | FirstName VARCHAR(255), 42 | PhoneNumber CHAR(13), 43 | Email VARCHAR(320), 44 | Faculty VARCHAR(255), 45 | Institution VARCHAR(255), 46 | Address VARCHAR(2048), 47 | PRIMARY KEY (ID) 48 | ); 49 | 50 | """ 51 | if len(Username) < 4: 52 | error_message = "Username should be at least 4 characters long" 53 | return (False, error_message) 54 | if len(Password) < 6: 55 | error_message = "Password should be at least 6 characters long" 56 | return (False, error_message) 57 | conn = get_conn() 58 | cur = conn.cursor() 59 | Password = generate_password_hash(Password) 60 | sql_str = f""" 61 | INSERT INTO Users 62 | (Username, Password, is_student, is_teacher, is_manager, is_admin, 63 | LastName, FirstName, PhoneNumber, Email, Faculty, Institution, Address) 64 | VALUES 65 | ('{Username}', '{Password}', {is_student}, {is_teacher}, 66 | {is_manager},{is_admin}, '{LastName}', '{FirstName}', '{PhoneNumber}', 67 | '{Email}', '{Faculty}', '{Institution}', '{Address}') 68 | """ 69 | try: 70 | cur.execute(sql_str) 71 | conn.commit() 72 | except Error as e: 73 | cur.close() 74 | conn.close() 75 | return (False, str(e)) 76 | cur.close() 77 | conn.close() 78 | return (True, f"User with username: {Username} created successfully") 79 | 80 | 81 | def check_login(Username, Password): 82 | conn = get_conn() 83 | cur = conn.cursor(dictionary=True) 84 | sql_str = f""" 85 | SELECT ID, Username, Password, FirstName, LastName, 86 | is_student, is_teacher, is_manager, is_admin 87 | FROM Users WHERE 88 | Username = '{Username}' 89 | """ 90 | try: 91 | cur.execute(sql_str) 92 | except Error as e: 93 | cur.close() 94 | conn.close() 95 | return (False, str(e), None) 96 | raw_user = cur.fetchone() 97 | cur.close() 98 | conn.close() 99 | if raw_user: 100 | if check_password_hash(raw_user.pop("Password"), Password): 101 | return (True, "Correct username and password", raw_user) 102 | return (False, "Wrong username or password", raw_user) 103 | 104 | 105 | def get_user_list(): 106 | conn = get_conn() 107 | cur = conn.cursor(dictionary=True) 108 | sql_str = """ 109 | SELECT Username, FirstName AS 'First Name', LastName AS 'Last Name', 110 | PhoneNumber AS 'Phone Number', Email, Faculty, ID 111 | FROM Users 112 | """ 113 | try: 114 | cur.execute(sql_str) 115 | except Error as e: 116 | cur.close() 117 | conn.close() 118 | return (False, str(e), None) 119 | user_list = cur.fetchall() 120 | cur.close() 121 | conn.close() 122 | return (True, "User list retrieved from db, successfully", user_list) 123 | 124 | 125 | def get_user_profile(ID): 126 | conn = get_conn() 127 | cur = conn.cursor(dictionary=True) 128 | sql_str = f""" 129 | SELECT * 130 | FROM Users 131 | WHERE ID={ID} 132 | """ 133 | try: 134 | cur.execute(sql_str) 135 | except Error as e: 136 | cur.close() 137 | conn.close() 138 | return (False, str(e), None) 139 | raw_user = cur.fetchone() 140 | cur.close() 141 | conn.close() 142 | if raw_user: 143 | raw_user.pop("Password") 144 | return (True, "User profile retrieved from db, successfully", raw_user) 145 | return (False, "No such user exists", None) 146 | 147 | 148 | def edit_user_profile( 149 | ID, 150 | Username, 151 | LastName="DEFAULT", 152 | FirstName="DEFAULT", 153 | PhoneNumber="DEFAULT", 154 | Email="DEFAULT", 155 | Faculty="DEFAULT", 156 | Institution="DEFAULT", 157 | Address="DEFAULT", 158 | ): 159 | conn = get_conn() 160 | cur = conn.cursor(dictionary=True) 161 | sql_str = f""" 162 | UPDATE Users 163 | SET LastName = '{LastName}', FirstName = '{FirstName}', 164 | PhoneNumber = '{PhoneNumber}', Email = '{Email}', 165 | Faculty = '{Faculty}', Institution = '{Institution}', 166 | Address = '{Address}', Username = '{Username}' 167 | WHERE ID = {ID}; 168 | """ 169 | try: 170 | cur.execute(sql_str) 171 | conn.commit() 172 | except Error as e: 173 | cur.close() 174 | conn.close() 175 | return (False, str(e)) 176 | cur.close() 177 | conn.close() 178 | return (True, "User profile updated successfully.") 179 | 180 | 181 | def create_cluster(Name): 182 | """ 183 | CREATE TABLE Clusters ( 184 | ID INT NOT NULL AUTO_INCREMENT, 185 | Name VARCHAR(64) NOT NULL UNIQUE, 186 | PRIMARY KEY (ID) 187 | ); 188 | """ 189 | conn = get_conn() 190 | cur = conn.cursor(dictionary=True) 191 | sql_str = f""" 192 | INSERT INTO Clusters (Name) 193 | VALUES ('{Name}') 194 | """ 195 | try: 196 | cur.execute(sql_str) 197 | conn.commit() 198 | except Error as e: 199 | cur.close() 200 | conn.close() 201 | return (False, str(e)) 202 | cur.close() 203 | conn.close() 204 | return (True, "Cluster created successfully.") 205 | 206 | 207 | def create_manager_cluster(ManagerID, ClusterID): 208 | """ 209 | CREATE TABLE ManagerCluster ( 210 | ManagerID INT NOT NULL, 211 | ClusterID INT NOT NULL, 212 | PRIMARY KEY (ManagerID, ClusterID), 213 | FOREIGN KEY (ManagerID) REFERENCES Users(ID) 214 | ON DELETE RESTRICT, 215 | FOREIGN KEY (ClusterID) REFERENCES Clusters(ID) 216 | ON DELETE RESTRICT 217 | ) 218 | """ 219 | conn = get_conn() 220 | cur = conn.cursor(dictionary=True) 221 | sql_str = f""" 222 | INSERT INTO ManagerCluster 223 | (ManagerID, ClusterID) 224 | VALUES 225 | ({ManagerID}, {ClusterID}) 226 | """ 227 | try: 228 | cur.execute(sql_str) 229 | conn.commit() 230 | except Error as e: 231 | cur.close() 232 | conn.close() 233 | return (False, str(e)) 234 | cur.close() 235 | conn.close() 236 | return (True, "Cluster manager created successfully.") 237 | 238 | 239 | def get_cluster_manager_list(ManagerID): 240 | conn = get_conn() 241 | cur = conn.cursor(dictionary=True) 242 | sql_str = f""" 243 | SELECT Name AS 'Cluster Name', ID AS 'Cluster ID' 244 | FROM Clusters WHERE ID IN 245 | (SELECT ClusterID 246 | FROM ManagerCluster 247 | WHERE ManagerID={ManagerID}) 248 | """ 249 | try: 250 | cur.execute(sql_str) 251 | except Error as e: 252 | cur.close() 253 | conn.close() 254 | return (False, str(e), None) 255 | cluster_manager_list = cur.fetchall() 256 | cur.close() 257 | conn.close() 258 | return ( 259 | True, 260 | "Manager's clusters retrieved from db, successfully", 261 | cluster_manager_list, 262 | ) 263 | 264 | 265 | def is_manager(UserID): 266 | conn = get_conn() 267 | cur = conn.cursor(dictionary=True) 268 | sql_str = f""" 269 | SELECT COUNT(1) AS is_manager 270 | FROM ManagerCluster 271 | WHERE ManagerID={UserID} 272 | """ 273 | try: 274 | cur.execute(sql_str) 275 | except Error as e: 276 | cur.close() 277 | conn.close() 278 | return (False, str(e), None) 279 | manager = cur.fetchone() 280 | cur.close() 281 | conn.close() 282 | return ( 283 | True, 284 | "User checked for manager successfully", 285 | bool(int(manager["is_manager"])), 286 | ) 287 | 288 | 289 | def get_cluster_list(ManagerID, is_admin=False): 290 | if is_admin: 291 | return get_all_cluster_list() 292 | return get_cluster_manager_list(ManagerID) 293 | 294 | 295 | def get_all_cluster_list(): 296 | conn = get_conn() 297 | cur = conn.cursor(dictionary=True) 298 | sql_str = """ 299 | SELECT Name AS 'Cluster Name', ID AS 'Cluster ID' 300 | FROM Clusters 301 | """ 302 | try: 303 | cur.execute(sql_str) 304 | except Error as e: 305 | cur.close() 306 | conn.close() 307 | return (False, str(e), None) 308 | cluster_list = cur.fetchall() 309 | cur.close() 310 | conn.close() 311 | return (True, "Clusters retrieved from db, successfully.", cluster_list) 312 | 313 | 314 | def create_course(Name, TeacherID, ClusterID): 315 | """ 316 | CREATE TABLE Courses ( 317 | ID INT NOT NULL AUTO_INCREMENT, 318 | Name VARCHAR(64) NOT NULL, 319 | ClusterID INT NOT NULL, 320 | TeacherID INT NOT NULL, 321 | PRIMARY KEY (ID), 322 | FOREIGN KEY (ClusterID) REFERENCES Clusters(ID) 323 | ON DELETE RESTRICT, 324 | FOREIGN KEY (TeacherID) REFERENCES Users(ID) 325 | ON DELETE RESTRICT 326 | ); 327 | """ 328 | conn = get_conn() 329 | cur = conn.cursor(dictionary=True) 330 | sql_str = f""" 331 | INSERT INTO Courses 332 | (Name, ClusterID, TeacherID) 333 | VALUES 334 | ('{Name}', {ClusterID}, {TeacherID}) 335 | """ 336 | try: 337 | cur.execute(sql_str) 338 | conn.commit() 339 | except Error as e: 340 | cur.close() 341 | conn.close() 342 | return (False, str(e)) 343 | cur.close() 344 | conn.close() 345 | return (True, "Course created successfully.") 346 | 347 | 348 | def get_all_course_list(): 349 | conn = get_conn() 350 | cur = conn.cursor(dictionary=True) 351 | sql_str = """ 352 | SELECT Name, ClusterID, TeacherID, ID 353 | FROM Courses 354 | """ 355 | try: 356 | cur.execute(sql_str) 357 | except Error as e: 358 | cur.close() 359 | conn.close() 360 | return (False, str(e)) 361 | course_list = cur.fetchall() 362 | cur.close() 363 | conn.close() 364 | return (True, "Course list retrieved from db, successfully", course_list) 365 | 366 | 367 | def create_student_course(StudentID, CourseID): 368 | """ 369 | CREATE TABLE StudentCourse ( 370 | StudentID INT NOT NULL, 371 | CourseID INT NOT NULL, 372 | PRIMARY KEY (StudentID, CourseID), 373 | FOREIGN KEY (StudentID) REFERENCES Users(ID) 374 | ON DELETE RESTRICT, 375 | FOREIGN KEY (CourseID) REFERENCES Courses(ID) 376 | ON DELETE RESTRICT 377 | ); 378 | """ 379 | conn = get_conn() 380 | cur = conn.cursor(dictionary=True) 381 | sql_str = f""" 382 | INSERT INTO StudentCourse 383 | (StudentID, CourseID) 384 | VALUES 385 | ({StudentID}, {CourseID}) 386 | """ 387 | try: 388 | cur.execute(sql_str) 389 | conn.commit() 390 | except Error as e: 391 | cur.close() 392 | conn.close() 393 | return (False, str(e)) 394 | cur.close() 395 | conn.close() 396 | return (True, "Student participated in the course successfully.") 397 | 398 | 399 | def get_student_course_list(StudentID): 400 | conn = get_conn() 401 | cur = conn.cursor(dictionary=True) 402 | sql_str = f""" 403 | SELECT c.Name AS 'Course Name', u.FirstName AS 'Teacher First Name', 404 | u.LastName AS 'Teacher Last Name', c.ID AS 'Course ID' 405 | FROM ((StudentCourse AS sc 406 | INNER JOIN Courses AS c ON sc.CourseID = c.ID) 407 | INNER JOIN Users AS u ON c.TeacherID = u.ID) 408 | WHERE sc.StudentID = {StudentID} 409 | """ 410 | try: 411 | cur.execute(sql_str) 412 | except Error as e: 413 | cur.close() 414 | conn.close() 415 | return (False, str(e), None) 416 | student_course_list = cur.fetchall() 417 | cur.close() 418 | conn.close() 419 | return (True, "Courses retrieved from db, successfully.", student_course_list) 420 | 421 | 422 | def get_teacher_course_list(TeacherID): 423 | conn = get_conn() 424 | cur = conn.cursor(dictionary=True) 425 | sql_str = f""" 426 | SELECT c.Name AS 'Course Name', u.FirstName AS 'Teacher First Name', 427 | u.LastName AS 'Teacher Last Name', c.ID AS 'Course ID' 428 | FROM Courses AS c 429 | INNER JOIN Users AS u ON c.TeacherID = u.ID 430 | WHERE c.TeacherID = {TeacherID} 431 | """ 432 | try: 433 | cur.execute(sql_str) 434 | except Error as e: 435 | cur.close() 436 | conn.close() 437 | return (False, str(e), None) 438 | teacher_course_list = cur.fetchall() 439 | cur.close() 440 | conn.close() 441 | return (True, "Courses retrieved from db, successfully.", teacher_course_list) 442 | 443 | 444 | def is_teacher(UserID, CourseID): 445 | conn = get_conn() 446 | cur = conn.cursor(dictionary=True) 447 | sql_str = f""" 448 | SELECT COUNT(1) AS is_teacher 449 | FROM Courses 450 | WHERE ID={CourseID} AND TeacherID={UserID} 451 | """ 452 | try: 453 | cur.execute(sql_str) 454 | except Error as e: 455 | cur.close() 456 | conn.close() 457 | return (False, str(e), None) 458 | teacher = cur.fetchone() 459 | cur.close() 460 | conn.close() 461 | return ( 462 | True, 463 | "User checked for teacher successfully", 464 | bool(int(teacher["is_teacher"])), 465 | ) 466 | 467 | 468 | def get_course(CourseID): 469 | conn = get_conn() 470 | cur = conn.cursor(dictionary=True) 471 | sql_str = f""" 472 | SELECT ID AS 'Course ID', Name AS 'Course Name', TeacherID AS 'Teacher ID' 473 | FROM Courses 474 | WHERE ID = {CourseID} 475 | """ 476 | try: 477 | cur.execute(sql_str) 478 | except Error as e: 479 | cur.close() 480 | conn.close() 481 | return (False, str(e), None) 482 | course = cur.fetchone() 483 | cur.close() 484 | conn.close() 485 | return (True, "Course retrieved from db, successfully", course) 486 | 487 | 488 | def create_content(CourseID, Title, TextContent): 489 | """ 490 | CREATE TABLE Contents ( 491 | ID INT NOT NULL AUTO_INCREMENT, 492 | CourseID INT NOT NULL, 493 | Title VARCHAR(255) NOT NULL, 494 | TextContent LONGTEXT, 495 | CreatedAt DATETIME DEFAULT CURRENT_TIMESTAMP, 496 | PRIMARY KEY (ID), 497 | FOREIGN KEY (CourseID) REFERENCES Courses(ID) 498 | ON DELETE RESTRICT 499 | ); 500 | """ 501 | conn = get_conn() 502 | cur = conn.cursor(dictionary=True) 503 | sql_str = f""" 504 | INSERT INTO Contents 505 | (CourseID, Title, TextContent) 506 | VALUES 507 | ({CourseID}, '{Title}', '{TextContent}') 508 | """ 509 | try: 510 | cur.execute(sql_str) 511 | conn.commit() 512 | except Error as e: 513 | cur.close() 514 | conn.close() 515 | return (False, str(e)) 516 | cur.close() 517 | conn.close() 518 | return (True, "Content created successfully.") 519 | 520 | 521 | def get_content_list(CourseID): 522 | conn = get_conn() 523 | cur = conn.cursor(dictionary=True) 524 | sql_str = f""" 525 | SELECT Title, TextContent, CreatedAt 526 | FROM Contents 527 | WHERE CourseID = {CourseID} 528 | """ 529 | try: 530 | cur.execute(sql_str) 531 | except Error as e: 532 | cur.close() 533 | conn.close() 534 | return (False, str(e), None) 535 | content_list = cur.fetchall() 536 | cur.close() 537 | conn.close() 538 | return (True, "Contents retrieved from db, successfully", content_list) 539 | 540 | 541 | def get_course_student_list(CourseID): 542 | conn = get_conn() 543 | cur = conn.cursor(dictionary=True) 544 | sql_str = f""" 545 | SELECT Username, FirstName AS 'First Name', LastName AS 'Last Name', 546 | PhoneNumber AS 'Phone Number', Email, Faculty 547 | FROM Users 548 | WHERE ID IN 549 | (SELECT StudentID FROM StudentCourse 550 | WHERE CourseID = {CourseID}) 551 | """ 552 | try: 553 | cur.execute(sql_str) 554 | except Error as e: 555 | cur.close() 556 | conn.close() 557 | return (False, str(e), None) 558 | student_list = cur.fetchall() 559 | cur.close() 560 | conn.close() 561 | return (True, "Student list retrieved from db, successfully", student_list) 562 | -------------------------------------------------------------------------------- /app/static/css/jquery.dataTables.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Table styles 3 | */ 4 | table.dataTable { 5 | width: 100%; 6 | margin: 0 auto; 7 | clear: both; 8 | border-collapse: separate; 9 | border-spacing: 0; 10 | /* 11 | * Header and footer styles 12 | */ 13 | /* 14 | * Body styles 15 | */ 16 | } 17 | table.dataTable thead th, 18 | table.dataTable tfoot th { 19 | font-weight: bold; 20 | } 21 | table.dataTable thead th, 22 | table.dataTable thead td { 23 | padding: 10px 18px; 24 | border-bottom: 1px solid #111; 25 | } 26 | table.dataTable thead th:active, 27 | table.dataTable thead td:active { 28 | outline: none; 29 | } 30 | table.dataTable tfoot th, 31 | table.dataTable tfoot td { 32 | padding: 10px 18px 6px 18px; 33 | border-top: 1px solid #111; 34 | } 35 | table.dataTable thead .sorting, 36 | table.dataTable thead .sorting_asc, 37 | table.dataTable thead .sorting_desc, 38 | table.dataTable thead .sorting_asc_disabled, 39 | table.dataTable thead .sorting_desc_disabled { 40 | cursor: pointer; 41 | *cursor: hand; 42 | background-repeat: no-repeat; 43 | background-position: center right; 44 | } 45 | table.dataTable thead .sorting { 46 | background-image: url("../images/sort_both.png"); 47 | } 48 | table.dataTable thead .sorting_asc { 49 | background-image: url("../images/sort_asc.png"); 50 | } 51 | table.dataTable thead .sorting_desc { 52 | background-image: url("../images/sort_desc.png"); 53 | } 54 | table.dataTable thead .sorting_asc_disabled { 55 | background-image: url("../images/sort_asc_disabled.png"); 56 | } 57 | table.dataTable thead .sorting_desc_disabled { 58 | background-image: url("../images/sort_desc_disabled.png"); 59 | } 60 | table.dataTable tbody tr { 61 | background-color: #ffffff; 62 | } 63 | table.dataTable tbody tr.selected { 64 | background-color: #B0BED9; 65 | } 66 | table.dataTable tbody th, 67 | table.dataTable tbody td { 68 | padding: 8px 10px; 69 | } 70 | table.dataTable.row-border tbody th, table.dataTable.row-border tbody td, table.dataTable.display tbody th, table.dataTable.display tbody td { 71 | border-top: 1px solid #ddd; 72 | } 73 | table.dataTable.row-border tbody tr:first-child th, 74 | table.dataTable.row-border tbody tr:first-child td, table.dataTable.display tbody tr:first-child th, 75 | table.dataTable.display tbody tr:first-child td { 76 | border-top: none; 77 | } 78 | table.dataTable.cell-border tbody th, table.dataTable.cell-border tbody td { 79 | border-top: 1px solid #ddd; 80 | border-right: 1px solid #ddd; 81 | } 82 | table.dataTable.cell-border tbody tr th:first-child, 83 | table.dataTable.cell-border tbody tr td:first-child { 84 | border-left: 1px solid #ddd; 85 | } 86 | table.dataTable.cell-border tbody tr:first-child th, 87 | table.dataTable.cell-border tbody tr:first-child td { 88 | border-top: none; 89 | } 90 | table.dataTable.stripe tbody tr.odd, table.dataTable.display tbody tr.odd { 91 | background-color: #f9f9f9; 92 | } 93 | table.dataTable.stripe tbody tr.odd.selected, table.dataTable.display tbody tr.odd.selected { 94 | background-color: #acbad4; 95 | } 96 | table.dataTable.hover tbody tr:hover, table.dataTable.display tbody tr:hover { 97 | background-color: #f6f6f6; 98 | } 99 | table.dataTable.hover tbody tr:hover.selected, table.dataTable.display tbody tr:hover.selected { 100 | background-color: #aab7d1; 101 | } 102 | table.dataTable.order-column tbody tr > .sorting_1, 103 | table.dataTable.order-column tbody tr > .sorting_2, 104 | table.dataTable.order-column tbody tr > .sorting_3, table.dataTable.display tbody tr > .sorting_1, 105 | table.dataTable.display tbody tr > .sorting_2, 106 | table.dataTable.display tbody tr > .sorting_3 { 107 | background-color: #fafafa; 108 | } 109 | table.dataTable.order-column tbody tr.selected > .sorting_1, 110 | table.dataTable.order-column tbody tr.selected > .sorting_2, 111 | table.dataTable.order-column tbody tr.selected > .sorting_3, table.dataTable.display tbody tr.selected > .sorting_1, 112 | table.dataTable.display tbody tr.selected > .sorting_2, 113 | table.dataTable.display tbody tr.selected > .sorting_3 { 114 | background-color: #acbad5; 115 | } 116 | table.dataTable.display tbody tr.odd > .sorting_1, table.dataTable.order-column.stripe tbody tr.odd > .sorting_1 { 117 | background-color: #f1f1f1; 118 | } 119 | table.dataTable.display tbody tr.odd > .sorting_2, table.dataTable.order-column.stripe tbody tr.odd > .sorting_2 { 120 | background-color: #f3f3f3; 121 | } 122 | table.dataTable.display tbody tr.odd > .sorting_3, table.dataTable.order-column.stripe tbody tr.odd > .sorting_3 { 123 | background-color: whitesmoke; 124 | } 125 | table.dataTable.display tbody tr.odd.selected > .sorting_1, table.dataTable.order-column.stripe tbody tr.odd.selected > .sorting_1 { 126 | background-color: #a6b4cd; 127 | } 128 | table.dataTable.display tbody tr.odd.selected > .sorting_2, table.dataTable.order-column.stripe tbody tr.odd.selected > .sorting_2 { 129 | background-color: #a8b5cf; 130 | } 131 | table.dataTable.display tbody tr.odd.selected > .sorting_3, table.dataTable.order-column.stripe tbody tr.odd.selected > .sorting_3 { 132 | background-color: #a9b7d1; 133 | } 134 | table.dataTable.display tbody tr.even > .sorting_1, table.dataTable.order-column.stripe tbody tr.even > .sorting_1 { 135 | background-color: #fafafa; 136 | } 137 | table.dataTable.display tbody tr.even > .sorting_2, table.dataTable.order-column.stripe tbody tr.even > .sorting_2 { 138 | background-color: #fcfcfc; 139 | } 140 | table.dataTable.display tbody tr.even > .sorting_3, table.dataTable.order-column.stripe tbody tr.even > .sorting_3 { 141 | background-color: #fefefe; 142 | } 143 | table.dataTable.display tbody tr.even.selected > .sorting_1, table.dataTable.order-column.stripe tbody tr.even.selected > .sorting_1 { 144 | background-color: #acbad5; 145 | } 146 | table.dataTable.display tbody tr.even.selected > .sorting_2, table.dataTable.order-column.stripe tbody tr.even.selected > .sorting_2 { 147 | background-color: #aebcd6; 148 | } 149 | table.dataTable.display tbody tr.even.selected > .sorting_3, table.dataTable.order-column.stripe tbody tr.even.selected > .sorting_3 { 150 | background-color: #afbdd8; 151 | } 152 | table.dataTable.display tbody tr:hover > .sorting_1, table.dataTable.order-column.hover tbody tr:hover > .sorting_1 { 153 | background-color: #eaeaea; 154 | } 155 | table.dataTable.display tbody tr:hover > .sorting_2, table.dataTable.order-column.hover tbody tr:hover > .sorting_2 { 156 | background-color: #ececec; 157 | } 158 | table.dataTable.display tbody tr:hover > .sorting_3, table.dataTable.order-column.hover tbody tr:hover > .sorting_3 { 159 | background-color: #efefef; 160 | } 161 | table.dataTable.display tbody tr:hover.selected > .sorting_1, table.dataTable.order-column.hover tbody tr:hover.selected > .sorting_1 { 162 | background-color: #a2aec7; 163 | } 164 | table.dataTable.display tbody tr:hover.selected > .sorting_2, table.dataTable.order-column.hover tbody tr:hover.selected > .sorting_2 { 165 | background-color: #a3b0c9; 166 | } 167 | table.dataTable.display tbody tr:hover.selected > .sorting_3, table.dataTable.order-column.hover tbody tr:hover.selected > .sorting_3 { 168 | background-color: #a5b2cb; 169 | } 170 | table.dataTable.no-footer { 171 | border-bottom: 1px solid #111; 172 | } 173 | table.dataTable.nowrap th, table.dataTable.nowrap td { 174 | white-space: nowrap; 175 | } 176 | table.dataTable.compact thead th, 177 | table.dataTable.compact thead td { 178 | padding: 4px 17px; 179 | } 180 | table.dataTable.compact tfoot th, 181 | table.dataTable.compact tfoot td { 182 | padding: 4px; 183 | } 184 | table.dataTable.compact tbody th, 185 | table.dataTable.compact tbody td { 186 | padding: 4px; 187 | } 188 | table.dataTable th.dt-left, 189 | table.dataTable td.dt-left { 190 | text-align: left; 191 | } 192 | table.dataTable th.dt-center, 193 | table.dataTable td.dt-center, 194 | table.dataTable td.dataTables_empty { 195 | text-align: center; 196 | } 197 | table.dataTable th.dt-right, 198 | table.dataTable td.dt-right { 199 | text-align: right; 200 | } 201 | table.dataTable th.dt-justify, 202 | table.dataTable td.dt-justify { 203 | text-align: justify; 204 | } 205 | table.dataTable th.dt-nowrap, 206 | table.dataTable td.dt-nowrap { 207 | white-space: nowrap; 208 | } 209 | table.dataTable thead th.dt-head-left, 210 | table.dataTable thead td.dt-head-left, 211 | table.dataTable tfoot th.dt-head-left, 212 | table.dataTable tfoot td.dt-head-left { 213 | text-align: left; 214 | } 215 | table.dataTable thead th.dt-head-center, 216 | table.dataTable thead td.dt-head-center, 217 | table.dataTable tfoot th.dt-head-center, 218 | table.dataTable tfoot td.dt-head-center { 219 | text-align: center; 220 | } 221 | table.dataTable thead th.dt-head-right, 222 | table.dataTable thead td.dt-head-right, 223 | table.dataTable tfoot th.dt-head-right, 224 | table.dataTable tfoot td.dt-head-right { 225 | text-align: right; 226 | } 227 | table.dataTable thead th.dt-head-justify, 228 | table.dataTable thead td.dt-head-justify, 229 | table.dataTable tfoot th.dt-head-justify, 230 | table.dataTable tfoot td.dt-head-justify { 231 | text-align: justify; 232 | } 233 | table.dataTable thead th.dt-head-nowrap, 234 | table.dataTable thead td.dt-head-nowrap, 235 | table.dataTable tfoot th.dt-head-nowrap, 236 | table.dataTable tfoot td.dt-head-nowrap { 237 | white-space: nowrap; 238 | } 239 | table.dataTable tbody th.dt-body-left, 240 | table.dataTable tbody td.dt-body-left { 241 | text-align: left; 242 | } 243 | table.dataTable tbody th.dt-body-center, 244 | table.dataTable tbody td.dt-body-center { 245 | text-align: center; 246 | } 247 | table.dataTable tbody th.dt-body-right, 248 | table.dataTable tbody td.dt-body-right { 249 | text-align: right; 250 | } 251 | table.dataTable tbody th.dt-body-justify, 252 | table.dataTable tbody td.dt-body-justify { 253 | text-align: justify; 254 | } 255 | table.dataTable tbody th.dt-body-nowrap, 256 | table.dataTable tbody td.dt-body-nowrap { 257 | white-space: nowrap; 258 | } 259 | 260 | table.dataTable, 261 | table.dataTable th, 262 | table.dataTable td { 263 | box-sizing: content-box; 264 | } 265 | 266 | /* 267 | * Control feature layout 268 | */ 269 | .dataTables_wrapper { 270 | position: relative; 271 | clear: both; 272 | *zoom: 1; 273 | zoom: 1; 274 | } 275 | .dataTables_wrapper .dataTables_length { 276 | float: left; 277 | } 278 | .dataTables_wrapper .dataTables_length select { 279 | border: 1px solid #aaa; 280 | border-radius: 3px; 281 | padding: 5px; 282 | background-color: transparent; 283 | padding: 4px; 284 | } 285 | .dataTables_wrapper .dataTables_filter { 286 | float: right; 287 | text-align: right; 288 | } 289 | .dataTables_wrapper .dataTables_filter input { 290 | border: 1px solid #aaa; 291 | border-radius: 3px; 292 | padding: 5px; 293 | background-color: transparent; 294 | margin-left: 3px; 295 | } 296 | .dataTables_wrapper .dataTables_info { 297 | clear: both; 298 | float: left; 299 | padding-top: 0.755em; 300 | } 301 | .dataTables_wrapper .dataTables_paginate { 302 | float: right; 303 | text-align: right; 304 | padding-top: 0.25em; 305 | } 306 | .dataTables_wrapper .dataTables_paginate .paginate_button { 307 | box-sizing: border-box; 308 | display: inline-block; 309 | min-width: 1.5em; 310 | padding: 0.5em 1em; 311 | margin-left: 2px; 312 | text-align: center; 313 | text-decoration: none !important; 314 | cursor: pointer; 315 | *cursor: hand; 316 | color: #333 !important; 317 | border: 1px solid transparent; 318 | border-radius: 2px; 319 | } 320 | .dataTables_wrapper .dataTables_paginate .paginate_button.current, .dataTables_wrapper .dataTables_paginate .paginate_button.current:hover { 321 | color: #333 !important; 322 | border: 1px solid #979797; 323 | background-color: white; 324 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, white), color-stop(100%, #dcdcdc)); 325 | /* Chrome,Safari4+ */ 326 | background: -webkit-linear-gradient(top, white 0%, #dcdcdc 100%); 327 | /* Chrome10+,Safari5.1+ */ 328 | background: -moz-linear-gradient(top, white 0%, #dcdcdc 100%); 329 | /* FF3.6+ */ 330 | background: -ms-linear-gradient(top, white 0%, #dcdcdc 100%); 331 | /* IE10+ */ 332 | background: -o-linear-gradient(top, white 0%, #dcdcdc 100%); 333 | /* Opera 11.10+ */ 334 | background: linear-gradient(to bottom, white 0%, #dcdcdc 100%); 335 | /* W3C */ 336 | } 337 | .dataTables_wrapper .dataTables_paginate .paginate_button.disabled, .dataTables_wrapper .dataTables_paginate .paginate_button.disabled:hover, .dataTables_wrapper .dataTables_paginate .paginate_button.disabled:active { 338 | cursor: default; 339 | color: #666 !important; 340 | border: 1px solid transparent; 341 | background: transparent; 342 | box-shadow: none; 343 | } 344 | .dataTables_wrapper .dataTables_paginate .paginate_button:hover { 345 | color: white !important; 346 | border: 1px solid #111; 347 | background-color: #585858; 348 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #585858), color-stop(100%, #111)); 349 | /* Chrome,Safari4+ */ 350 | background: -webkit-linear-gradient(top, #585858 0%, #111 100%); 351 | /* Chrome10+,Safari5.1+ */ 352 | background: -moz-linear-gradient(top, #585858 0%, #111 100%); 353 | /* FF3.6+ */ 354 | background: -ms-linear-gradient(top, #585858 0%, #111 100%); 355 | /* IE10+ */ 356 | background: -o-linear-gradient(top, #585858 0%, #111 100%); 357 | /* Opera 11.10+ */ 358 | background: linear-gradient(to bottom, #585858 0%, #111 100%); 359 | /* W3C */ 360 | } 361 | .dataTables_wrapper .dataTables_paginate .paginate_button:active { 362 | outline: none; 363 | background-color: #2b2b2b; 364 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #2b2b2b), color-stop(100%, #0c0c0c)); 365 | /* Chrome,Safari4+ */ 366 | background: -webkit-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%); 367 | /* Chrome10+,Safari5.1+ */ 368 | background: -moz-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%); 369 | /* FF3.6+ */ 370 | background: -ms-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%); 371 | /* IE10+ */ 372 | background: -o-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%); 373 | /* Opera 11.10+ */ 374 | background: linear-gradient(to bottom, #2b2b2b 0%, #0c0c0c 100%); 375 | /* W3C */ 376 | box-shadow: inset 0 0 3px #111; 377 | } 378 | .dataTables_wrapper .dataTables_paginate .ellipsis { 379 | padding: 0 1em; 380 | } 381 | .dataTables_wrapper .dataTables_processing { 382 | position: absolute; 383 | top: 50%; 384 | left: 50%; 385 | width: 100%; 386 | height: 40px; 387 | margin-left: -50%; 388 | margin-top: -25px; 389 | padding-top: 20px; 390 | text-align: center; 391 | font-size: 1.2em; 392 | background-color: white; 393 | background: -webkit-gradient(linear, left top, right top, color-stop(0%, rgba(255, 255, 255, 0)), color-stop(25%, rgba(255, 255, 255, 0.9)), color-stop(75%, rgba(255, 255, 255, 0.9)), color-stop(100%, rgba(255, 255, 255, 0))); 394 | background: -webkit-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%); 395 | background: -moz-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%); 396 | background: -ms-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%); 397 | background: -o-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%); 398 | background: linear-gradient(to right, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%); 399 | } 400 | .dataTables_wrapper .dataTables_length, 401 | .dataTables_wrapper .dataTables_filter, 402 | .dataTables_wrapper .dataTables_info, 403 | .dataTables_wrapper .dataTables_processing, 404 | .dataTables_wrapper .dataTables_paginate { 405 | color: #333; 406 | } 407 | .dataTables_wrapper .dataTables_scroll { 408 | clear: both; 409 | } 410 | .dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody { 411 | *margin-top: -1px; 412 | -webkit-overflow-scrolling: touch; 413 | } 414 | .dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > thead > tr > th, .dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > thead > tr > td, .dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > tbody > tr > th, .dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > tbody > tr > td { 415 | vertical-align: middle; 416 | } 417 | .dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > thead > tr > th > div.dataTables_sizing, 418 | .dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > thead > tr > td > div.dataTables_sizing, .dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > tbody > tr > th > div.dataTables_sizing, 419 | .dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > tbody > tr > td > div.dataTables_sizing { 420 | height: 0; 421 | overflow: hidden; 422 | margin: 0 !important; 423 | padding: 0 !important; 424 | } 425 | .dataTables_wrapper.no-footer .dataTables_scrollBody { 426 | border-bottom: 1px solid #111; 427 | } 428 | .dataTables_wrapper.no-footer div.dataTables_scrollHead table.dataTable, 429 | .dataTables_wrapper.no-footer div.dataTables_scrollBody > table { 430 | border-bottom: none; 431 | } 432 | .dataTables_wrapper:after { 433 | visibility: hidden; 434 | display: block; 435 | content: ""; 436 | clear: both; 437 | height: 0; 438 | } 439 | 440 | @media screen and (max-width: 767px) { 441 | .dataTables_wrapper .dataTables_info, 442 | .dataTables_wrapper .dataTables_paginate { 443 | float: none; 444 | text-align: center; 445 | } 446 | .dataTables_wrapper .dataTables_paginate { 447 | margin-top: 0.5em; 448 | } 449 | } 450 | @media screen and (max-width: 640px) { 451 | .dataTables_wrapper .dataTables_length, 452 | .dataTables_wrapper .dataTables_filter { 453 | float: none; 454 | text-align: center; 455 | } 456 | .dataTables_wrapper .dataTables_filter { 457 | margin-top: 0.5em; 458 | } 459 | } 460 | -------------------------------------------------------------------------------- /app/static/js/popper.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) Federico Zivolo 2019 3 | Distributed under the MIT License (license terms are at http://opensource.org/licenses/MIT). 4 | */(function(e,t){'object'==typeof exports&&'undefined'!=typeof module?module.exports=t():'function'==typeof define&&define.amd?define(t):e.Popper=t()})(this,function(){'use strict';function e(e){return e&&'[object Function]'==={}.toString.call(e)}function t(e,t){if(1!==e.nodeType)return[];var o=e.ownerDocument.defaultView,n=o.getComputedStyle(e,null);return t?n[t]:n}function o(e){return'HTML'===e.nodeName?e:e.parentNode||e.host}function n(e){if(!e)return document.body;switch(e.nodeName){case'HTML':case'BODY':return e.ownerDocument.body;case'#document':return e.body;}var i=t(e),r=i.overflow,p=i.overflowX,s=i.overflowY;return /(auto|scroll|overlay)/.test(r+s+p)?e:n(o(e))}function i(e){return e&&e.referenceNode?e.referenceNode:e}function r(e){return 11===e?re:10===e?pe:re||pe}function p(e){if(!e)return document.documentElement;for(var o=r(10)?document.body:null,n=e.offsetParent||null;n===o&&e.nextElementSibling;)n=(e=e.nextElementSibling).offsetParent;var i=n&&n.nodeName;return i&&'BODY'!==i&&'HTML'!==i?-1!==['TH','TD','TABLE'].indexOf(n.nodeName)&&'static'===t(n,'position')?p(n):n:e?e.ownerDocument.documentElement:document.documentElement}function s(e){var t=e.nodeName;return'BODY'!==t&&('HTML'===t||p(e.firstElementChild)===e)}function d(e){return null===e.parentNode?e:d(e.parentNode)}function a(e,t){if(!e||!e.nodeType||!t||!t.nodeType)return document.documentElement;var o=e.compareDocumentPosition(t)&Node.DOCUMENT_POSITION_FOLLOWING,n=o?e:t,i=o?t:e,r=document.createRange();r.setStart(n,0),r.setEnd(i,0);var l=r.commonAncestorContainer;if(e!==l&&t!==l||n.contains(i))return s(l)?l:p(l);var f=d(e);return f.host?a(f.host,t):a(e,d(t).host)}function l(e){var t=1=o.clientWidth&&n>=o.clientHeight}),l=0a[e]&&!t.escapeWithReference&&(n=Q(f[o],a[e]-('right'===e?f.width:f.height))),ae({},o,n)}};return l.forEach(function(e){var t=-1===['left','top'].indexOf(e)?'secondary':'primary';f=le({},f,m[t](e))}),e.offsets.popper=f,e},priority:['left','right','top','bottom'],padding:5,boundariesElement:'scrollParent'},keepTogether:{order:400,enabled:!0,fn:function(e){var t=e.offsets,o=t.popper,n=t.reference,i=e.placement.split('-')[0],r=Z,p=-1!==['top','bottom'].indexOf(i),s=p?'right':'bottom',d=p?'left':'top',a=p?'width':'height';return o[s]r(n[s])&&(e.offsets.popper[d]=r(n[s])),e}},arrow:{order:500,enabled:!0,fn:function(e,o){var n;if(!K(e.instance.modifiers,'arrow','keepTogether'))return e;var i=o.element;if('string'==typeof i){if(i=e.instance.popper.querySelector(i),!i)return e;}else if(!e.instance.popper.contains(i))return console.warn('WARNING: `arrow.element` must be child of its popper element!'),e;var r=e.placement.split('-')[0],p=e.offsets,s=p.popper,d=p.reference,a=-1!==['left','right'].indexOf(r),l=a?'height':'width',f=a?'Top':'Left',m=f.toLowerCase(),h=a?'left':'top',c=a?'bottom':'right',u=S(i)[l];d[c]-us[c]&&(e.offsets.popper[m]+=d[m]+u-s[c]),e.offsets.popper=g(e.offsets.popper);var b=d[m]+d[l]/2-u/2,w=t(e.instance.popper),y=parseFloat(w['margin'+f],10),E=parseFloat(w['border'+f+'Width'],10),v=b-e.offsets.popper[m]-y-E;return v=ee(Q(s[l]-u,v),0),e.arrowElement=i,e.offsets.arrow=(n={},ae(n,m,$(v)),ae(n,h,''),n),e},element:'[x-arrow]'},flip:{order:600,enabled:!0,fn:function(e,t){if(W(e.instance.modifiers,'inner'))return e;if(e.flipped&&e.placement===e.originalPlacement)return e;var o=v(e.instance.popper,e.instance.reference,t.padding,t.boundariesElement,e.positionFixed),n=e.placement.split('-')[0],i=T(n),r=e.placement.split('-')[1]||'',p=[];switch(t.behavior){case ce.FLIP:p=[n,i];break;case ce.CLOCKWISE:p=G(n);break;case ce.COUNTERCLOCKWISE:p=G(n,!0);break;default:p=t.behavior;}return p.forEach(function(s,d){if(n!==s||p.length===d+1)return e;n=e.placement.split('-')[0],i=T(n);var a=e.offsets.popper,l=e.offsets.reference,f=Z,m='left'===n&&f(a.right)>f(l.left)||'right'===n&&f(a.left)f(l.top)||'bottom'===n&&f(a.top)f(o.right),g=f(a.top)f(o.bottom),b='left'===n&&h||'right'===n&&c||'top'===n&&g||'bottom'===n&&u,w=-1!==['top','bottom'].indexOf(n),y=!!t.flipVariations&&(w&&'start'===r&&h||w&&'end'===r&&c||!w&&'start'===r&&g||!w&&'end'===r&&u),E=!!t.flipVariationsByContent&&(w&&'start'===r&&c||w&&'end'===r&&h||!w&&'start'===r&&u||!w&&'end'===r&&g),v=y||E;(m||b||v)&&(e.flipped=!0,(m||b)&&(n=p[d+1]),v&&(r=z(r)),e.placement=n+(r?'-'+r:''),e.offsets.popper=le({},e.offsets.popper,C(e.instance.popper,e.offsets.reference,e.placement)),e=P(e.instance.modifiers,e,'flip'))}),e},behavior:'flip',padding:5,boundariesElement:'viewport',flipVariations:!1,flipVariationsByContent:!1},inner:{order:700,enabled:!1,fn:function(e){var t=e.placement,o=t.split('-')[0],n=e.offsets,i=n.popper,r=n.reference,p=-1!==['left','right'].indexOf(o),s=-1===['top','left'].indexOf(o);return i[p?'left':'top']=r[o]-(s?i[p?'width':'height']:0),e.placement=T(t),e.offsets.popper=g(i),e}},hide:{order:800,enabled:!0,fn:function(e){if(!K(e.instance.modifiers,'hide','preventOverflow'))return e;var t=e.offsets.reference,o=D(e.instance.modifiers,function(e){return'preventOverflow'===e.name}).boundaries;if(t.bottomo.right||t.top>o.bottom||t.rightwindow.devicePixelRatio||!fe),c='bottom'===o?'top':'bottom',g='right'===n?'left':'right',b=B('transform');if(d='bottom'==c?'HTML'===l.nodeName?-l.clientHeight+h.bottom:-f.height+h.bottom:h.top,s='right'==g?'HTML'===l.nodeName?-l.clientWidth+h.right:-f.width+h.right:h.left,a&&b)m[b]='translate3d('+s+'px, '+d+'px, 0)',m[c]=0,m[g]=0,m.willChange='transform';else{var w='bottom'==c?-1:1,y='right'==g?-1:1;m[c]=d*w,m[g]=s*y,m.willChange=c+', '+g}var E={"x-placement":e.placement};return e.attributes=le({},E,e.attributes),e.styles=le({},m,e.styles),e.arrowStyles=le({},e.offsets.arrow,e.arrowStyles),e},gpuAcceleration:!0,x:'bottom',y:'right'},applyStyle:{order:900,enabled:!0,fn:function(e){return V(e.instance.popper,e.styles),j(e.instance.popper,e.attributes),e.arrowElement&&Object.keys(e.arrowStyles).length&&V(e.arrowElement,e.arrowStyles),e},onLoad:function(e,t,o,n,i){var r=L(i,t,e,o.positionFixed),p=O(o.placement,r,t,e,o.modifiers.flip.boundariesElement,o.modifiers.flip.padding);return t.setAttribute('x-placement',p),V(t,{position:o.positionFixed?'fixed':'absolute'}),o},gpuAcceleration:void 0}}},ge}); 5 | //# sourceMappingURL=popper.min.js.map 6 | -------------------------------------------------------------------------------- /app/static/js/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v4.4.1 (https://getbootstrap.com/) 3 | * Copyright 2011-2019 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports,require("jquery"),require("popper.js")):"function"==typeof define&&define.amd?define(["exports","jquery","popper.js"],e):e((t=t||self).bootstrap={},t.jQuery,t.Popper)}(this,function(t,g,u){"use strict";function i(t,e){for(var n=0;nthis._items.length-1||t<0))if(this._isSliding)g(this._element).one(Y.SLID,function(){return e.to(t)});else{if(n===t)return this.pause(),void this.cycle();var i=ndocument.documentElement.clientHeight;!this._isBodyOverflowing&&t&&(this._element.style.paddingLeft=this._scrollbarWidth+"px"),this._isBodyOverflowing&&!t&&(this._element.style.paddingRight=this._scrollbarWidth+"px")},t._resetAdjustments=function(){this._element.style.paddingLeft="",this._element.style.paddingRight=""},t._checkScrollbar=function(){var t=document.body.getBoundingClientRect();this._isBodyOverflowing=t.left+t.right
',trigger:"hover focus",title:"",delay:0,html:!1,selector:!1,placement:"top",offset:0,container:!1,fallbackPlacement:"flip",boundary:"scrollParent",sanitize:!0,sanitizeFn:null,whiteList:Se,popperConfig:null},Fe="show",Ue="out",We={HIDE:"hide"+Oe,HIDDEN:"hidden"+Oe,SHOW:"show"+Oe,SHOWN:"shown"+Oe,INSERTED:"inserted"+Oe,CLICK:"click"+Oe,FOCUSIN:"focusin"+Oe,FOCUSOUT:"focusout"+Oe,MOUSEENTER:"mouseenter"+Oe,MOUSELEAVE:"mouseleave"+Oe},qe="fade",Me="show",Ke=".tooltip-inner",Qe=".arrow",Be="hover",Ve="focus",Ye="click",ze="manual",Xe=function(){function i(t,e){if("undefined"==typeof u)throw new TypeError("Bootstrap's tooltips require Popper.js (https://popper.js.org/)");this._isEnabled=!0,this._timeout=0,this._hoverState="",this._activeTrigger={},this._popper=null,this.element=t,this.config=this._getConfig(e),this.tip=null,this._setListeners()}var t=i.prototype;return t.enable=function(){this._isEnabled=!0},t.disable=function(){this._isEnabled=!1},t.toggleEnabled=function(){this._isEnabled=!this._isEnabled},t.toggle=function(t){if(this._isEnabled)if(t){var e=this.constructor.DATA_KEY,n=g(t.currentTarget).data(e);n||(n=new this.constructor(t.currentTarget,this._getDelegateConfig()),g(t.currentTarget).data(e,n)),n._activeTrigger.click=!n._activeTrigger.click,n._isWithActiveTrigger()?n._enter(null,n):n._leave(null,n)}else{if(g(this.getTipElement()).hasClass(Me))return void this._leave(null,this);this._enter(null,this)}},t.dispose=function(){clearTimeout(this._timeout),g.removeData(this.element,this.constructor.DATA_KEY),g(this.element).off(this.constructor.EVENT_KEY),g(this.element).closest(".modal").off("hide.bs.modal",this._hideModalHandler),this.tip&&g(this.tip).remove(),this._isEnabled=null,this._timeout=null,this._hoverState=null,this._activeTrigger=null,this._popper&&this._popper.destroy(),this._popper=null,this.element=null,this.config=null,this.tip=null},t.show=function(){var e=this;if("none"===g(this.element).css("display"))throw new Error("Please use show on visible elements");var t=g.Event(this.constructor.Event.SHOW);if(this.isWithContent()&&this._isEnabled){g(this.element).trigger(t);var n=_.findShadowRoot(this.element),i=g.contains(null!==n?n:this.element.ownerDocument.documentElement,this.element);if(t.isDefaultPrevented()||!i)return;var o=this.getTipElement(),r=_.getUID(this.constructor.NAME);o.setAttribute("id",r),this.element.setAttribute("aria-describedby",r),this.setContent(),this.config.animation&&g(o).addClass(qe);var s="function"==typeof this.config.placement?this.config.placement.call(this,o,this.element):this.config.placement,a=this._getAttachment(s);this.addAttachmentClass(a);var l=this._getContainer();g(o).data(this.constructor.DATA_KEY,this),g.contains(this.element.ownerDocument.documentElement,this.tip)||g(o).appendTo(l),g(this.element).trigger(this.constructor.Event.INSERTED),this._popper=new u(this.element,o,this._getPopperConfig(a)),g(o).addClass(Me),"ontouchstart"in document.documentElement&&g(document.body).children().on("mouseover",null,g.noop);var c=function(){e.config.animation&&e._fixTransition();var t=e._hoverState;e._hoverState=null,g(e.element).trigger(e.constructor.Event.SHOWN),t===Ue&&e._leave(null,e)};if(g(this.tip).hasClass(qe)){var h=_.getTransitionDurationFromElement(this.tip);g(this.tip).one(_.TRANSITION_END,c).emulateTransitionEnd(h)}else c()}},t.hide=function(t){function e(){n._hoverState!==Fe&&i.parentNode&&i.parentNode.removeChild(i),n._cleanTipClass(),n.element.removeAttribute("aria-describedby"),g(n.element).trigger(n.constructor.Event.HIDDEN),null!==n._popper&&n._popper.destroy(),t&&t()}var n=this,i=this.getTipElement(),o=g.Event(this.constructor.Event.HIDE);if(g(this.element).trigger(o),!o.isDefaultPrevented()){if(g(i).removeClass(Me),"ontouchstart"in document.documentElement&&g(document.body).children().off("mouseover",null,g.noop),this._activeTrigger[Ye]=!1,this._activeTrigger[Ve]=!1,this._activeTrigger[Be]=!1,g(this.tip).hasClass(qe)){var r=_.getTransitionDurationFromElement(i);g(i).one(_.TRANSITION_END,e).emulateTransitionEnd(r)}else e();this._hoverState=""}},t.update=function(){null!==this._popper&&this._popper.scheduleUpdate()},t.isWithContent=function(){return Boolean(this.getTitle())},t.addAttachmentClass=function(t){g(this.getTipElement()).addClass(Pe+"-"+t)},t.getTipElement=function(){return this.tip=this.tip||g(this.config.template)[0],this.tip},t.setContent=function(){var t=this.getTipElement();this.setElementContent(g(t.querySelectorAll(Ke)),this.getTitle()),g(t).removeClass(qe+" "+Me)},t.setElementContent=function(t,e){"object"!=typeof e||!e.nodeType&&!e.jquery?this.config.html?(this.config.sanitize&&(e=we(e,this.config.whiteList,this.config.sanitizeFn)),t.html(e)):t.text(e):this.config.html?g(e).parent().is(t)||t.empty().append(e):t.text(g(e).text())},t.getTitle=function(){var t=this.element.getAttribute("data-original-title");return t=t||("function"==typeof this.config.title?this.config.title.call(this.element):this.config.title)},t._getPopperConfig=function(t){var e=this;return l({},{placement:t,modifiers:{offset:this._getOffset(),flip:{behavior:this.config.fallbackPlacement},arrow:{element:Qe},preventOverflow:{boundariesElement:this.config.boundary}},onCreate:function(t){t.originalPlacement!==t.placement&&e._handlePopperPlacementChange(t)},onUpdate:function(t){return e._handlePopperPlacementChange(t)}},{},this.config.popperConfig)},t._getOffset=function(){var e=this,t={};return"function"==typeof this.config.offset?t.fn=function(t){return t.offsets=l({},t.offsets,{},e.config.offset(t.offsets,e.element)||{}),t}:t.offset=this.config.offset,t},t._getContainer=function(){return!1===this.config.container?document.body:_.isElement(this.config.container)?g(this.config.container):g(document).find(this.config.container)},t._getAttachment=function(t){return Re[t.toUpperCase()]},t._setListeners=function(){var i=this;this.config.trigger.split(" ").forEach(function(t){if("click"===t)g(i.element).on(i.constructor.Event.CLICK,i.config.selector,function(t){return i.toggle(t)});else if(t!==ze){var e=t===Be?i.constructor.Event.MOUSEENTER:i.constructor.Event.FOCUSIN,n=t===Be?i.constructor.Event.MOUSELEAVE:i.constructor.Event.FOCUSOUT;g(i.element).on(e,i.config.selector,function(t){return i._enter(t)}).on(n,i.config.selector,function(t){return i._leave(t)})}}),this._hideModalHandler=function(){i.element&&i.hide()},g(this.element).closest(".modal").on("hide.bs.modal",this._hideModalHandler),this.config.selector?this.config=l({},this.config,{trigger:"manual",selector:""}):this._fixTitle()},t._fixTitle=function(){var t=typeof this.element.getAttribute("data-original-title");!this.element.getAttribute("title")&&"string"==t||(this.element.setAttribute("data-original-title",this.element.getAttribute("title")||""),this.element.setAttribute("title",""))},t._enter=function(t,e){var n=this.constructor.DATA_KEY;(e=e||g(t.currentTarget).data(n))||(e=new this.constructor(t.currentTarget,this._getDelegateConfig()),g(t.currentTarget).data(n,e)),t&&(e._activeTrigger["focusin"===t.type?Ve:Be]=!0),g(e.getTipElement()).hasClass(Me)||e._hoverState===Fe?e._hoverState=Fe:(clearTimeout(e._timeout),e._hoverState=Fe,e.config.delay&&e.config.delay.show?e._timeout=setTimeout(function(){e._hoverState===Fe&&e.show()},e.config.delay.show):e.show())},t._leave=function(t,e){var n=this.constructor.DATA_KEY;(e=e||g(t.currentTarget).data(n))||(e=new this.constructor(t.currentTarget,this._getDelegateConfig()),g(t.currentTarget).data(n,e)),t&&(e._activeTrigger["focusout"===t.type?Ve:Be]=!1),e._isWithActiveTrigger()||(clearTimeout(e._timeout),e._hoverState=Ue,e.config.delay&&e.config.delay.hide?e._timeout=setTimeout(function(){e._hoverState===Ue&&e.hide()},e.config.delay.hide):e.hide())},t._isWithActiveTrigger=function(){for(var t in this._activeTrigger)if(this._activeTrigger[t])return!0;return!1},t._getConfig=function(t){var e=g(this.element).data();return Object.keys(e).forEach(function(t){-1!==je.indexOf(t)&&delete e[t]}),"number"==typeof(t=l({},this.constructor.Default,{},e,{},"object"==typeof t&&t?t:{})).delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),_.typeCheckConfig(Ae,t,this.constructor.DefaultType),t.sanitize&&(t.template=we(t.template,t.whiteList,t.sanitizeFn)),t},t._getDelegateConfig=function(){var t={};if(this.config)for(var e in this.config)this.constructor.Default[e]!==this.config[e]&&(t[e]=this.config[e]);return t},t._cleanTipClass=function(){var t=g(this.getTipElement()),e=t.attr("class").match(Le);null!==e&&e.length&&t.removeClass(e.join(""))},t._handlePopperPlacementChange=function(t){var e=t.instance;this.tip=e.popper,this._cleanTipClass(),this.addAttachmentClass(this._getAttachment(t.placement))},t._fixTransition=function(){var t=this.getTipElement(),e=this.config.animation;null===t.getAttribute("x-placement")&&(g(t).removeClass(qe),this.config.animation=!1,this.hide(),this.show(),this.config.animation=e)},i._jQueryInterface=function(n){return this.each(function(){var t=g(this).data(Ne),e="object"==typeof n&&n;if((t||!/dispose|hide/.test(n))&&(t||(t=new i(this,e),g(this).data(Ne,t)),"string"==typeof n)){if("undefined"==typeof t[n])throw new TypeError('No method named "'+n+'"');t[n]()}})},s(i,null,[{key:"VERSION",get:function(){return"4.4.1"}},{key:"Default",get:function(){return xe}},{key:"NAME",get:function(){return Ae}},{key:"DATA_KEY",get:function(){return Ne}},{key:"Event",get:function(){return We}},{key:"EVENT_KEY",get:function(){return Oe}},{key:"DefaultType",get:function(){return He}}]),i}();g.fn[Ae]=Xe._jQueryInterface,g.fn[Ae].Constructor=Xe,g.fn[Ae].noConflict=function(){return g.fn[Ae]=ke,Xe._jQueryInterface};var $e="popover",Ge="bs.popover",Je="."+Ge,Ze=g.fn[$e],tn="bs-popover",en=new RegExp("(^|\\s)"+tn+"\\S+","g"),nn=l({},Xe.Default,{placement:"right",trigger:"click",content:"",template:''}),on=l({},Xe.DefaultType,{content:"(string|element|function)"}),rn="fade",sn="show",an=".popover-header",ln=".popover-body",cn={HIDE:"hide"+Je,HIDDEN:"hidden"+Je,SHOW:"show"+Je,SHOWN:"shown"+Je,INSERTED:"inserted"+Je,CLICK:"click"+Je,FOCUSIN:"focusin"+Je,FOCUSOUT:"focusout"+Je,MOUSEENTER:"mouseenter"+Je,MOUSELEAVE:"mouseleave"+Je},hn=function(t){function i(){return t.apply(this,arguments)||this}!function(t,e){t.prototype=Object.create(e.prototype),(t.prototype.constructor=t).__proto__=e}(i,t);var e=i.prototype;return e.isWithContent=function(){return this.getTitle()||this._getContent()},e.addAttachmentClass=function(t){g(this.getTipElement()).addClass(tn+"-"+t)},e.getTipElement=function(){return this.tip=this.tip||g(this.config.template)[0],this.tip},e.setContent=function(){var t=g(this.getTipElement());this.setElementContent(t.find(an),this.getTitle());var e=this._getContent();"function"==typeof e&&(e=e.call(this.element)),this.setElementContent(t.find(ln),e),t.removeClass(rn+" "+sn)},e._getContent=function(){return this.element.getAttribute("data-content")||this.config.content},e._cleanTipClass=function(){var t=g(this.getTipElement()),e=t.attr("class").match(en);null!==e&&0=this._offsets[o]&&("undefined"==typeof this._offsets[o+1]||t+~]|"+R+")"+R+"*"),U=new RegExp(R+"|>"),V=new RegExp(W),X=new RegExp("^"+B+"$"),Q={ID:new RegExp("^#("+B+")"),CLASS:new RegExp("^\\.("+B+")"),TAG:new RegExp("^("+B+"|[*])"),ATTR:new RegExp("^"+M),PSEUDO:new RegExp("^"+W),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+R+"*(even|odd|(([+-]|)(\\d*)n|)"+R+"*(?:([+-]|)"+R+"*(\\d+)|))"+R+"*\\)|)","i"),bool:new RegExp("^(?:"+I+")$","i"),needsContext:new RegExp("^"+R+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+R+"*((?:-\\d)?\\d*)"+R+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,G=/^(?:input|select|textarea|button)$/i,K=/^h\d$/i,J=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\([\\da-f]{1,6}"+R+"?|("+R+")|.)","ig"),ne=function(e,t,n){var r="0x"+t-65536;return r!=r||n?t:r<0?String.fromCharCode(r+65536):String.fromCharCode(r>>10|55296,1023&r|56320)},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){C()},ae=xe(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{O.apply(t=P.call(m.childNodes),m.childNodes),t[m.childNodes.length].nodeType}catch(e){O={apply:t.length?function(e,t){q.apply(e,P.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,d=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==d&&9!==d&&11!==d)return n;if(!r&&((e?e.ownerDocument||e:m)!==T&&C(e),e=e||T,E)){if(11!==d&&(u=Z.exec(t)))if(i=u[1]){if(9===d){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return O.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&p.getElementsByClassName&&e.getElementsByClassName)return O.apply(n,e.getElementsByClassName(i)),n}if(p.qsa&&!S[t+" "]&&(!v||!v.test(t))&&(1!==d||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===d&&U.test(t)){(s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=N),o=(l=h(t)).length;while(o--)l[o]="#"+s+" "+be(l[o]);c=l.join(","),f=ee.test(t)&&ye(e.parentNode)||e}try{return O.apply(n,f.querySelectorAll(c)),n}catch(e){S(t,!0)}finally{s===N&&e.removeAttribute("id")}}}return g(t.replace(F,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>x.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[N]=!0,e}function ce(e){var t=T.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)x.attrHandle[n[r]]=t}function de(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function pe(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in p=se.support={},i=se.isXML=function(e){var t=e.namespaceURI,n=(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},C=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:m;return r!==T&&9===r.nodeType&&r.documentElement&&(a=(T=r).documentElement,E=!i(T),m!==T&&(n=T.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),p.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),p.getElementsByTagName=ce(function(e){return e.appendChild(T.createComment("")),!e.getElementsByTagName("*").length}),p.getElementsByClassName=J.test(T.getElementsByClassName),p.getById=ce(function(e){return a.appendChild(e).id=N,!T.getElementsByName||!T.getElementsByName(N).length}),p.getById?(x.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},x.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(x.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},x.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),x.find.TAG=p.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):p.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},x.find.CLASS=p.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(p.qsa=J.test(T.querySelectorAll))&&(ce(function(e){a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+R+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+R+"*(?:value|"+I+")"),e.querySelectorAll("[id~="+N+"-]").length||v.push("~="),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+N+"+*").length||v.push(".#.+[+~]")}),ce(function(e){e.innerHTML="";var t=T.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+R+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(p.matchesSelector=J.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){p.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",W)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=J.test(a.compareDocumentPosition),y=t||J.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},D=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)===(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!p.sortDetached&&t.compareDocumentPosition(e)===n?e===T||e.ownerDocument===m&&y(m,e)?-1:t===T||t.ownerDocument===m&&y(m,t)?1:u?H(u,e)-H(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e===T?-1:t===T?1:i?-1:o?1:u?H(u,e)-H(u,t):0;if(i===o)return de(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?de(a[r],s[r]):a[r]===m?-1:s[r]===m?1:0}),T},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if((e.ownerDocument||e)!==T&&C(e),p.matchesSelector&&E&&!S[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||p.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){S(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return Q.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&V.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=d[e+" "];return t||(t=new RegExp("(^|"+R+")"+e+"("+R+"|$)"))&&d(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function L(e,n,r){return x(n)?E.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?E.grep(e,function(e){return e===n!==r}):"string"!=typeof n?E.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(E.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||j,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof E?t[0]:t,E.merge(this,E.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:v,!0)),D.test(r[1])&&E.isPlainObject(t))for(r in t)x(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=v.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):x(e)?void 0!==n.ready?n.ready(e):e(E):E.makeArray(e,this)}).prototype=E.fn,j=E(v);var O=/^(?:parents|prev(?:Until|All))/,P={children:!0,contents:!0,next:!0,prev:!0};function H(e,t){while((e=e[t])&&1!==e.nodeType);return e}E.fn.extend({has:function(e){var t=E(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,pe=/^$|^module$|\/(?:java|ecma)script/i,he={option:[1,""],thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function ge(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&S(e,t)?E.merge([e],n):n}function ve(e,t){for(var n=0,r=e.length;nx",b.noCloneChecked=!!ye.cloneNode(!0).lastChild.defaultValue;var we=/^key/,Ce=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,Te=/^([^.]*)(?:\.(.+)|)/;function Ee(){return!0}function Ne(){return!1}function Ae(e,t){return e===function(){try{return v.activeElement}catch(e){}}()==("focus"===t)}function ke(e,t,n,r,i,o){var a,s;if("object"==typeof t){for(s in"string"!=typeof n&&(r=r||n,n=void 0),t)ke(e,s,n,r,t[s],o);return e}if(null==r&&null==i?(i=n,r=n=void 0):null==i&&("string"==typeof n?(i=r,r=void 0):(i=r,r=n,n=void 0)),!1===i)i=Ne;else if(!i)return e;return 1===o&&(a=i,(i=function(e){return E().off(e),a.apply(this,arguments)}).guid=a.guid||(a.guid=E.guid++)),e.each(function(){E.event.add(this,t,i,r,n)})}function Se(e,i,o){o?(G.set(e,i,!1),E.event.add(e,i,{namespace:!1,handler:function(e){var t,n,r=G.get(this,i);if(1&e.isTrigger&&this[i]){if(r.length)(E.event.special[i]||{}).delegateType&&e.stopPropagation();else if(r=s.call(arguments),G.set(this,i,r),t=o(this,i),this[i](),r!==(n=G.get(this,i))||t?G.set(this,i,!1):n={},r!==n)return e.stopImmediatePropagation(),e.preventDefault(),n.value}else r.length&&(G.set(this,i,{value:E.event.trigger(E.extend(r[0],E.Event.prototype),r.slice(1),this)}),e.stopImmediatePropagation())}})):void 0===G.get(e,i)&&E.event.add(e,i,Ee)}E.event={global:{},add:function(t,e,n,r,i){var o,a,s,u,l,c,f,d,p,h,g,v=G.get(t);if(v){n.handler&&(n=(o=n).handler,i=o.selector),i&&E.find.matchesSelector(ie,i),n.guid||(n.guid=E.guid++),(u=v.events)||(u=v.events={}),(a=v.handle)||(a=v.handle=function(e){return"undefined"!=typeof E&&E.event.triggered!==e.type?E.event.dispatch.apply(t,arguments):void 0}),l=(e=(e||"").match(I)||[""]).length;while(l--)p=g=(s=Te.exec(e[l])||[])[1],h=(s[2]||"").split(".").sort(),p&&(f=E.event.special[p]||{},p=(i?f.delegateType:f.bindType)||p,f=E.event.special[p]||{},c=E.extend({type:p,origType:g,data:r,handler:n,guid:n.guid,selector:i,needsContext:i&&E.expr.match.needsContext.test(i),namespace:h.join(".")},o),(d=u[p])||((d=u[p]=[]).delegateCount=0,f.setup&&!1!==f.setup.call(t,r,h,a)||t.addEventListener&&t.addEventListener(p,a)),f.add&&(f.add.call(t,c),c.handler.guid||(c.handler.guid=n.guid)),i?d.splice(d.delegateCount++,0,c):d.push(c),E.event.global[p]=!0)}},remove:function(e,t,n,r,i){var o,a,s,u,l,c,f,d,p,h,g,v=G.hasData(e)&&G.get(e);if(v&&(u=v.events)){l=(t=(t||"").match(I)||[""]).length;while(l--)if(p=g=(s=Te.exec(t[l])||[])[1],h=(s[2]||"").split(".").sort(),p){f=E.event.special[p]||{},d=u[p=(r?f.delegateType:f.bindType)||p]||[],s=s[2]&&new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),a=o=d.length;while(o--)c=d[o],!i&&g!==c.origType||n&&n.guid!==c.guid||s&&!s.test(c.namespace)||r&&r!==c.selector&&("**"!==r||!c.selector)||(d.splice(o,1),c.selector&&d.delegateCount--,f.remove&&f.remove.call(e,c));a&&!d.length&&(f.teardown&&!1!==f.teardown.call(e,h,v.handle)||E.removeEvent(e,p,v.handle),delete u[p])}else for(p in u)E.event.remove(e,p+t[l],n,r,!0);E.isEmptyObject(u)&&G.remove(e,"handle events")}},dispatch:function(e){var t,n,r,i,o,a,s=E.event.fix(e),u=new Array(arguments.length),l=(G.get(this,"events")||{})[s.type]||[],c=E.event.special[s.type]||{};for(u[0]=s,t=1;t\x20\t\r\n\f]*)[^>]*)\/>/gi,Le=/\s*$/g;function Oe(e,t){return S(e,"table")&&S(11!==t.nodeType?t:t.firstChild,"tr")&&E(e).children("tbody")[0]||e}function Pe(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function He(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Ie(e,t){var n,r,i,o,a,s,u,l;if(1===t.nodeType){if(G.hasData(e)&&(o=G.access(e),a=G.set(t,o),l=o.events))for(i in delete a.handle,a.events={},l)for(n=0,r=l[i].length;n")},clone:function(e,t,n){var r,i,o,a,s,u,l,c=e.cloneNode(!0),f=oe(e);if(!(b.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||E.isXMLDoc(e)))for(a=ge(c),r=0,i=(o=ge(e)).length;r
",2===pt.childNodes.length),E.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(b.createHTMLDocument?((r=(t=v.implementation.createHTMLDocument("")).createElement("base")).href=v.location.href,t.head.appendChild(r)):t=v),o=!n&&[],(i=D.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&E(o).remove(),E.merge([],i.childNodes)));var r,i,o},E.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=E.css(e,"position"),c=E(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=E.css(e,"top"),u=E.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),x(t)&&(t=t.call(e,n,E.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},E.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){E.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===E.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===E.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=E(e).offset()).top+=E.css(e,"borderTopWidth",!0),i.left+=E.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-E.css(r,"marginTop",!0),left:t.left-i.left-E.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===E.css(e,"position"))e=e.offsetParent;return e||ie})}}),E.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;E.fn[t]=function(e){return z(this,function(e,t,n){var r;if(w(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),E.each(["top","left"],function(e,n){E.cssHooks[n]=ze(b.pixelPosition,function(e,t){if(t)return t=Fe(e,n),Me.test(t)?E(e).position()[n]+"px":t})}),E.each({Height:"height",Width:"width"},function(a,s){E.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){E.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return z(this,function(e,t,n){var r;return w(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?E.css(e,t,i):E.style(e,t,n,i)},s,n?e:void 0,n)}})}),E.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){E.fn[n]=function(e,t){return 0