├── flask_final ├── logs │ ├── kbd.log │ ├── flask_app.log │ └── newslet │ │ ├── extractors.log │ │ └── kantipur.log ├── main │ ├── __init__.py │ └── routes.py ├── users │ ├── __init__.py │ ├── models.py │ ├── forms.py │ └── routes.py ├── newslet │ ├── __init__.py │ ├── rss_news.py │ ├── nagarik_international.py │ ├── kantipur_daily.py │ ├── kantipur_international.py │ ├── top_international_news.py │ ├── kathmandupost.py │ ├── models.py │ ├── news_loader.py │ └── routes.py ├── static │ ├── images │ │ ├── icon.jpg │ │ ├── lake.jpg │ │ ├── papers.jpeg │ │ ├── tools.jpeg │ │ ├── dashboard.jpg │ │ └── news_sources.jpeg │ ├── home │ │ └── home.css │ └── dashboard │ │ └── dashboard.css ├── __init__.py ├── templates │ ├── about.html │ ├── reset_request.html │ ├── reset_password.html │ ├── login.html │ ├── layout.html │ ├── detail_news.html │ ├── home.html │ ├── privacy_policy.html │ ├── signup.html │ ├── reset_password_email.html │ └── dashboard.html └── config.py ├── Procfile ├── migrations ├── README ├── script.py.mako ├── alembic.ini ├── env.py └── versions │ └── f682ccb0c931_.py ├── .gitattributes ├── Makefile ├── .gitignore ├── template_secrets.json ├── Dockerfile ├── run.py ├── manage.py ├── docs ├── venv.md └── postgres_setup.md ├── LICENSE ├── pyproject.toml ├── README.md └── requirements.txt /flask_final/logs/kbd.log: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /flask_final/main/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /flask_final/logs/flask_app.log: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /flask_final/users/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: gunicorn run:app 2 | -------------------------------------------------------------------------------- /flask_final/logs/newslet/extractors.log: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /migrations/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * linguist-vendored 2 | *.py linguist-vendored=false -------------------------------------------------------------------------------- /flask_final/newslet/__init__.py: -------------------------------------------------------------------------------- 1 | # parser = 'lxml' 2 | parser = "html.parser" 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | dev: 2 | .venv/bin/python manage.py db upgrade 3 | .venv/bin/python run.py debug 4 | -------------------------------------------------------------------------------- /flask_final/static/images/icon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hemanta212/Nepali-news-portal-kbd/HEAD/flask_final/static/images/icon.jpg -------------------------------------------------------------------------------- /flask_final/static/images/lake.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hemanta212/Nepali-news-portal-kbd/HEAD/flask_final/static/images/lake.jpg -------------------------------------------------------------------------------- /flask_final/static/images/papers.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hemanta212/Nepali-news-portal-kbd/HEAD/flask_final/static/images/papers.jpeg -------------------------------------------------------------------------------- /flask_final/static/images/tools.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hemanta212/Nepali-news-portal-kbd/HEAD/flask_final/static/images/tools.jpeg -------------------------------------------------------------------------------- /flask_final/static/images/dashboard.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hemanta212/Nepali-news-portal-kbd/HEAD/flask_final/static/images/dashboard.jpg -------------------------------------------------------------------------------- /flask_final/static/images/news_sources.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hemanta212/Nepali-news-portal-kbd/HEAD/flask_final/static/images/news_sources.jpeg -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | *venv/ 3 | /.vscode/ 4 | *.pyc 5 | *.vim 6 | *.swp 7 | *.swo 8 | *.swn 9 | *.lock 10 | *.orig 11 | secrets* 12 | *.db 13 | *.env 14 | -------------------------------------------------------------------------------- /flask_final/logs/newslet/kantipur.log: -------------------------------------------------------------------------------- 1 | titles:2019/03/122019/03/122019/03/122019/03/122019/03/122019/03/122019/03/122019/03/122019/03/122019/03/122019/03/122019/03/122019/03/122019/03/122019/03/122019/03/12 -------------------------------------------------------------------------------- /template_secrets.json: -------------------------------------------------------------------------------- 1 | { 2 | "SECRET_KEY": "jpt strings like lskdjf;slkdfjldskfj;kldf", 3 | "SQLALCHEMY_DATABASE_URI": "sqlite:///site.db", 4 | "MAIL_USERNAME": "your email", 5 | "MAIL_PASSWORD": "your password" 6 | } 7 | -------------------------------------------------------------------------------- /migrations/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | ${imports if imports else ""} 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = ${repr(up_revision)} 14 | down_revision = ${repr(down_revision)} 15 | branch_labels = ${repr(branch_labels)} 16 | depends_on = ${repr(depends_on)} 17 | 18 | 19 | def upgrade(): 20 | ${upgrades if upgrades else "pass"} 21 | 22 | 23 | def downgrade(): 24 | ${downgrades if downgrades else "pass"} 25 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10.6-slim as python 2 | ENV PYTHONUNBUFFERED=true 3 | WORKDIR /app 4 | 5 | FROM python as poetry 6 | ENV POETRY_HOME=/opt/poetry 7 | ENV POETRY_VIRTUALENVS_IN_PROJECT=true 8 | ENV PATH="$POETRY_HOME/bin:$PATH" 9 | RUN python3 -m venv $POETRY_HOME 10 | RUN $POETRY_HOME/bin/pip install poetry==1.3.2 11 | COPY . ./ 12 | COPY template_secrets.json secrets.json 13 | RUN poetry install --only main --no-interaction --no-ansi -vvv 14 | 15 | FROM python as runtime 16 | ENV PATH="/app/.venv/bin:$PATH" 17 | COPY --from=poetry /app /app 18 | EXPOSE 5000 19 | CMD python manage.py db upgrade && python run.py debug 20 | -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from flask_final.config import Prod, Debug, Secrets 4 | from flask_final import create_app 5 | 6 | app = None 7 | args = sys.argv[1:] 8 | if "run:app" in args: 9 | args = sys.argv[2:] 10 | 11 | if len(args) == 0 or args[0] == "prod": 12 | app = create_app(Prod) 13 | elif args[0] == "debug": 14 | app = create_app(Debug) 15 | else: 16 | print(f"Usage: run.py [prod/debug]. Unrecognized argument {args[0]}") 17 | 18 | is_env_var_set = os.getenv("SQLALCHEMY_DATABASE_URI") 19 | if not is_env_var_set: 20 | print( 21 | ":: No environment variables set!\n" 22 | ":: Falling back to Debug Mode and reading secrets from file" 23 | ) 24 | app = create_app(Secrets()) 25 | 26 | if __name__ == "__main__": 27 | app.run('0.0.0.0') 28 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from flask_final.config import Debug, Secrets 4 | from flask_final import db, create_app 5 | 6 | is_env_var_set = os.getenv("SQLALCHEMY_DATABASE_URI") 7 | if not is_env_var_set: 8 | config = Secrets() 9 | else: 10 | config = Debug 11 | 12 | # Support for relative sqlite URIs 13 | if config.SQLALCHEMY_DATABASE_URI == "sqlite:///site.db": 14 | temp_app = create_app(config) 15 | config.SQLALCHEMY_DATABASE_URI = "sqlite:///" + os.path.join( 16 | temp_app.root_path, "site.db" 17 | ) 18 | 19 | app = create_app(config) 20 | 21 | from flask_script import Manager 22 | from flask_migrate import Migrate, MigrateCommand 23 | 24 | migrate = Migrate(app, db) 25 | manager = Manager(app) 26 | manager.add_command("db", MigrateCommand) 27 | 28 | if __name__ == "__main__": 29 | manager.run() 30 | -------------------------------------------------------------------------------- /flask_final/main/routes.py: -------------------------------------------------------------------------------- 1 | """ 2 | contains core routes of webapp 3 | """ 4 | 5 | from flask import render_template, redirect, Blueprint, url_for 6 | from flask_login import current_user 7 | 8 | main = Blueprint("main", __name__) 9 | 10 | 11 | @main.route("/", methods=["GET", "POST"]) 12 | def home(): 13 | # if current_user.is_authenticated: 14 | return redirect(url_for("newslet.nep_national_news")) 15 | # return redirect(url_for("newslet.eng_international_news")) 16 | 17 | 18 | @main.route("/home", methods=["GET", "POST"]) 19 | def homepage(): 20 | return render_template("home.html") 21 | 22 | 23 | @main.route("/about", methods=["GET"]) 24 | def about(): 25 | return render_template("about.html", title="About this website") 26 | 27 | 28 | @main.route("/privacy_policy", methods=["GET"]) 29 | def privacy_policy(): 30 | return render_template("privacy_policy.html", title="Privacy policy") 31 | -------------------------------------------------------------------------------- /migrations/alembic.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # template used to generate migration files 5 | # file_template = %%(rev)s_%%(slug)s 6 | 7 | # set to 'true' to run the environment during 8 | # the 'revision' command, regardless of autogenerate 9 | # revision_environment = false 10 | 11 | 12 | # Logging configuration 13 | [loggers] 14 | keys = root,sqlalchemy,alembic 15 | 16 | [handlers] 17 | keys = console 18 | 19 | [formatters] 20 | keys = generic 21 | 22 | [logger_root] 23 | level = WARN 24 | handlers = console 25 | qualname = 26 | 27 | [logger_sqlalchemy] 28 | level = WARN 29 | handlers = 30 | qualname = sqlalchemy.engine 31 | 32 | [logger_alembic] 33 | level = INFO 34 | handlers = 35 | qualname = alembic 36 | 37 | [handler_console] 38 | class = StreamHandler 39 | args = (sys.stderr,) 40 | level = NOTSET 41 | formatter = generic 42 | 43 | [formatter_generic] 44 | format = %(levelname)-5.5s [%(name)s] %(message)s 45 | datefmt = %H:%M:%S 46 | -------------------------------------------------------------------------------- /docs/venv.md: -------------------------------------------------------------------------------- 1 | ### Setting up a Virtual Environment 2 | Venv is provided with the default installation of python since python 3.4 and above. 3 | 4 | * Make a virtual environment: 5 | ``` 6 | python -m venv venv 7 | ``` 8 | 9 | * Activate it 10 | 11 | - For Windows: 12 | ``` 13 | venv\Scripts\activate 14 | ``` 15 | 16 | - For Linux/Mac/(other Unix): 17 | ``` 18 | source venv/bin/activate 19 | ``` 20 | 21 | Once activated you can use regular python and pip commands and it will operate under your local project. 22 | 23 | You can verify this by running 24 | ``` 25 | $ where python # for Windows 26 | $ which pip python # for others 27 | ``` 28 | 29 | NOTE: This applies to not only pip and python but also other python scripts like black, pylint, etc 30 | 31 | After finishing your work you can simply use the ```deactivate``` command to deactivate your virtual environment and use the system's version of pip and python. 32 | 33 | Similarly closing the current terminal window also deactivates it. 34 | 35 | 36 | -------------------------------------------------------------------------------- /flask_final/static/home/home.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | width: 100%; 4 | height: 100%; 5 | } 6 | 7 | nav { 8 | padding-bottom: 2%; 9 | } 10 | 11 | #nav1 ul { 12 | text-align: right; 13 | margin-left: 70%; 14 | } 15 | 16 | #nav1 ul a { 17 | margin-left: 16px; 18 | } 19 | 20 | ul a:hover { 21 | border-bottom: 2px solid black; 22 | } 23 | 24 | #intro { 25 | padding: 14% 0; 26 | background-size: cover; 27 | height: 100%; 28 | margin-top: 3%; 29 | } 30 | 31 | #get { 32 | background: #bab9b9; 33 | } 34 | 35 | #id1 { 36 | background-size: cover; 37 | background: url(images/bg.jpg); 38 | } 39 | 40 | #row1 { 41 | color: white; 42 | } 43 | 44 | #content { 45 | color: black; 46 | } 47 | 48 | .social a { 49 | margin: 17px; 50 | 51 | } 52 | 53 | footer { 54 | color: white; 55 | background: black; 56 | margin-top: -2%; 57 | } 58 | 59 | #ft1, 60 | #ft2 { 61 | margin-top: 2%; 62 | } 63 | 64 | .text-overlay { 65 | color: black; 66 | } 67 | 68 | .get-started { 69 | color: white; 70 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Hemanta Sharma 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /flask_final/__init__.py: -------------------------------------------------------------------------------- 1 | # -*-code: UTF-8 2 | """ 3 | Initialization module of projects that initializes 4 | app, db, Kbdlog,bcrypt, login_manager, mail, Config 5 | 6 | """ 7 | import json 8 | import os 9 | 10 | from flask import Flask 11 | from flask_sqlalchemy import SQLAlchemy 12 | from flask_bcrypt import Bcrypt 13 | from flask_login import LoginManager 14 | from flask_mail import Mail 15 | 16 | bcrypt = Bcrypt() 17 | login_manager = LoginManager() 18 | login_manager.login_view = "users.login" 19 | login_manager.login_message_category = "info" 20 | db = SQLAlchemy() 21 | mail = Mail() 22 | NEWS_API_KEY = os.getenv("NEWS_API_KEY") 23 | 24 | 25 | def create_app(config): 26 | app = Flask(__name__.split('.')[0]) 27 | app.config.from_object(config) 28 | 29 | db.init_app(app) 30 | bcrypt.init_app(app) 31 | login_manager.init_app(app) 32 | mail.init_app(app) 33 | 34 | from flask_final.users.routes import users 35 | from flask_final.newslet.routes import newslet 36 | from flask_final.main.routes import main 37 | 38 | app.register_blueprint(users) 39 | app.register_blueprint(newslet) 40 | app.register_blueprint(main) 41 | 42 | return app 43 | -------------------------------------------------------------------------------- /flask_final/templates/about.html: -------------------------------------------------------------------------------- 1 | {%extends 'dashboard.html'%} 2 | 3 | {%block content%} 4 | 5 |
6 | 7 | 8 |

9 | About this website 10 |

11 |
12 | 13 |

14 | This website is built as the pet project of mine. On the journey to learn backend web-development, I decided to 15 | make something of my own interest to practice my skills. 16 |

17 | 18 |

19 | Khabar-board currently featured extracted summary news from various news portals of Nepal. Similarly, We will 20 | soon incorporate news from top international sites as well. Similarly, handy tools like conversion tools, 21 | weather and other facilities are also on the way. 22 |

23 | 24 |

25 | 26 |

27 | I am also planning to include push notificatiion and newsletters in the future. 28 |

29 | 30 |
31 |
32 | 37 |
38 | 39 |
40 | {%endblock content%} 41 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "khabar-board" 3 | version = "0.5" 4 | description = "" 5 | authors = ["hemanta212 "] 6 | license = "MIT" 7 | 8 | [tool.poetry.dependencies] 9 | python = "^3.6" 10 | bcrypt = "^3.1.4" 11 | beautifulsoup4 = "^4.6.3" 12 | blinker = "^1.4" 13 | bs4 = "^0.0.1" 14 | certifi = "^2020.6.20" 15 | chardet = "^4.0.0" 16 | Click = "^7.0" 17 | Flask = "^1.0.2" 18 | Flask-Bcrypt = "^0.7.1" 19 | Flask-Login = "^0.5.0" 20 | Flask-Mail = "^0.9.1" 21 | Flask-SQLAlchemy = "^2.3.2" 22 | Flask-WTF = "^0.14.2" 23 | idna = "^2.7" 24 | itsdangerous = "^1.1" 25 | Jinja2 = "^2.10" 26 | lxml = "^4.2.5" 27 | MarkupSafe = "^1.1.1" 28 | pycparser = "^2.19" 29 | requests = "^2.21.0" 30 | six = "^1.12.0" 31 | SQLAlchemy = "^1.2.12" 32 | urllib3 = "^1.23" 33 | Werkzeug = "^1.0.0" 34 | WTForms = "^2.2.1" 35 | gunicorn = "^19.9" 36 | flask-script = "^2.0" 37 | flask-migrate = "^2.4" 38 | psycopg2-binary = "^2.8" 39 | feedparser = "^6.0.1" 40 | cffi = "^1.13.0" 41 | email-validator = "^1.1.3" 42 | 43 | [tool.poetry.dev-dependencies] 44 | pylint = "^2.3" 45 | pytest = "^6.0.2" 46 | autopep8 = "^1.4" 47 | rope = "^0.18.0" 48 | ptpython = "^3.0.1" 49 | rich = "^10.3.0" 50 | 51 | 52 | [build-system] 53 | requires = ["poetry>=0.12"] 54 | build-backend = "poetry.masonry.api" 55 | -------------------------------------------------------------------------------- /flask_final/users/models.py: -------------------------------------------------------------------------------- 1 | from flask import current_app 2 | from flask_final import db, login_manager 3 | from flask_login import UserMixin 4 | from itsdangerous import TimedJSONWebSignatureSerializer as Serializer 5 | 6 | 7 | @login_manager.user_loader 8 | def load_user(user_id): 9 | return User.query.get(user_id) 10 | 11 | 12 | class User(db.Model, UserMixin): 13 | id = db.Column(db.Integer, primary_key=True) 14 | full_name = db.Column(db.String(20), nullable=False) 15 | email = db.Column(db.String(30), unique=True, nullable=False) 16 | password = db.Column(db.String(100), nullable=False) 17 | 18 | def get_reset_token(self, expires_sec=1800): 19 | """Gets token for confirming email 20 | 21 | input: 22 | arg1:optional expires sec(default is 1800 ) 23 | output: 24 | a serializer token.""" 25 | s = Serializer(current_app.config["SECRET_KEY"], expires_sec) 26 | return s.dumps({"user_id": self.id}).decode("utf-8") 27 | 28 | @staticmethod 29 | def verify_reset_token(token): 30 | s = Serializer(current_app.config["SECRET_KEY"]) 31 | try: 32 | user_id = s.loads(token)["user_id"] 33 | except: 34 | return None 35 | return User.query.get(user_id) 36 | 37 | def __repr__(self): 38 | return "user({0}, {1})".format(self.full_name, self.email) 39 | -------------------------------------------------------------------------------- /docs/postgres_setup.md: -------------------------------------------------------------------------------- 1 | ## Setup with postgres database 2 | 3 | * [Unix setup](#unix_pg) 4 | * [Windows setup (WIP)](#windows_pg) 5 | 6 | ### Run using postgres database. 7 | To run the project on posgres database you need to install postgresql 10+ in your system 8 | 9 | #### Linux setup: 10 | 1. Install postgres: 11 | 12 | ``` 13 | sudo apt-get install postgresql postgresql-contrib 14 | ``` 15 | 16 | 2. Now create a superuser for PostgreSQL 17 | 18 | ``` 19 | sudo -u postgres createuser --superuser name_of_user 20 | ``` 21 | 22 | 3. And create a database using created user account 23 | 24 | ``` 25 | sudo -u name_of_user createdb name_of_database 26 | ``` 27 | 28 | 4. You can access created database with created user by, 29 | 30 | ``` 31 | psql -U name_of_user -d name_of_database 32 | ``` 33 | 34 | 5. Your postgres database url wil be something like 35 | 36 | ``` 37 | postgresql://localhost/name_of_database 38 | ``` 39 | Or, if you have setup password then, 40 | 41 | ``` 42 | postgresql://username:password@localhost/name_of_database 43 | ``` 44 | 45 | 7. Now take this url and go back to [this section in Main Readme file](../README.md#setting up-the-postgres-databse) 46 | 47 | ``` 48 | #### Windows setup: 49 | Download and install [official site](https://www.postgresql.org/download/windows/) 50 | 51 | 1. Create a postgreql database and obtain its local url 52 | -------------------------------------------------------------------------------- /flask_final/templates/reset_request.html: -------------------------------------------------------------------------------- 1 | {%extends 'layout.html'%} 2 | 3 | {%block content%} 4 |



5 | 6 | 7 | 8 |
9 | {{form.hidden_tag()}} 10 |
11 |

Request reset

12 | 13 | 14 | {%if form.email.errors%} 15 |
16 | 17 | {{form.email(type="email", id="materialFormLoginEmailEx", class="form-control")}} 18 | {{form.email.label(for="materialFormLoginEmailEx")}} 19 | 20 | {%for error in form.email.errors%} 21 | {{error}} 22 |
23 | {%endfor%} 24 | {%else%} 25 |
26 | 27 | {{form.email(type="email", id="materialFormLoginEmailEx", class="form-control")}} 28 | {{form.email.label(for="materialFormLoginEmailEx")}} 29 |
30 | {%endif%} 31 | 32 | 33 |
34 | {{form.submit(class="btn btn-default")}} 35 |
36 | 37 | 44 |
45 |
46 | 47 |
48 | 49 | 50 | {%endblock content%} -------------------------------------------------------------------------------- /flask_final/config.py: -------------------------------------------------------------------------------- 1 | """Configurations for the app itself""" 2 | import os 3 | import sys 4 | import json 5 | 6 | 7 | class Prod: 8 | # basic debuggin properties: 9 | DEBUG = False 10 | TESTING = False 11 | WTF_CSRF_ENABLED = True # security for forms 12 | 13 | # whether to detect changes in project 14 | SQLALCHEMY_TRACK_MODIFICATIONS = False 15 | 16 | SECRET_KEY = os.getenv("SECRET_KEY") 17 | SQLALCHEMY_DATABASE_URI = os.getenv("SQLALCHEMY_DATABASE_URI") or "" 18 | SQLALCHEMY_DATABASE_URI = SQLALCHEMY_DATABASE_URI.replace( 19 | # temporary workaround since sqlalchemy has deprecated postgres:// dialect and 20 | # heroku hasn't updated or allowed users to change the default dialect 21 | "postgres://", 22 | "postgresql://", 23 | 1, 24 | ) 25 | 26 | # Email configs for reseting things you know. 27 | MAIL_SERVER = "smtp.googlemail.com" 28 | MAIL_PORT = 587 29 | MAIL_USE_TLS = True 30 | MAIL_USERNAME = os.getenv("MAIL_USERNAME") 31 | MAIL_PASSWORD = os.getenv("MAIL_PASSWORD") 32 | 33 | 34 | class Debug(Prod): 35 | DEBUG = True 36 | TESTING = True 37 | SQLALCHEMY_TRACK_MODIFICATIONS = (True,) 38 | 39 | 40 | # check if there is a secrets.json file. 41 | class Secrets(Debug): 42 | def __init__(self): 43 | file = "secrets.json" 44 | if not os.path.exists(file): 45 | print(f":: No secrets file {file} found! Exiting..") 46 | sys.exit(1) 47 | 48 | with open(file, "r") as rf: 49 | print(":: Reading secrets.json file (Only Debug mode suppported!)") 50 | configs = json.load(rf) 51 | 52 | self.SECRET_KEY = configs["SECRET_KEY"] 53 | self.MAIL_USERNAME = configs["MAIL_USERNAME"] 54 | self.MAIL_PASSWORD = configs["MAIL_PASSWORD"] 55 | self.SQLALCHEMY_DATABASE_URI = configs["SQLALCHEMY_DATABASE_URI"] 56 | self.NEWS_API_KEY = configs.get("NEWS_API_KEY", "") 57 | -------------------------------------------------------------------------------- /flask_final/templates/reset_password.html: -------------------------------------------------------------------------------- 1 | {%extends 'layout.html'%} 2 | {%block content%} 3 |


4 |
5 | {{form.hidden_tag()}} 6 |
7 |
8 | Reset password 9 |
10 | 11 | {%if form.password.errors %} 12 |
13 | 14 | {{form.password(type="password", id="materialFormLoginPasswordEx", class="form-control")}} 15 | {{form.password.label(for="materialFormLoginPasswordEx")}} 16 | 17 | {%for error in form.password.errors%} 18 |

{{error}}

19 |
20 | {%endfor%} 21 | {%else%} 22 |
23 | 24 | {{form.password(type="password", id="materialFormLoginPasswordEx", class="form-control")}} 25 | {{form.password.label(for="materialFormLoginPasswordEx")}} 26 |
27 | {%endif%} 28 | 29 | {%if form.confirm_password.errors%} 30 |
31 | 32 | {{form.confirm_password(type="password", id="modalLRInput13", class="form-control form-control-sm validate")}} 33 | {{form.confirm_password.label(for="modalLRInput13")}} 34 |
35 | {%for error in form.confirm_password.errors%} 36 | {{error}} 37 | {%endfor%} 38 | {%else%} 39 |
40 | 41 | {{form.confirm_password(type="password", id="modalLRInput13", class="form-control form-control-sm validate")}} 42 | {{form.confirm_password.label(for="modalLRInput13")}} 43 |
44 | {%endif%} 45 |
46 | {{form.submit(class="btn btn-info", type="submit")}} 47 |
48 | 49 | 54 | 55 | 56 |
57 |
58 | {%endblock content%} -------------------------------------------------------------------------------- /flask_final/newslet/rss_news.py: -------------------------------------------------------------------------------- 1 | import feedparser 2 | from bs4 import BeautifulSoup 3 | 4 | 5 | def get_news_from_rss(source, feed_url=None): 6 | url_map = { 7 | "himalayan_times": "http://thehimalayantimes.com/feed/", 8 | "ujyaalo_online": "http://ujyaaloonline.com/rss/", 9 | "hamra_kura": "https://hamrakura.com/rss.xml", 10 | "nepali_times": "https://www.nepalitimes.com/feed/", 11 | } 12 | 13 | print(f"etting source {source}") 14 | feed_url = feed_url if feed_url else url_map[source] 15 | news_list = parse_feed_for_news(feed_url) 16 | print(f"etting newslist {len(news_list)}") 17 | add_source_to_news(news_list, source) 18 | reverse_order = "ujyaalo_online" 19 | if source in reverse_order: 20 | news_list.reverse() 21 | return news_list 22 | 23 | 24 | def parse_feed_for_news(feed_url): 25 | feed = feedparser.parse(feed_url) 26 | entries = feed.entries 27 | news = get_news_from_entries(entries) 28 | return news 29 | 30 | 31 | def add_source_to_news(news_list, source): 32 | for news_dict in news_list: 33 | news_dict["source"] = source 34 | 35 | 36 | def get_news_from_entries(entries): 37 | news_list = [] 38 | for entry in entries: 39 | title = entry.get("title") 40 | if not title: # There must be title else skip 41 | continue 42 | 43 | summary = entry.get("summary") 44 | if summary: 45 | summary = parse_text_from_html(summary) 46 | summary = escape_html_charectars(summary) 47 | 48 | date = entry.get("published") 49 | if date: # Wed, 13 Oct 2019 10:56:09 +00000' 50 | date = date.split(" ") 51 | date = " ".join([date[1], date[2], date[3]]) 52 | 53 | link = entry.get("link") 54 | entry_dict = { 55 | "title": title, 56 | "raw_date": date, 57 | "image_link": "", 58 | "summary": summary, 59 | "news_link": link, 60 | } 61 | news_list.append(entry_dict) 62 | return news_list 63 | 64 | 65 | def escape_html_charectars(text): 66 | soup = BeautifulSoup(text, features="lxml") 67 | text = parse_text_from_html(soup.prettify(formatter="html")) 68 | return text 69 | 70 | 71 | def parse_text_from_html(html_str): 72 | tree = BeautifulSoup(html_str, "lxml") 73 | 74 | body = tree.body 75 | if body is None: 76 | return None 77 | 78 | for tag in body.select("script"): 79 | tag.decompose() 80 | for tag in body.select("style"): 81 | tag.decompose() 82 | 83 | text = body.get_text(separator="\n") 84 | return text 85 | -------------------------------------------------------------------------------- /flask_final/newslet/nagarik_international.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from bs4 import BeautifulSoup as BS 3 | 4 | from flask_final.newslet import parser 5 | 6 | 7 | def setup(): 8 | url = "http://nagariknews.nagariknetwork.com/category/27" 9 | headers = { 10 | "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36\ 11 | (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36" 12 | } 13 | 14 | try: 15 | page = requests.get(url, headers=headers) 16 | except Exception as e: 17 | print("Connection refused by the server..", e) 18 | 19 | response = requests.get(url, headers=headers) 20 | soup = BS(response.content, parser) 21 | return soup 22 | 23 | 24 | def nagarik_international_extractor(): 25 | soup = setup() 26 | 27 | def cover_news(nep_date): 28 | cover_news_list = [] 29 | cover_div = soup.find_all("div", class_="col-sm-3 part-ent") 30 | for news in cover_div: 31 | img = news.div.img["src"] 32 | image = img.replace("media/cache/nagarik_thumbnail_460_300/", "") 33 | image_url = image.replace( 34 | "media/cache/resolve/nagarik_thumbnail_460_300/", "" 35 | ) 36 | title = news.h3.a.text 37 | summary = news.p.text 38 | primary_url = "https://nagariknews.nagariknetwork.com" 39 | news_link = primary_url + news.h3.a["href"] 40 | cover_news_dict = { 41 | "title": title, 42 | "summary": summary, 43 | "source": "Nagarik news", 44 | "summary": summary, 45 | "news_link": news_link, 46 | "image_link": img, 47 | "raw_date": nep_date, 48 | } 49 | cover_news_list.append(cover_news_dict) 50 | return cover_news_list 51 | 52 | news_articles = soup.find_all("div", class_="detail-on") 53 | counter = 0 54 | main_news_list = [] 55 | 56 | for news in news_articles: 57 | title = news.h3.a.text 58 | date, summary = news.p.text, news.find_all("p")[1].text 59 | link = "http://nagariknews.nagariknetwork.com" + news.h3.a["href"] 60 | news_dict = { 61 | "title": title, 62 | "raw_date": date, 63 | "source": "Nagarik news", 64 | "summary": summary, 65 | "news_link": link, 66 | "image_link": None, 67 | } 68 | main_news_list.append(news_dict) 69 | 70 | nep_date = main_news_list[0]["raw_date"] 71 | cover_news_list = cover_news(nep_date) 72 | final_list = cover_news_list + main_news_list 73 | return final_list 74 | 75 | 76 | if __name__ == "__main__": 77 | nagarik_international_extractor() 78 | -------------------------------------------------------------------------------- /flask_final/templates/login.html: -------------------------------------------------------------------------------- 1 | {%extends 'layout.html'%} 2 | 3 | {%block content%} 4 |


5 | 6 | 7 | 8 |
9 | {{form.hidden_tag()}} 10 |
11 |

Log in

12 | 13 | 14 | {%if form.email.errors%} 15 |
16 | 17 | {{form.email(type="email", id="materialFormLoginEmailEx", class="form-control")}} 18 | {{form.email.label(for="materialFormLoginEmailEx")}} 19 | 20 | {%for error in form.email.errors%} 21 | {{error}} 22 |
23 | {%endfor%} 24 | {%else%} 25 |
26 | 27 | {{form.email(type="email", id="materialFormLoginEmailEx", class="form-control")}} 28 | {{form.email.label(for="materialFormLoginEmailEx")}} 29 |
30 | {%endif%} 31 | 32 | {%if form.password.errors %} 33 |
34 | 35 | {{form.password(type="password", id="materialFormLoginPasswordEx", class="form-control")}} 36 | {{form.password.label(for="materialFormLoginPasswordEx")}} 37 | 38 | {%for error in form.password.errors%} 39 |

{{error}}

40 |
41 | {%endfor%} 42 | {%else%} 43 |
44 | 45 | {{form.password(type="password", id="materialFormLoginPasswordEx", class="form-control")}} 46 | {{form.password.label(for="materialFormLoginPasswordEx")}} 47 |
48 | {%endif%} 49 |
50 | {{form.remember(class="form-check-input")}} 51 | {{form.remember.label(class="form-check-label")}} 52 |
53 |
54 | {{form.submit(class="btn btn-default")}} 55 |
56 | 57 | 65 |
66 |
67 | 68 |
69 | 70 | 71 | {%endblock content%} 72 | -------------------------------------------------------------------------------- /flask_final/newslet/kantipur_daily.py: -------------------------------------------------------------------------------- 1 | """ 2 | Script to scrape news from www.kantipurdaily.com/news 3 | Contains: 4 | kantipur_daily_extractor(): Gives list of news dicts 5 | """ 6 | from bs4 import BeautifulSoup as BS 7 | import requests 8 | from datetime import datetime 9 | 10 | try: 11 | from flask_final.newslet import parser 12 | except ImportError: 13 | parser = "lxml" 14 | 15 | 16 | def setup(): 17 | url = "https://www.kantipurdaily.com/news" 18 | headers = { 19 | "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit\ 20 | /537.36(KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36" 21 | } 22 | try: 23 | page = requests.get(url, headers=headers) 24 | except Exception as e: 25 | print("Connection refused by the server..", e) 26 | soup = BS(page.content, parser) 27 | return soup 28 | 29 | 30 | def kantipur_daily_extractor(): 31 | """ 32 | Extracts news from www.kantipurdaily.com/news 33 | Retruns 34 | The order is as given by the website 35 | A list containing news dictionaries. Here is a sample 36 | 37 | { 38 | title: str in nepali 39 | 'raw_date': 2019/06/34 like date, 40 | 'source': 'ekantipur', 41 | 'summary': news summary in nepali, 42 | 'news_link': link, 43 | 'image_link': imglink, 44 | } 45 | 46 | """ 47 | soup = setup() 48 | counter = 0 49 | news_list = [] 50 | for article in soup.find_all("article", class_="normal"): 51 | 52 | title = article.h2.a.text 53 | summary = article.find("p").text 54 | image = article.find("div", class_="image").figure.a.img["data-src"] 55 | img = image.replace("-lowquality", "") 56 | small_img = img.replace("lowquality", "") 57 | big_img = small_img.replace("300x0", "1000x0") 58 | date_ore = article.h2.a["href"] 59 | contaminated_list = date_ore.split("/") 60 | pure_date_list = [ 61 | contaminated_list[2], 62 | contaminated_list[3], 63 | contaminated_list[4], 64 | ] 65 | date = "/".join(pure_date_list) 66 | link = "https://kantipurdaily.com" + date_ore 67 | date = format_date(date) 68 | news_dict = { 69 | "title": title, 70 | "raw_date": date, 71 | "source": "ekantipur", 72 | "summary": summary, 73 | "news_link": link, 74 | "image_link": big_img, 75 | } 76 | news_list.append(news_dict) 77 | counter += 1 78 | 79 | return news_list 80 | 81 | 82 | def format_date(raw_date): 83 | org_format = "%Y/%m/%d" 84 | datetime_obj = datetime.strptime(raw_date, org_format) 85 | dest_format = "%d %b %Y" 86 | date = datetime_obj.strftime(dest_format) 87 | return date 88 | 89 | 90 | if __name__ == "__main__": 91 | kantipur_daily_extractor() 92 | -------------------------------------------------------------------------------- /flask_final/newslet/kantipur_international.py: -------------------------------------------------------------------------------- 1 | """ 2 | Script to scrape news from www.kantipurdaily.com/world 3 | Contains: 4 | kantipur_international_extractor(): Gives list of news dicts 5 | """ 6 | from datetime import datetime 7 | from bs4 import BeautifulSoup as BS 8 | import requests 9 | 10 | try: 11 | from flask_final.newslet import parser 12 | except ImportError: 13 | parser = "lxml" 14 | 15 | 16 | def setup(): 17 | url = "https://www.kantipurdaily.com/world" 18 | headers = { 19 | "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36\ 20 | (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36" 21 | } 22 | 23 | try: 24 | page = requests.get(url, headers=headers) 25 | except Exception as e: 26 | print("Connection refused by the server..", e) 27 | 28 | soup = BS(page.content, parser) 29 | return soup 30 | 31 | 32 | def kantipur_international_extractor(): 33 | """ 34 | Scrapes news from www.kantipurdaily.com/world 35 | Retruns: 36 | A list containing dictionaries of news. list[0] has latest 37 | The format or attributes of dictionary is like this sample 38 | { 39 | 'title': title in nepali, 40 | 'raw_date': date in 2019/02/34 format, 41 | 'source': 'ekantipur', 42 | 'summary': summary in nepali, 43 | 'news_link': link, 44 | 'image_link': high resolution image, 45 | } 46 | """ 47 | soup = setup() 48 | news_list = [] 49 | for article in soup.find_all("article", class_="normal"): 50 | 51 | title = article.h2.a.text 52 | summary = article.find("p").text 53 | image = article.find("div", class_="image").figure.a.img["data-src"] 54 | img = image.replace("-lowquality", "") 55 | small_img = img.replace("lowquality", "") 56 | big_img = small_img.replace("300x0", "1000x0") 57 | date_ore = article.h2.a["href"] 58 | contaminated_list = date_ore.split("/") 59 | pure_date_list = [ 60 | contaminated_list[2], 61 | contaminated_list[3], 62 | contaminated_list[4], 63 | ] 64 | date = "/".join(pure_date_list) 65 | date = format_date(date) 66 | link = "https://kantipurdaily.com" + date_ore 67 | 68 | news_dict = { 69 | "title": title, 70 | "raw_date": date, 71 | "source": "ekantipur", 72 | "summary": summary, 73 | "news_link": link, 74 | "image_link": big_img, 75 | } 76 | news_list.append(news_dict) 77 | 78 | return news_list 79 | 80 | 81 | def format_date(raw_date): 82 | org_format = "%Y/%m/%d" 83 | datetime_obj = datetime.strptime(raw_date, org_format) 84 | dest_format = "%d %b %Y" 85 | date = datetime_obj.strftime(dest_format) 86 | return date 87 | 88 | 89 | if __name__ == "__main__": 90 | kantipur_international_extractor() 91 | -------------------------------------------------------------------------------- /migrations/env.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement 2 | from alembic import context 3 | from sqlalchemy import engine_from_config, pool 4 | from logging.config import fileConfig 5 | import logging 6 | 7 | # this is the Alembic Config object, which provides 8 | # access to the values within the .ini file in use. 9 | config = context.config 10 | 11 | # Interpret the config file for Python logging. 12 | # This line sets up loggers basically. 13 | fileConfig(config.config_file_name) 14 | logger = logging.getLogger("alembic.env") 15 | 16 | # add your model's MetaData object here 17 | # for 'autogenerate' support 18 | # from myapp import mymodel 19 | # target_metadata = mymodel.Base.metadata 20 | from flask import current_app 21 | 22 | config.set_main_option( 23 | "sqlalchemy.url", current_app.config.get("SQLALCHEMY_DATABASE_URI") 24 | ) 25 | target_metadata = current_app.extensions["migrate"].db.metadata 26 | 27 | # other values from the config, defined by the needs of env.py, 28 | # can be acquired: 29 | # my_important_option = config.get_main_option("my_important_option") 30 | # ... etc. 31 | 32 | 33 | def run_migrations_offline(): 34 | """Run migrations in 'offline' mode. 35 | 36 | This configures the context with just a URL 37 | and not an Engine, though an Engine is acceptable 38 | here as well. By skipping the Engine creation 39 | we don't even need a DBAPI to be available. 40 | 41 | Calls to context.execute() here emit the given string to the 42 | script output. 43 | 44 | """ 45 | url = config.get_main_option("sqlalchemy.url") 46 | context.configure(url=url) 47 | 48 | with context.begin_transaction(): 49 | context.run_migrations() 50 | 51 | 52 | def run_migrations_online(): 53 | """Run migrations in 'online' mode. 54 | 55 | In this scenario we need to create an Engine 56 | and associate a connection with the context. 57 | 58 | """ 59 | 60 | # this callback is used to prevent an auto-migration from being generated 61 | # when there are no changes to the schema 62 | # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html 63 | def process_revision_directives(context, revision, directives): 64 | if getattr(config.cmd_opts, "autogenerate", False): 65 | script = directives[0] 66 | if script.upgrade_ops.is_empty(): 67 | directives[:] = [] 68 | logger.info("No changes in schema detected.") 69 | 70 | engine = engine_from_config( 71 | config.get_section(config.config_ini_section), 72 | prefix="sqlalchemy.", 73 | poolclass=pool.NullPool, 74 | ) 75 | 76 | connection = engine.connect() 77 | context.configure( 78 | connection=connection, 79 | target_metadata=target_metadata, 80 | process_revision_directives=process_revision_directives, 81 | **current_app.extensions["migrate"].configure_args 82 | ) 83 | 84 | try: 85 | with context.begin_transaction(): 86 | context.run_migrations() 87 | finally: 88 | connection.close() 89 | 90 | 91 | if context.is_offline_mode(): 92 | run_migrations_offline() 93 | else: 94 | run_migrations_online() 95 | -------------------------------------------------------------------------------- /flask_final/templates/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {% if title %} 8 | {{title}} 9 | {% else %} 10 | Khabar-board 11 | {%endif %} 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 37 | 38 | {% with messages = get_flashed_messages(with_categories = True)%} 39 | {%if messages%} 40 | {%for category,message in messages%} 41 |

42 |
{{message}}
43 | {%endfor%} 44 | {%endif%} 45 | {%endwith%} 46 | {% block content%} 47 | 48 | {%endblock content%} 49 | 50 | 51 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /flask_final/newslet/top_international_news.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import requests 4 | import datetime 5 | 6 | API_KEY = os.getenv("NEWS_API_KEY") 7 | 8 | headers = { 9 | "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1)\ 10 | AppleWebKit/537.36(KHTML, like Gecko)\ 11 | Chrome/39.0.2171.95 Safari/537.36" 12 | } 13 | 14 | sources = [ 15 | "al-jazeera-english", 16 | "bbc-news", 17 | "associated-press", 18 | "cnn", 19 | "the-new-york-times", 20 | "the-times-of-india", 21 | ] 22 | 23 | 24 | def get_general_headlines(api_key, **kwargs): 25 | """ 26 | Returns: A list of news dictionaries 27 | Params: api_key* 28 | Optional_params: language, pageSize, page, apiKey, sources, 29 | q, category, country 30 | """ 31 | 32 | basic_url = ["https://newsapi.org/v2/top-headlines?"] 33 | for param, value in kwargs.items(): 34 | string = "{0}={1}&".format(param, value) 35 | basic_url.append(string) 36 | 37 | api_str = "apiKey={0}".format(api_key) 38 | sources_str = "sources=" + ",".join(sources) + "&" 39 | basic_url.append(sources_str) 40 | basic_url.append(api_str) 41 | url = "".join(basic_url) 42 | try: 43 | response = requests.get(url, headers=headers) 44 | except: 45 | print("No internet Exiting....") 46 | sys.exit(1) 47 | 48 | response_dict = response.json() 49 | total_news = response_dict.get("totalResults") 50 | status = response_dict.get("status", "error") 51 | try: 52 | news_list = response_dict["articles"] 53 | except: 54 | return [] 55 | 56 | refined_news_list = [] 57 | for index, news in enumerate(news_list): 58 | title = news.get("title", "error") 59 | try: 60 | source = news["source"]["name"] 61 | except: 62 | source = "English press" 63 | 64 | news_url = news.get("url", "error") 65 | image_url = news.get("urlToImage", "error") 66 | summary = news.get("description", "Read full news at..") 67 | pub_date = news.get("publishedAt", "error") 68 | # '2019-03-04T08:45:39.4..Z' -> '2019-03-04 08:45:39.45..' 69 | raw_date = pub_date[:10] + " " + pub_date[12:-1] 70 | # remove the . after seconds 71 | raw_date = raw_date.split(".")[0] 72 | date = None 73 | try: 74 | raw_date, date = format_date(raw_date) 75 | except Exception as e: 76 | print(e) 77 | pass 78 | 79 | final_news_dict = { 80 | "title": title, 81 | "raw_date": raw_date, 82 | "date": date, 83 | "source": source, 84 | "summary": summary, 85 | "news_link": news_url, 86 | "image_link": image_url, 87 | } 88 | refined_news_list.append(final_news_dict) 89 | 90 | return refined_news_list 91 | 92 | 93 | def format_date(raw_date): 94 | org_format = "%Y-%m-%d %H:%M:%S" 95 | datetime_obj = datetime.datetime.strptime(raw_date, org_format) 96 | dest_format = "%d %b %Y" 97 | date_str = datetime_obj.strftime(dest_format) 98 | return date_str, datetime_obj 99 | 100 | 101 | if __name__ == "__main__": 102 | get_general_headlines(API_KEY) 103 | -------------------------------------------------------------------------------- /migrations/versions/f682ccb0c931_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: f682ccb0c931 4 | Revises: 5 | Create Date: 2019-04-14 18:56:17.106975 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "f682ccb0c931" 14 | down_revision = None 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.create_table( 22 | "ENG_INTERNATIONAL", 23 | sa.Column("id", sa.Integer(), nullable=False), 24 | sa.Column("source", sa.String(length=20), nullable=False), 25 | sa.Column("raw_date", sa.Text(), nullable=False), 26 | sa.Column("date", sa.DateTime(), nullable=True), 27 | sa.Column("summary", sa.Text(), nullable=True), 28 | sa.Column("title", sa.Text(), nullable=False), 29 | sa.Column("image_link", sa.Text(), nullable=True), 30 | sa.Column("news_link", sa.Text(), nullable=True), 31 | sa.PrimaryKeyConstraint("id"), 32 | ) 33 | op.create_table( 34 | "ENG_NATIONAL", 35 | sa.Column("id", sa.Integer(), nullable=False), 36 | sa.Column("source", sa.String(length=20), nullable=False), 37 | sa.Column("date", sa.DateTime(), nullable=True), 38 | sa.Column("raw_date", sa.Text(), nullable=False), 39 | sa.Column("summary", sa.Text(), nullable=True), 40 | sa.Column("title", sa.Text(), nullable=False), 41 | sa.Column("image_link", sa.Text(), nullable=True), 42 | sa.Column("news_link", sa.Text(), nullable=True), 43 | sa.PrimaryKeyConstraint("id"), 44 | ) 45 | op.create_table( 46 | "NEP_INTERNATIONAL", 47 | sa.Column("id", sa.Integer(), nullable=False), 48 | sa.Column("source", sa.String(length=20), nullable=False), 49 | sa.Column("raw_date", sa.Text(), nullable=False), 50 | sa.Column("date", sa.DateTime(), nullable=True), 51 | sa.Column("summary", sa.Text(), nullable=True), 52 | sa.Column("title", sa.Text(), nullable=False), 53 | sa.Column("image_link", sa.Text(), nullable=True), 54 | sa.Column("news_link", sa.Text(), nullable=True), 55 | sa.PrimaryKeyConstraint("id"), 56 | ) 57 | op.create_table( 58 | "NEP_NATIONAL", 59 | sa.Column("id", sa.Integer(), nullable=False), 60 | sa.Column("source", sa.String(length=20), nullable=False), 61 | sa.Column("date", sa.DateTime(), nullable=True), 62 | sa.Column("raw_date", sa.Text(), nullable=False), 63 | sa.Column("summary", sa.Text(), nullable=True), 64 | sa.Column("title", sa.Text(), nullable=False), 65 | sa.Column("image_link", sa.Text(), nullable=True), 66 | sa.Column("news_link", sa.Text(), nullable=True), 67 | sa.PrimaryKeyConstraint("id"), 68 | ) 69 | op.create_table( 70 | "user", 71 | sa.Column("id", sa.Integer(), nullable=False), 72 | sa.Column("full_name", sa.String(length=20), nullable=False), 73 | sa.Column("email", sa.String(length=30), nullable=False), 74 | sa.Column("password", sa.String(length=100), nullable=False), 75 | sa.PrimaryKeyConstraint("id"), 76 | sa.UniqueConstraint("email"), 77 | ) 78 | # ### end Alembic commands ### 79 | 80 | 81 | def downgrade(): 82 | # ### commands auto generated by Alembic - please adjust! ### 83 | op.drop_table("user") 84 | op.drop_table("NEP_NATIONAL") 85 | op.drop_table("NEP_INTERNATIONAL") 86 | op.drop_table("ENG_NATIONAL") 87 | op.drop_table("ENG_INTERNATIONAL") 88 | # ### end Alembic commands ### 89 | -------------------------------------------------------------------------------- /flask_final/newslet/kathmandupost.py: -------------------------------------------------------------------------------- 1 | """ 2 | Script to scrape news from kathmandupost.ekantipur.com 3 | Contains: 4 | kathmandu_post_extractor(): Gives list of news dicts 5 | """ 6 | 7 | from datetime import datetime 8 | from bs4 import BeautifulSoup as BS 9 | import requests 10 | 11 | try: 12 | from flask_final.newslet import parser 13 | except ImportError: 14 | parser = "lxml" 15 | 16 | URL = "https://kathmandupost.ekantipur.com" 17 | 18 | 19 | def setup(): 20 | HEADERS = { 21 | "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36\ 22 | (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36" 23 | } 24 | 25 | try: 26 | PAGE = requests.get(URL, headers=HEADERS) 27 | except Exception as e: 28 | print("Connection refused by the server..", e) 29 | 30 | soup = BS(PAGE.content, parser) 31 | return soup 32 | 33 | 34 | def format_date(raw_date): 35 | org_format = "%Y/%m/%d" 36 | datetime_obj = datetime.strptime(raw_date, org_format) 37 | dest_format = "%d %b %Y" 38 | date = datetime_obj.strftime(dest_format) 39 | return date 40 | 41 | 42 | def kathmandu_post_extractor(): 43 | """Extracts the news from https://kathmandupost.ekantipur.com 44 | with the same order as that of the website 45 | Retruns: 46 | A list containing dictionaries of news list[0] has latest 47 | example of such dictionary is 48 | { 49 | "image_link": image_link, 50 | "title": title in englist, 51 | "raw_date": date in 23 Mar 2018 format, 52 | "source": "ekantipur", 53 | "news_link": full_link, 54 | "summary": summary, 55 | } 56 | 57 | """ 58 | soup = setup() 59 | news_list = [] 60 | column_one = soup.find("div", class_="grid-first") 61 | column_two = soup.find("div", class_="grid-second") 62 | column_three = soup.find("div", class_="grid-third") 63 | latest_column = soup.find("div", class_="block--morenews") 64 | sources = [column_one, column_two, column_three, latest_column] 65 | 66 | for column in sources: 67 | articles = column.find_all("article") 68 | 69 | if column == column_two: 70 | featured_article = articles[0] 71 | h3_tag = soup.new_tag("h3") 72 | featured_article.h2.wrap(h3_tag) 73 | featured_article.h3.h2.unwrap() 74 | 75 | elif column == latest_column: 76 | for article in articles: 77 | a_tag = article.a 78 | a_tag.string = article.h3.string 79 | article.h3.insert(0, a_tag) 80 | 81 | for article in articles: 82 | href_link = article.h3.a["href"] 83 | article_link = URL + href_link 84 | title = article.h3.a.text 85 | 86 | raw_date = "/".join(href_link.split("/")[2:5]) 87 | date = format_date(raw_date) 88 | 89 | image_tag = article.find("img") 90 | if image_tag: 91 | image_link = image_tag["data-src"] 92 | else: 93 | image_link = None 94 | 95 | summary = article.p.text 96 | 97 | news_dict = { 98 | "title": title, 99 | "source": "ekantipur", 100 | "news_link": article_link, 101 | "raw_date": date, 102 | "summary": summary, 103 | "image_link": image_link, 104 | } 105 | 106 | news_list.append(news_dict) 107 | 108 | return news_list 109 | 110 | 111 | if __name__ == "__main__": 112 | kathmandu_post_extractor() 113 | -------------------------------------------------------------------------------- /flask_final/templates/detail_news.html: -------------------------------------------------------------------------------- 1 | {% extends "dashboard.html" %} 2 | {% block content %} 3 | 4 |
5 | Categories 6 | 25 |
26 | 27 | 28 |
29 |
30 |
31 |

{{ heading }}

32 |
33 |
34 | 35 | 36 | {% for news in news_list.items %} 37 | 38 |
39 |
40 | 41 |

{{news.title}}

42 |
43 | 44 | {%if news.image_link %} 45 | 46 | {%endif%} 47 | 48 | 49 |
50 |
51 | {{ news.raw_date }} 52 | | {{news.source}} 53 |

54 | 55 |
56 | {{news.summary}} 57 |
58 | 59 | {{ read_more }} 60 |
61 |
62 | {% endfor %} 63 | 64 | 65 | 66 | 67 | {%for page_num in news_list.iter_pages(left_edge=1, right_edge=1, left_current=1, right_current=2)%} 68 | {%if page_num%} 69 | {%if news_list.page == page_num%} 70 | 71 | 72 | {{page_num}} 73 | {%else%} 74 | {{page_num}} 75 | {%endif%} 76 | {%else%} 77 | ... 78 | 79 | {%endif%} 80 | {%endfor%} 81 | 82 |
83 | 84 | 109 | {% endblock content %} 110 | -------------------------------------------------------------------------------- /flask_final/users/forms.py: -------------------------------------------------------------------------------- 1 | # -*-Code UTF-8 2 | """ 3 | forms for user registering 4 | contains 4 classes: 5 | SignupForm 6 | LoginForm 7 | RequestResetForm 8 | PasswordResetForm 9 | """ 10 | 11 | from flask import flash 12 | from flask_wtf import FlaskForm 13 | from wtforms import StringField, PasswordField, SubmitField, BooleanField 14 | from wtforms.validators import DataRequired, Length, Email, EqualTo, ValidationError 15 | from flask_final.users.models import User 16 | 17 | 18 | class SignupForm(FlaskForm): 19 | """ 20 | Form for signing up the user 21 | Attributes: 22 | full_name [str]: full name of user 23 | email [str]: email of the user 24 | password [password]: secret password 25 | confirm_password [password]: confirmed password 26 | submit [submit]: submit field 27 | Methods: 28 | validate_email(): checks if email is already registered 29 | """ 30 | 31 | full_name = StringField( 32 | "Full Name", validators=[DataRequired(), Length(max=20, min=3)] 33 | ) 34 | email = StringField("Email-address", validators=[DataRequired(), Email()]) 35 | password = PasswordField("password", validators=[DataRequired(), Length(min=3)]) 36 | confirm_password = PasswordField( 37 | "confirm password", validators=[DataRequired(), EqualTo("password")] 38 | ) 39 | submit = SubmitField("Sign UP") 40 | 41 | def validate_email(self, email): 42 | """ 43 | Checks if email is already registered and raises a 44 | ValidationError and flashes a warning if so 45 | """ 46 | 47 | duplicate_user = User.query.filter_by(email=email.data).first() 48 | if duplicate_user: 49 | flash("Email already registered. try another", "warning") 50 | raise ValidationError("Email already registered. try another") 51 | 52 | 53 | class LoginForm(FlaskForm): 54 | """ 55 | Form for logging in the user 56 | Attributes: 57 | email [str]: email of the user 58 | password [password]: secret password 59 | remember [bool]: checkbox for whether to remember sessions 60 | submit [submit]: submit field 61 | """ 62 | 63 | email = StringField("Email-address", validators=[DataRequired(), Email()]) 64 | password = PasswordField("password", validators=[DataRequired(), Length(min=3)]) 65 | remember = BooleanField("Remember me") 66 | submit = SubmitField("Log in") 67 | 68 | 69 | class RequestResetForm(FlaskForm): 70 | """ 71 | Form for the user to request his passowrd reset 72 | Attributes: 73 | email [str]: email of the user 74 | submit [submit]: submit field 75 | Methods: 76 | validate_email(): checks if email is registered 77 | """ 78 | 79 | email = StringField("Email-address", validators=[DataRequired(), Email()]) 80 | submit = SubmitField("Request Password Reset") 81 | 82 | def validate_email(self, email): 83 | """ 84 | Checks for the given email in database 85 | raises ValidationError and flashes waring if not found 86 | """ 87 | 88 | registered_user = User.query.filter_by(email=email.data).first() 89 | if registered_user is None: 90 | flash("Email not registered. try another", "warning") 91 | raise ValidationError("Email not registered. try another") 92 | 93 | 94 | class PasswordResetForm(FlaskForm): 95 | """ 96 | Form for reseting the user's password 97 | Attributes: 98 | password [password]: secret password 99 | confirm_password [password]: confirmed password 100 | submit [submit]: submit field 101 | """ 102 | 103 | password = PasswordField("password", validators=[DataRequired(), Length(min=3)]) 104 | confirm_password = PasswordField( 105 | "confirm password", validators=[DataRequired(), EqualTo("password")] 106 | ) 107 | submit = SubmitField("Reset") 108 | -------------------------------------------------------------------------------- /flask_final/templates/home.html: -------------------------------------------------------------------------------- 1 | {%extends 'layout.html' %} 2 | 3 | {%block content%} 4 | 5 | 6 | 7 |
8 |
9 |

Khabar Board
Get informed, Get smart

10 |
11 |
It's Free and always will be!
12 | 13 |
Try without Signup?
14 | 15 |
16 |
17 |
18 | 19 | 20 | 21 | 27 |
28 | 29 | 30 |
31 | 32 | 33 |
34 |
35 | 36 | 37 |

News That Matters.

38 |
39 |

Latest, important and summarized national and international news right on your finger Tips.

40 |
41 | 42 |
43 |
44 | 45 |
46 | 47 | 48 | 49 |
50 | 51 | 52 |
53 |
54 | 55 |

Diverse and Trust worthy sources.

56 |
57 |

We bring you news from reputed and established national and international news agencies. Our default news sources comprise BBC, CNN, The himalayan times, and Kantipur.

58 |
59 | 60 |
61 |
62 | 63 |
64 | 65 | 66 | 67 |
68 | 69 | 70 |
71 |
72 | 73 |

Customize your dashboard.

74 |
75 |

Pick your own topics, sources to filter your interest. Get live score updates of your favorite sports. Get updates about film releases, shows. So what are you waiting for?

76 |
77 | 78 |
79 |
80 | 81 |
82 | 83 | 84 | 85 | 86 |
87 | 88 | 89 |
90 |
91 | 92 |

Wide range of Handy tools

93 |
94 |

We bring you small handy tools like weather reports, conversion tools, calendar etc.

95 |
96 | 97 |
98 |
99 | 100 |
101 | 102 | 103 | 104 |
105 | 106 | 107 | 108 | 109 | {%endblock%} 110 | -------------------------------------------------------------------------------- /flask_final/newslet/models.py: -------------------------------------------------------------------------------- 1 | """ 2 | Database models for storing news 3 | Contains 3 classes: 4 | NepNationalNews: national news in nepali database model 5 | NepInternationalNews : international news in nepali database model 6 | EngNationalNews :National news in english database model 7 | """ 8 | from datetime import datetime 9 | from flask_final import db 10 | 11 | 12 | class NepNationalNews(db.Model): 13 | """ 14 | Provides date(model created), id atrribute by default 15 | Initialize with compulsory 16 | source : news source like 'ekantipur' 17 | raw_date: date provided by the website itself 18 | summary : news summary 19 | title : title of the news 20 | Optional initialization 21 | image_link 22 | news_link 23 | """ 24 | 25 | __tablename__ = "NEP_NATIONAL" 26 | id = db.Column(db.Integer, primary_key=True) 27 | source = db.Column(db.String(20), nullable=False) 28 | date = db.Column(db.DateTime, nullable=True, default=datetime.utcnow) 29 | raw_date = db.Column(db.Text, nullable=False) 30 | summary = db.Column(db.Text) 31 | title = db.Column(db.Text, nullable=False) 32 | image_link = db.Column(db.Text) 33 | news_link = db.Column(db.Text) 34 | 35 | def __repr__(self): 36 | return "news({0}, {1}, {2})".format(self.id, self.source, self.date) 37 | 38 | 39 | class NepInternationalNews(db.Model): 40 | """ 41 | Provides date(model created), id atrribute by default 42 | Initialize with compulsory 43 | source : news source like 'ekantipur' 44 | raw_date: date provided by the website itself 45 | summary : news summary 46 | title : title of the news 47 | Optional initialization 48 | image_link 49 | news_link 50 | """ 51 | 52 | __tablename__ = "NEP_INTERNATIONAL" 53 | id = db.Column(db.Integer, primary_key=True) 54 | source = db.Column(db.String(20), nullable=False) 55 | raw_date = db.Column(db.Text, nullable=False) 56 | date = db.Column(db.DateTime, nullable=True, default=datetime.utcnow) 57 | summary = db.Column(db.Text) 58 | title = db.Column(db.Text, nullable=False) 59 | image_link = db.Column(db.Text) 60 | news_link = db.Column(db.Text) 61 | 62 | def __repr__(self): 63 | return "news({0}, {1}, {2})".format(self.id, self.source, self.date) 64 | 65 | 66 | class EngNationalNews(db.Model): 67 | """ 68 | Provides date(model created), id atrribute by default 69 | Initialize with compulsory 70 | source : news source like 'ekantipur' 71 | raw_date: date provided by the website itself 72 | summary : news summary 73 | title : title of the news 74 | Optional initialization 75 | image_link 76 | news_link 77 | """ 78 | 79 | __tablename__ = "ENG_NATIONAL" 80 | id = db.Column(db.Integer, primary_key=True) 81 | source = db.Column(db.String(20), nullable=False) 82 | date = db.Column(db.DateTime, nullable=True, default=datetime.utcnow) 83 | raw_date = db.Column(db.Text, nullable=False) 84 | summary = db.Column(db.Text) 85 | title = db.Column(db.Text, nullable=False) 86 | image_link = db.Column(db.Text) 87 | news_link = db.Column(db.Text) 88 | 89 | def __repr__(self): 90 | return "news({0}, {1}, {2})".format(self.id, self.source, self.date) 91 | 92 | 93 | class EngInternationalNews(db.Model): 94 | """ 95 | Provides date(model created), id atrribute by default 96 | Initialize with compulsory 97 | source : news source like 'the-washington-post' 98 | raw_date: date provided by the website itself 99 | summary : news summary 100 | title : title of the news 101 | Optional initialization 102 | image_link 103 | news_link 104 | """ 105 | 106 | __tablename__ = "ENG_INTERNATIONAL" 107 | id = db.Column(db.Integer, primary_key=True) 108 | source = db.Column(db.String(20), nullable=False) 109 | raw_date = db.Column(db.Text, nullable=False) 110 | date = db.Column(db.DateTime, nullable=True, default=datetime.utcnow) 111 | summary = db.Column(db.Text) 112 | title = db.Column(db.Text, nullable=False) 113 | image_link = db.Column(db.Text) 114 | news_link = db.Column(db.Text) 115 | 116 | def __repr__(self): 117 | return "news({0}, {1}, {2})".format(self.id, self.source, self.date) 118 | -------------------------------------------------------------------------------- /flask_final/users/routes.py: -------------------------------------------------------------------------------- 1 | """ 2 | contains resource for managing users functionality. 3 | """ 4 | from flask import Blueprint, render_template, url_for, redirect, flash 5 | from flask_login import login_user, logout_user, current_user, login_required 6 | from flask_mail import Message 7 | from flask_final import bcrypt, db, mail 8 | from flask_final.users.forms import ( 9 | SignupForm, 10 | LoginForm, 11 | RequestResetForm, 12 | PasswordResetForm, 13 | ) 14 | from flask_final.users.models import User 15 | 16 | 17 | users = Blueprint("users", __name__) 18 | 19 | 20 | @users.route("/signup", methods=["GET", "POST"]) 21 | def signup(): 22 | if current_user.is_authenticated: 23 | return redirect(url_for("newslet.nep_national_news")) 24 | form = SignupForm() 25 | if form.validate_on_submit(): 26 | flash("Your account is created", "success") 27 | hashed_password = bcrypt.generate_password_hash(form.password.data).decode( 28 | "utf-8" 29 | ) 30 | user = User( 31 | full_name=form.full_name.data, 32 | email=form.email.data, 33 | password=hashed_password, 34 | ) 35 | 36 | db.session.add(user) 37 | db.session.commit() 38 | return redirect(url_for("users.login")) 39 | 40 | return render_template("signup.html", title="Sign Up", form=form) 41 | 42 | 43 | @users.route("/login", methods=["GET", "POST"]) 44 | def login(): 45 | if current_user.is_authenticated: 46 | return redirect(url_for("newslet.nep_national_news")) 47 | form = LoginForm() 48 | if form.validate_on_submit(): 49 | user = User.query.filter_by(email=form.email.data).first() 50 | if user and bcrypt.check_password_hash(user.password, form.password.data): 51 | allowed_emails = ["try@try.com"] 52 | if user.email in allowed_emails: 53 | login_user(user, remember=form.remember.data) 54 | return redirect(url_for("newslet.nep_national_news")) 55 | else: 56 | flash("Invalid email or password. Try again!", "info ") 57 | return redirect(url_for("users.login")) 58 | return render_template("login.html", title="Log in", form=form) 59 | 60 | 61 | @users.route("/logout", methods=["GET", "POST"]) 62 | @login_required 63 | def logout(): 64 | logout_user() 65 | return redirect(url_for("newslet.nep_national_news")) 66 | 67 | 68 | @users.route("/password/reset", methods=["GET", "POST"]) 69 | def reset_request(): 70 | form = RequestResetForm() 71 | if form.email.data == "try@try.com": 72 | flash("Invalid request. This is public try account", "warning") 73 | 74 | elif form.validate_on_submit(): 75 | user = User.query.filter_by(email=form.email.data).first() 76 | send_reset_mail(user) 77 | flash("Email with reset link is sent. Check your email!", "success") 78 | if current_user.is_authenticated: 79 | return redirect(url_for("users.reset_request")) 80 | else: 81 | return redirect(url_for("users.login")) 82 | 83 | return render_template("reset_request.html", titile="Reset password", form=form) 84 | 85 | 86 | @users.route("/password/reset/", methods=["GET", "POST"]) 87 | def reset_token(token): 88 | if current_user.is_authenticated: 89 | flash("Logout and change your password.", "info") 90 | return redirect(url_for("newslet.nep_national_news")) 91 | 92 | user = User.verify_reset_token(token) 93 | if user is None: 94 | flash("Invalid or expired token!", "warning") 95 | return redirect(url_for("users.reset_request")) 96 | form = PasswordResetForm() 97 | if form.validate_on_submit(): 98 | flash("Your password has been succesfully updated! Login now. ", "success") 99 | hashed_password = bcrypt.generate_password_hash(form.password.data).decode( 100 | "utf-8" 101 | ) 102 | user.password = hashed_password 103 | db.session.commit() 104 | return redirect(url_for("users.login")) 105 | 106 | return render_template("reset_password.html", title="Password reset", form=form) 107 | 108 | 109 | def send_reset_mail(user): 110 | token = user.get_reset_token() 111 | msg = Message( 112 | "Reset Password for Khabar Board", 113 | sender="noreply@demo.com", 114 | recipients=[user.email], 115 | ) 116 | msg.html = render_template( 117 | "reset_password_email.html", 118 | user=user, 119 | action_url=url_for("users.reset_token", token=token, _external=True), 120 | ) 121 | mail.send(msg) 122 | -------------------------------------------------------------------------------- /flask_final/templates/privacy_policy.html: -------------------------------------------------------------------------------- 1 | {% extends "dashboard.html" %} 2 | {% block content %} 3 | 4 |
5 | 6 |

Privacy Policy of Khabarboard

7 |

Khabarboard operates the https://kbd.herokuapp.com website, which provides the SERVICE.

8 |

This page is used to inform website visitors regarding our policies with the collection, use, and disclosure of Personal Information if anyone decided to use our Service, the khabarboard website.

9 |

If you choose to use our Service, then you agree to the collection and use of information in relation with this policy. The Personal Information that we collect are used for providing and improving the Service. We will not use or share your information with anyone except as described in this Privacy Policy.

10 |

The terms used in this Privacy Policy have the same meanings as in our Terms and Conditions, which is accessible at https://kbd.herokuapp.com, unless otherwise defined in this Privacy Policy. 11 |

Information Collection and Use

12 |

For a better experience while using our Service, we may require you to provide us with certain personally identifiable information, including but not limited to your name, phone number, and postal address. The information that we collect will be used to contact or identify you.

13 |

Log Data

14 |

We want to inform you that whenever you visit our Service, we collect information that your browser sends to us that is called Log Data. This Log Data may include information such as your computer’s Internet Protocol ("IP") address, browser version, pages of our Service that you visit, the time and date of your visit, the time spent on those pages, and other statistics.

15 |

Cookies

16 |

Cookies are files with small amount of data that is commonly used an anonymous unique identifier. These are sent to your browser from the website that you visit and are stored on your computer’s hard drive.

17 |

Our website uses these "cookies" to collection information and to improve our Service. You have the option to either accept or refuse these cookies, and know when a cookie is being sent to your computer. If you choose to refuse our cookies, you may not be able to use some portions of our Service.

18 |

Service Providers

19 |

We may employ third-party companies and individuals due to the following reasons:

20 |
    21 |
  • To facilitate our Service;
  • 22 |
  • To provide the Service on our behalf;
  • 23 |
  • To perform Service-related services; or
  • 24 |
  • To assist us in analyzing how our Service is used.
  • 25 |
26 |

We want to inform our Service users that these third parties have access to your Personal Information. The reason is to perform the tasks assigned to them on our behalf. However, they are obligated not to disclose or use the information for any other purpose.

27 |

Security

28 |

We value your trust in providing us your Personal Information, thus we are striving to use commercially acceptable means of protecting it. But remember that no method of transmission over the internet, or method of electronic storage is 100% secure and reliable, and we cannot guarantee its absolute security.

29 |

Links to Other Sites

30 |

Our Service may contain links to other sites. If you click on a third-party link, you will be directed to that site. Note that these external sites are not operated by us. Therefore, we strongly advise you to review the Privacy Policy of these websites. We have no control over, and assume no responsibility for the content, privacy policies, or practices of any third-party sites or services.

31 |

Children’s Privacy

32 |

Our Services do not address anyone under the age of 13. We do not knowingly collect personal identifiable information from children under 13. In the case we discover that a child under 13 has provided us with personal information, we immediately delete this from our servers. If you are a parent or guardian and you are aware that your child has provided us with personal information, please contact us so that we will be able to do necessary actions.

33 |

Changes to This Privacy Policy

34 |

We may update our Privacy Policy from time to time. Thus, we advise you to review this page periodically for any changes. We will notify you of any changes by posting the new Privacy Policy on this page. These changes are effective immediately, after they are posted on this page.

35 |

Contact Us

36 |

If you have any questions or suggestions about our Privacy Policy, do not hesitate to contact us.

37 | 38 |
39 | {% endblock content %} 40 | -------------------------------------------------------------------------------- /flask_final/templates/signup.html: -------------------------------------------------------------------------------- 1 | {%extends 'layout.html'%} 2 | 3 | {%block content%} 4 |


5 | 6 | 7 | 8 | 9 |
10 | {{form.hidden_tag()}} 11 |
12 |

Sign Up

13 | 14 | 15 | {%if form.full_name.errors%} 16 | 97 | 98 | 105 |
106 |
107 | 108 | {%endblock content%} -------------------------------------------------------------------------------- /flask_final/newslet/news_loader.py: -------------------------------------------------------------------------------- 1 | # -*=Code = UTF-8 2 | """ 3 | Utils module to get news from extractor and register them with an order 4 | to the database models. 5 | Contains: 6 | load_news_to_models(): collects and registers news of specified category 7 | """ 8 | 9 | # Try catch block to make orffline development possble 10 | try: 11 | from flask_final.newslet.kantipur_international import ( 12 | kantipur_international_extractor, 13 | ) 14 | from flask_final.newslet.kantipur_daily import kantipur_daily_extractor 15 | from flask_final.newslet.kathmandupost import kathmandu_post_extractor 16 | from flask_final.newslet.nagarik_international import ( 17 | nagarik_international_extractor, 18 | ) 19 | from flask_final.newslet.top_international_news import get_general_headlines 20 | from flask_final.newslet.rss_news import get_news_from_rss 21 | 22 | # Incase scrapers cannot be imported (networks or some reasons) 23 | except Exception as E: 24 | print(E) 25 | 26 | def load_news_to_models(category): 27 | """ 28 | Have an ineffective loader function 29 | just to prevent import errros 30 | """ 31 | pass 32 | 33 | # Script terminates from here 34 | 35 | # This else block is only runned if try block is succesful 36 | else: 37 | from flask_final.newslet.models import NepNationalNews as NNN 38 | from flask_final.newslet.models import NepInternationalNews as NIN 39 | from flask_final.newslet.models import EngNationalNews as ENN 40 | from flask_final.newslet.models import EngInternationalNews as EIN 41 | from flask_final import db, NEWS_API_KEY 42 | 43 | def load_news_to_models(category): 44 | """ 45 | Get news from scraper and registers to database model of 46 | associated given category 47 | Param: category (either of ['NIN','NNN','ENN']) 48 | Returns: Nothing, Once you call this funtion with specific 49 | category, you can use the updated models directly 50 | """ 51 | models = {"NNN": NNN, "NIN": NIN, "ENN": ENN, "EIN": EIN} 52 | 53 | scrapers = { 54 | "NNN": (kantipur_daily_extractor,), 55 | "NIN": (kantipur_international_extractor, nagarik_international_extractor), 56 | "ENN": (kathmandu_post_extractor,), 57 | } 58 | 59 | extractors = {"EIN": ((get_general_headlines, NEWS_API_KEY, dict()),)} 60 | 61 | news_list = [] 62 | if category in scrapers: 63 | for extractor in scrapers.get(category): 64 | news_list += extractor() 65 | if category == "ENN": 66 | for news in news_list: 67 | pass 68 | 69 | else: 70 | for func_info in extractors.get(category): 71 | func, API_KEY, kwargs = func_info 72 | news_list = func(API_KEY, **kwargs) 73 | 74 | category_rss_map = { 75 | "NNN": ("ujyaalo_online", "hamra_kura"), 76 | "ENN": ("himalayan_times", "nepali_times"), 77 | } 78 | if category in category_rss_map: 79 | sources = category_rss_map[category] 80 | for source in sources: 81 | news_list += get_news_from_rss(source) 82 | print("NEWSLIST IS ", len(news_list)) 83 | 84 | i = 0 85 | for news in reversed(news_list): 86 | # In scraped_news_list index 0 is latest one. This for loop 87 | # iterates in opposite direction so that 88 | # index 0 (latest news) is registered at last so that 89 | # it has newest date and the last item is registerd at first 90 | # so it gets oldest date assigned by db model 91 | duplicate = ( 92 | models[category].query.filter_by(news_link=news["news_link"]).first() 93 | ) 94 | 95 | if not duplicate: 96 | news_post = models[category]( 97 | title=news["title"], 98 | source=news["source"], 99 | summary=news["summary"], 100 | image_link=news["image_link"], 101 | news_link=news["news_link"], 102 | raw_date=news["raw_date"], 103 | date=news.get("date"), 104 | ) 105 | db.session.add(news_post) 106 | db.session.commit() 107 | i += 1 108 | 109 | order = models[category].date.desc() 110 | print("Unique news", i) 111 | 112 | # this for loop picks iterates over latest news list 113 | # and then preserves first 60 items and deletes all others 114 | for i in models[category].query.order_by(order)[60:]: 115 | db.session.delete(i) 116 | db.session.commit() 117 | -------------------------------------------------------------------------------- /flask_final/newslet/routes.py: -------------------------------------------------------------------------------- 1 | # -*-code; UTF_8 2 | """ 3 | Contains routes for managing news. 4 | news(): Mix collech 3 samples 5 | Detail news routes 6 | nep_national_news() 7 | nep_international_news() 8 | eng_national_news() 9 | eng_international_news() 10 | """ 11 | 12 | from flask import Blueprint 13 | from flask import request, render_template 14 | from flask_login import login_required, current_user 15 | from flask_final.newslet.models import NepNationalNews as NNN 16 | from flask_final.newslet.models import NepInternationalNews as NIN 17 | from flask_final.newslet.models import EngNationalNews as ENN 18 | from flask_final.newslet.models import EngInternationalNews as EIN 19 | from flask_final.newslet.news_loader import load_news_to_models 20 | 21 | newslet = Blueprint("newslet", __name__) 22 | 23 | 24 | @newslet.route("/dashboard/news", methods=["GET"]) 25 | @newslet.route("/dashboard/news/nep/national", methods=["GET"]) 26 | def nep_national_news(): 27 | """ 28 | Save extracted news from scraper to db model 29 | then passes to detail_news.html template to render it 30 | """ 31 | 32 | load_news_to_models("NNN") 33 | page = request.args.get("page", 1, type=int) 34 | free_sources = ("ujyaalo_online", "hamra_kura") 35 | news = None 36 | logged = current_user.is_authenticated 37 | if not logged: 38 | for source in free_sources: 39 | if not news: 40 | news = NNN.query.filter_by(source=source) 41 | else: 42 | news = news.union(NNN.query.filter_by(source=source)) 43 | 44 | else: 45 | news = NNN.query.order_by(NNN.date.desc()) 46 | 47 | news = news.order_by(NNN.date.desc()) 48 | news_list = news.paginate(page=page, per_page=15) 49 | return render_template( 50 | "detail_news.html", 51 | title="National-Nep", 52 | news_list=news_list, 53 | heading="National News [नेपा]", 54 | newslet_func="newslet.nep_national_news", 55 | read_more="|थप पढ्नुहोस >>|", 56 | logged=logged, 57 | ) 58 | 59 | 60 | @newslet.route("/dashboard/news/nep/international", methods=["GET"]) 61 | @login_required 62 | def nep_international_news(): 63 | """ 64 | Save extracted news from scraper to db model 65 | then passes to detail_news.html template to render it 66 | """ 67 | 68 | load_news_to_models("NIN") 69 | page = request.args.get("page", 1, type=int) 70 | news_list = NIN.query.order_by(NIN.date.desc()).paginate(page=page, per_page=15) 71 | return render_template( 72 | "detail_news.html", 73 | title="International-Nep", 74 | news_list=news_list, 75 | heading="International News [नेपा]", 76 | newslet_func="newslet.nep_international_news", 77 | read_more="|थप पढ्नुहोस >>|", 78 | logged=True, 79 | ) 80 | 81 | 82 | @newslet.route("/dashboard/news/eng/national", methods=["GET"]) 83 | def eng_national_news(): 84 | """ 85 | Save extracted news from scraper to db model 86 | then passes to detail_news.html template to render it 87 | """ 88 | load_news_to_models("ENN") 89 | page = request.args.get("page", 1, type=int) 90 | free_sources = ("himalayan_times", "nepali_times") 91 | news = None 92 | logged = current_user.is_authenticated 93 | if not logged: 94 | for source in free_sources: 95 | if not news: 96 | news = ENN.query.filter_by(source=source) 97 | else: 98 | news = news.union(ENN.query.filter_by(source=source)) 99 | 100 | else: 101 | news = ENN.query.order_by(ENN.date.desc()) 102 | 103 | news = news.order_by(ENN.date.desc()) 104 | news_list = news.paginate(page=page, per_page=15) 105 | return render_template( 106 | "detail_news.html", 107 | title="National-Eng", 108 | news_list=news_list, 109 | heading="National News [Eng]", 110 | newslet_func="newslet.eng_national_news", 111 | read_more="|Read More>>|", 112 | logged=logged, 113 | ) 114 | 115 | 116 | @newslet.route("/dashboard/news/eng/international", methods=["GET"]) 117 | def eng_international_news(): 118 | """ 119 | Save extracted news from scraper to db model 120 | then passes to detail_news.html template to render it 121 | """ 122 | load_news_to_models("EIN") 123 | load_news_to_models("NNN") 124 | load_news_to_models("ENN") 125 | page = request.args.get("page", 1, type=int) 126 | news_list = EIN.query.order_by(EIN.date.desc()).paginate(page=page, per_page=15) 127 | logged = current_user.is_authenticated 128 | return render_template( 129 | "detail_news.html", 130 | title="International-Eng", 131 | news_list=news_list, 132 | heading="International News [Eng]", 133 | newslet_func="newslet.eng_international_news", 134 | read_more="|Read More>>|", 135 | logged=logged, 136 | ) 137 | 138 | 139 | @newslet.route("/update", methods=["GET"]) 140 | def update(): 141 | categories = ("EIN", "NIN", "ENN") 142 | for category in categories: 143 | try: 144 | load_news_to_models(category) 145 | except Exception as E: 146 | print(E) 147 | 148 | return "

Updated all news sources

" 149 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Khabar-board 2 | Khabar-board is an online webapp that scrapes news from diffrent new portals of Nepal and worldwide. 3 | 4 | Currently, the news is Scraped for National nepali, 5 | International nepali, National english and International english sections from Kantipur, Nagarik, Ujyaalo and Kathmandu Post, himalayan times, nepal times and top headlines from International sources like bbc, cnn, new york times, etc 6 | 7 | Features: 8 | 9 | * Login, Logout, Remember login through cookies 10 | * Reset and confirm email address 11 | * National and International news in both nepali and english 12 | * News collection(scraping, api) and serving 13 | * Pagination, Collapsible sidebar, Responsive navigation 14 | * Use proper structure with flask blueprinting 15 | * Hosted Live at heroku @ kbd.herokuapp.com 16 | 17 | Todos: 18 | 19 | - [x] Integrate various international news apis(top, general, sports, tech...) 20 | - [x] Rewrite manage.py and database management 21 | - [x] Experiment with docker deploys 22 | - [ ] Add logging 23 | - [ ] Add tests 24 | - [ ] Extract the scraper to separate news API 25 | - [ ] Redesign the dashboard 26 | - [ ] Remove compulsory login and establish per person db record of preferences 27 | - [ ] Give user choice to customize news topic, sources, language etc 28 | - [ ] News search on keyword for custom category (like bitcoin, trump, etc) 29 | - [ ] Integrate social media login (facebook, github) 30 | 31 | ## Run with Docker 32 | 33 | ```sh 34 | docker build -t kbd . 35 | docker run -it --rm --net=host kbd 36 | ``` 37 | 38 | ## Installation and Usage 39 | 40 | - Install [python 3.6 or above](https://python.org/downloads) 41 | - Clone/download this repository and navigate to this repo through cmd 42 | 43 | ```sh 44 | $ git clone https://github.com/hemanta212/nepali-news-portal-kbd 45 | ``` 46 | 47 | - Install either [poetry](https://github.com/python-poetry/poetry) or follow [this guide](/docs/venv.md) for setting up virtual environment. 48 | - Installing dependencies using poetry 49 | ```sh 50 | $ poetry install 51 | ``` 52 | - Installing dependencies using pip. 53 | ```sh 54 | $ python -m pip install -r requirements.txt 55 | ``` 56 | 57 | ### Setting up Database 58 | You can setup any database supported [here](https://docs.sqlalchemy.org/en/13/core/engines.html#supported-databases). This doc covers setting up sqlite and postgres db. 59 | 60 | ##### Setting up SQLite 61 | Populate the following as your environment variables 62 | 63 | ``` 64 | SECRET_KEY=" 109 | Gunicorn is a production WSGI server that is essential for running flask project in production environments like with Procfile in [Heroku](https://heroku.com). 110 | 111 | After installing, you just provide the application instance of project to gunicorn. 112 | 113 | eg. 114 | ``` 115 | gunicorn run:app // prod env 116 | 117 | gunicorn run:app debug 118 | ``` 119 | 120 | #### *NOTE*: 121 | Gunicorn is not supported in windows operating system. However, you can run it using [WSL](https://google.com/search?query=windows%20subsystem%20for%20linux). 122 | 123 | ## Accessing all news 124 | After successfully running the app, go to localhost:5000/signup and sign up with an account with this admin provisioned email 'try@try.com' (yes this exact email only) and login. You should see news from all the sources. 125 | -------------------------------------------------------------------------------- /flask_final/templates/reset_password_email.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Set up a new password for Khabar Board 8 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | Use this link to reset your password. The link is only valid for 24 hours. 21 | 22 | 23 | 98 | 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /flask_final/static/dashboard/dashboard.css: -------------------------------------------------------------------------------- 1 | @import "https://fonts.googleapis.com/css?family=Poppins:300,400,500,600,700"; 2 | 3 | body { 4 | font-family: 'Poppins', sans-serif; 5 | background: #fafafa; 6 | } 7 | 8 | p { 9 | font-family: 'Poppins', sans-serif; 10 | font-size: 1.1em; 11 | font-weight: 300; 12 | line-height: 1.7em; 13 | color: #999; 14 | } 15 | 16 | a, 17 | a:hover, 18 | a:focus { 19 | color: inherit; 20 | text-decoration: none; 21 | transition: all 0.3s; 22 | } 23 | 24 | .navbar { 25 | padding: 15px 10px; 26 | background: #fff; 27 | border: none; 28 | border-radius: 0; 29 | margin-bottom: 40px; 30 | box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.1); 31 | } 32 | 33 | .navbar-btn { 34 | box-shadow: none; 35 | outline: none !important; 36 | border: none; 37 | } 38 | 39 | .line { 40 | width: 100%; 41 | height: 1px; 42 | border-bottom: 1px dashed #ddd; 43 | margin: 40px 0; 44 | } 45 | 46 | /* --------------------------------------------------- 47 | SIDEBAR STYLE 48 | ----------------------------------------------------- */ 49 | 50 | /* Icon size */ 51 | img.img-responsive.mCS_img_loaded { 52 | width: 30%; 53 | } 54 | 55 | #sidebar { 56 | width: 250px; 57 | position: fixed; 58 | top: 0; 59 | left: -250px; 60 | height: 100vh; 61 | z-index: 999; 62 | background: #7386D5; 63 | color: #fff; 64 | transition: all 0.3s; 65 | overflow-y: scroll; 66 | box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.2); 67 | } 68 | 69 | #sidebar.active { 70 | left: 0; 71 | } 72 | 73 | #dismiss { 74 | width: 35px; 75 | height: 35px; 76 | line-height: 35px; 77 | text-align: center; 78 | background: #7386D5; 79 | position: absolute; 80 | top: 10px; 81 | right: 10px; 82 | cursor: pointer; 83 | -webkit-transition: all 0.3s; 84 | -o-transition: all 0.3s; 85 | transition: all 0.3s; 86 | } 87 | 88 | #dismiss:hover { 89 | background: #fff; 90 | color: #7386D5; 91 | } 92 | 93 | .overlay { 94 | position: fixed; 95 | width: 100vw; 96 | height: 100vh; 97 | background: rgba(0, 0, 0, 0.7); 98 | z-index: 998; 99 | display: none; 100 | } 101 | 102 | #sidebar .sidebar-header { 103 | padding: 20px; 104 | background: #6d7fcc; 105 | } 106 | 107 | #sidebar ul.components { 108 | padding: 20px 0; 109 | border-bottom: 1px solid #47748b; 110 | } 111 | 112 | #sidebar ul p { 113 | color: #fff; 114 | padding: 10px; 115 | } 116 | 117 | #sidebar ul li a { 118 | padding: 10px; 119 | font-size: 1.1em; 120 | display: block; 121 | } 122 | 123 | #sidebar ul li a:hover { 124 | color: #7386D5; 125 | background: #fff; 126 | } 127 | 128 | #sidebar ul li.active>a, 129 | a[aria-expanded="true"] { 130 | color: #fff; 131 | background: #6d7fcc; 132 | } 133 | 134 | 135 | a[data-toggle="collapse"] { 136 | position: relative; 137 | } 138 | 139 | .kb-icon { 140 | position: center; 141 | margin-left: ; 142 | } 143 | 144 | a[aria-expanded="false"]::before, 145 | a[aria-expanded="true"]::before { 146 | content: '\e259'; 147 | display: block; 148 | position: absolute; 149 | right: 20px; 150 | font-family: 'Glyphicons Halflings'; 151 | font-size: 0.6em; 152 | } 153 | 154 | a[aria-expanded="true"]::before { 155 | content: '\e260'; 156 | } 157 | 158 | 159 | ul ul a { 160 | font-size: 0.9em !important; 161 | padding-left: 30px !important; 162 | background: #6d7fcc; 163 | } 164 | 165 | ul.CTAs { 166 | padding: 20px; 167 | } 168 | 169 | ul.CTAs a { 170 | text-align: center; 171 | font-size: 0.9em !important; 172 | display: block; 173 | border-radius: 5px; 174 | margin-bottom: 5px; 175 | } 176 | 177 | a.download { 178 | background: #fff; 179 | color: #7386D5; 180 | } 181 | 182 | a.article, 183 | a.article:hover { 184 | background: #6d7fcc !important; 185 | color: #fff !important; 186 | } 187 | 188 | 189 | /* --------------------------------------------------- 190 | CONTENT STYLE 191 | ----------------------------------------------------- */ 192 | #content { 193 | width: 100%; 194 | margin:0 auto; 195 | padding: 20px; 196 | min-height: 100vh; 197 | transition: all 0.3s; 198 | position: absolute; 199 | top: 0; 200 | right: 0; 201 | } 202 | 203 | 204 | /* --------------------------------------------------- 205 | NEWS STYLE 206 | ----------------------------------------------------- */ 207 | 208 | .news_menu { 209 | width: 23%; 210 | float: left; 211 | background:#f5f5f5; 212 | margin-top: 10px; 213 | margin-bottom: 10px; 214 | text-align: center; 215 | } 216 | 217 | .sticky { 218 | position: fixed; 219 | top: 0; 220 | } 221 | 222 | .news_menu span{ 223 | padding:5px; 224 | font: 20px/1.5 Arial, Helvetica, sans-serif; 225 | color: black; 226 | } 227 | 228 | .news_menu .current, 229 | #sidebar .current { 230 | color:#e8491d; 231 | font-weight:bold; 232 | } 233 | .news_menu ul{ 234 | margin-left: 0em; 235 | list-style: none; 236 | padding-inline-start: 0px; 237 | } 238 | .news_menu ul li{ 239 | padding: 0.3em 0 0.3em 0; 240 | text-align: center; 241 | } 242 | .news_menu ul li a{ 243 | text-decoration: none; 244 | color: black; 245 | font: 15px/1.5 Arial, Helvetica, sans-serif; 246 | border: 0px solid #f4f4; 247 | } 248 | .news_menu ul li a:hover{ 249 | font-weight: bold; 250 | color:grey; 251 | } 252 | 253 | .news{ 254 | width:75%; 255 | float: right; 256 | margin: 0 auto; 257 | } 258 | 259 | @media (min-width: 600px){ 260 | 261 | .news .load_sideway{ 262 | float: right; 263 | width: 30%; 264 | padding-left: 5px; 265 | } 266 | } 267 | 268 | @media (max-width: 768px){ 269 | #content, 270 | div.news, 271 | div.news_menu { 272 | float: none; 273 | width: 100%; 274 | padding:2px; 275 | } 276 | div.news_menu { 277 | width: 60%; 278 | margin:0 auto; 279 | } 280 | } 281 | 282 | @media (max-width: 500px){ 283 | .dark_switch { 284 | margin-top: -14%; 285 | width: 60px; 286 | } 287 | } 288 | 289 | @media screen and (max-width: 768px) and (min-width: 650px) { 290 | .dark_switch { 291 | margin-top: -6%; 292 | width: 60px; 293 | } 294 | 295 | #dark_label { 296 | white-space: nowrap; 297 | margin-top: 5px; 298 | } 299 | } 300 | 301 | @media screen and (max-width: 650px) and (min-width: 500px) { 302 | .dark_switch { 303 | margin-top: -9%; 304 | width: 60px; 305 | } 306 | } 307 | 308 | .panel-heading { 309 | color: #333; 310 | background-color: #ffffff; 311 | } 312 | 313 | .panel-heading { 314 | padding: 10px 15px; 315 | border-color: white; 316 | border: 0px solid white; 317 | } 318 | .panel-default { 319 | border-color: white; 320 | border: 0px solid white; 321 | } 322 | 323 | 324 | /* ///////////////////////////DARK_MODE_SWITCH/////////////////////////////////// */ 325 | 326 | .dark_switch { 327 | position: relative; 328 | float: right; 329 | display: inline-block; 330 | height: 34px; 331 | } 332 | 333 | 334 | #dark_label { 335 | white-space: nowrap; 336 | margin-top: 8px; 337 | } 338 | 339 | /* Hide default HTML checkbox */ 340 | .dark_switch input { 341 | opacity: 0; 342 | width: 0; 343 | height: 0; 344 | } 345 | 346 | /* The dark_slider */ 347 | .dark_slider { 348 | position: absolute; 349 | cursor: pointer; 350 | top: 0; 351 | left: 0; 352 | right: 0; 353 | bottom: 0; 354 | background-color: #ccc; 355 | -webkit-transition: .4s; 356 | transition: .4s; 357 | } 358 | 359 | .dark_slider:before { 360 | position: absolute; 361 | content: ""; 362 | height: 26px; 363 | width: 26px; 364 | left: 4px; 365 | bottom: 4px; 366 | background-color: white; 367 | -webkit-transition: .4s; 368 | transition: .4s; 369 | } 370 | 371 | input:checked + .dark_slider { 372 | background-color: #2196F3; 373 | } 374 | 375 | input:focus + .dark_slider { 376 | box-shadow: 0 0 1px #2196F3; 377 | } 378 | 379 | input:checked + .dark_slider:before { 380 | -webkit-transform: translateX(26px); 381 | -ms-transform: translateX(26px); 382 | transform: translateX(26px); 383 | } 384 | 385 | /* Rounded dark_sliders */ 386 | .dark_slider.round { 387 | border-radius: 34px; 388 | } 389 | 390 | .dark_slider.round:before { 391 | border-radius: 50%; 392 | } 393 | 394 | -------------------------------------------------------------------------------- /flask_final/templates/dashboard.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | {% if title %} 10 | {{title}} 11 | {% else %} 12 | Dashboard | Khabar-board 13 | {%endif %} 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 | 31 | 81 |
82 | 83 | 84 |
85 | 107 | 108 | 109 | 110 | 111 | {%block content%} 112 | {% with messages = get_flashed_messages(with_categories = True)%} 113 | {%if messages%} 114 | {%for category,message in messages%} 115 |

116 |
{{message}}
117 | {%endfor%} 118 | {%endif%} 119 | {%endwith%} 120 | 121 | {%endblock content%} 122 |
123 | 124 |
125 | 126 | 127 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 152 | 153 | 173 | 174 | 210 | 211 | 212 | 213 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | alembic==1.6.5; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" \ 2 | --hash=sha256:e78be5b919f5bb184e3e0e2dd1ca986f2362e29a2bc933c446fe89f39dbe4e9c \ 3 | --hash=sha256:a21fedebb3fb8f6bbbba51a11114f08c78709377051384c9c5ead5705ee93a51 4 | bcrypt==3.2.0; python_version >= "3.6" \ 5 | --hash=sha256:c95d4cbebffafcdd28bd28bb4e25b31c50f6da605c81ffd9ad8a3d1b2ab7b1b6 \ 6 | --hash=sha256:63d4e3ff96188e5898779b6057878fecf3f11cfe6ec3b313ea09955d587ec7a7 \ 7 | --hash=sha256:cd1ea2ff3038509ea95f687256c46b79f5fc382ad0aa3664d200047546d511d1 \ 8 | --hash=sha256:cdcdcb3972027f83fe24a48b1e90ea4b584d35f1cc279d76de6fc4b13376239d \ 9 | --hash=sha256:a67fb841b35c28a59cebed05fbd3e80eea26e6d75851f0574a9273c80f3e9b55 \ 10 | --hash=sha256:81fec756feff5b6818ea7ab031205e1d323d8943d237303baca2c5f9c7846f34 \ 11 | --hash=sha256:5b93c1726e50a93a033c36e5ca7fdcd29a5c7395af50a6892f5d9e7c6cfbfb29 12 | beautifulsoup4==4.9.3 \ 13 | --hash=sha256:4c98143716ef1cb40bf7f39a8e3eec8f8b009509e74904ba3a7b315431577e35 \ 14 | --hash=sha256:fff47e031e34ec82bf17e00da8f592fe7de69aeea38be00523c04623c04fb666 \ 15 | --hash=sha256:84729e322ad1d5b4d25f805bfa05b902dd96450f43842c4e99067d5e1369eb25 16 | blinker==1.4 \ 17 | --hash=sha256:471aee25f3992bd325afa3772f1063dbdbbca947a041b8b89466dc00d606f8b6 18 | bs4==0.0.1 \ 19 | --hash=sha256:36ecea1fd7cc5c0c6e4a1ff075df26d50da647b75376626cc186e2212886dd3a 20 | certifi==2020.12.5 \ 21 | --hash=sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830 \ 22 | --hash=sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c 23 | cffi==1.14.5 \ 24 | --hash=sha256:bb89f306e5da99f4d922728ddcd6f7fcebb3241fc40edebcb7284d7514741991 \ 25 | --hash=sha256:34eff4b97f3d982fb93e2831e6750127d1355a923ebaeeb565407b3d2f8d41a1 \ 26 | --hash=sha256:99cd03ae7988a93dd00bcd9d0b75e1f6c426063d6f03d2f90b89e29b25b82dfa \ 27 | --hash=sha256:65fa59693c62cf06e45ddbb822165394a288edce9e276647f0046e1ec26920f3 \ 28 | --hash=sha256:51182f8927c5af975fece87b1b369f722c570fe169f9880764b1ee3bca8347b5 \ 29 | --hash=sha256:43e0b9d9e2c9e5d152946b9c5fe062c151614b262fda2e7b201204de0b99e482 \ 30 | --hash=sha256:cbde590d4faaa07c72bf979734738f328d239913ba3e043b1e98fe9a39f8b2b6 \ 31 | --hash=sha256:5de7970188bb46b7bf9858eb6890aad302577a5f6f75091fd7cdd3ef13ef3045 \ 32 | --hash=sha256:a465da611f6fa124963b91bf432d960a555563efe4ed1cc403ba5077b15370aa \ 33 | --hash=sha256:d42b11d692e11b6634f7613ad8df5d6d5f8875f5d48939520d351007b3c13406 \ 34 | --hash=sha256:72d8d3ef52c208ee1c7b2e341f7d71c6fd3157138abf1a95166e6165dd5d4369 \ 35 | --hash=sha256:29314480e958fd8aab22e4a58b355b629c59bf5f2ac2492b61e3dc06d8c7a315 \ 36 | --hash=sha256:3d3dd4c9e559eb172ecf00a2a7517e97d1e96de2a5e610bd9b68cea3925b4892 \ 37 | --hash=sha256:48e1c69bbacfc3d932221851b39d49e81567a4d4aac3b21258d9c24578280058 \ 38 | --hash=sha256:69e395c24fc60aad6bb4fa7e583698ea6cc684648e1ffb7fe85e3c1ca131a7d5 \ 39 | --hash=sha256:9e93e79c2551ff263400e1e4be085a1210e12073a31c2011dbbda14bda0c6132 \ 40 | --hash=sha256:24ec4ff2c5c0c8f9c6b87d5bb53555bf267e1e6f70e52e5a9740d32861d36b6f \ 41 | --hash=sha256:3c3f39fa737542161d8b0d680df2ec249334cd70a8f420f71c9304bd83c3cbed \ 42 | --hash=sha256:681d07b0d1e3c462dd15585ef5e33cb021321588bebd910124ef4f4fb71aef55 \ 43 | --hash=sha256:58e3f59d583d413809d60779492342801d6e82fefb89c86a38e040c16883be53 \ 44 | --hash=sha256:005a36f41773e148deac64b08f233873a4d0c18b053d37da83f6af4d9087b813 \ 45 | --hash=sha256:2894f2df484ff56d717bead0a5c2abb6b9d2bf26d6960c4604d5c48bbc30ee73 \ 46 | --hash=sha256:0857f0ae312d855239a55c81ef453ee8fd24136eaba8e87a2eceba644c0d4c06 \ 47 | --hash=sha256:cd2868886d547469123fadc46eac7ea5253ea7fcb139f12e1dfc2bbd406427d1 \ 48 | --hash=sha256:35f27e6eb43380fa080dccf676dece30bef72e4a67617ffda586641cd4508d49 \ 49 | --hash=sha256:06d7cd1abac2ffd92e65c0609661866709b4b2d82dd15f611e602b9b188b0b69 \ 50 | --hash=sha256:0f861a89e0043afec2a51fd177a567005847973be86f709bbb044d7f42fc4e05 \ 51 | --hash=sha256:cc5a8e069b9ebfa22e26d0e6b97d6f9781302fe7f4f2b8776c3e1daea35f1adc \ 52 | --hash=sha256:9ff227395193126d82e60319a673a037d5de84633f11279e336f9c0f189ecc62 \ 53 | --hash=sha256:9cf8022fb8d07a97c178b02327b284521c7708d7c71a9c9c355c178ac4bbd3d4 \ 54 | --hash=sha256:8b198cec6c72df5289c05b05b8b0969819783f9418e0409865dac47288d2a053 \ 55 | --hash=sha256:ad17025d226ee5beec591b52800c11680fca3df50b8b29fe51d882576e039ee0 \ 56 | --hash=sha256:6c97d7350133666fbb5cf4abdc1178c812cb205dc6f41d174a7b0f18fb93337e \ 57 | --hash=sha256:8ae6299f6c68de06f136f1f9e69458eae58f1dacf10af5c17353eae03aa0d827 \ 58 | --hash=sha256:04c468b622ed31d408fea2346bec5bbffba2cc44226302a0de1ade9f5ea3d373 \ 59 | --hash=sha256:06db6321b7a68b2bd6df96d08a5adadc1fa0e8f419226e25b2a5fbf6ccc7350f \ 60 | --hash=sha256:293e7ea41280cb28c6fcaaa0b1aa1f533b8ce060b9e701d78511e1e6c4a1de76 \ 61 | --hash=sha256:b85eb46a81787c50650f2392b9b4ef23e1f126313b9e0e9013b35c15e4288e2e \ 62 | --hash=sha256:1f436816fc868b098b0d63b8920de7d208c90a67212546d02f84fe78a9c26396 \ 63 | --hash=sha256:1071534bbbf8cbb31b498d5d9db0f274f2f7a865adca4ae429e147ba40f73dea \ 64 | --hash=sha256:9de2e279153a443c656f2defd67769e6d1e4163952b3c622dcea5b08a6405322 \ 65 | --hash=sha256:6e4714cc64f474e4d6e37cfff31a814b509a35cb17de4fb1999907575684479c \ 66 | --hash=sha256:158d0d15119b4b7ff6b926536763dc0714313aa59e320ddf787502c70c4d4bee \ 67 | --hash=sha256:1bf1ac1984eaa7675ca8d5745a8cb87ef7abecb5592178406e55858d411eadc0 \ 68 | --hash=sha256:df5052c5d867c1ea0b311fb7c3cd28b19df469c056f7fdcfe88c7473aa63e333 \ 69 | --hash=sha256:24a570cd11895b60829e941f2613a4f79df1a27344cbbb82164ef2e0116f09c7 \ 70 | --hash=sha256:afb29c1ba2e5a3736f1c301d9d0abe3ec8b86957d04ddfa9d7a6a42b9367e396 \ 71 | --hash=sha256:f2d45f97ab6bb54753eab54fffe75aaf3de4ff2341c9daee1987ee1837636f1d \ 72 | --hash=sha256:fd78e5fee591709f32ef6edb9a015b4aa1a5022598e36227500c8f4e02328d9c 73 | chardet==4.0.0; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") \ 74 | --hash=sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5 \ 75 | --hash=sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa 76 | click==7.1.2; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") \ 77 | --hash=sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc \ 78 | --hash=sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a 79 | dnspython==2.1.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" \ 80 | --hash=sha256:95d12f6ef0317118d2a1a6fc49aac65ffec7eb8087474158f42f26a639135216 \ 81 | --hash=sha256:e4a87f0b573201a0f3727fa18a516b055fd1107e0e5477cded4a2de497df1dd4 82 | email-validator==1.1.3; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") \ 83 | --hash=sha256:5675c8ceb7106a37e40e2698a57c056756bf3f272cfa8682a4f87ebd95d8440b \ 84 | --hash=sha256:aa237a65f6f4da067119b7df3f13e89c25c051327b2b5b66dc075f33d62480d7 85 | feedparser==6.0.6; python_version >= "3.6" \ 86 | --hash=sha256:1c35e9ef43d8f95959cf8cfa337b68a2cb0888cab7cd982868d23850bb1e08ae \ 87 | --hash=sha256:78f62a5b872fdef451502bb96e64a8fd4180535eb749954f1ad528604809cdeb 88 | flask-bcrypt==0.7.1 \ 89 | --hash=sha256:d71c8585b2ee1c62024392ebdbc447438564e2c8c02b4e57b56a4cafd8d13c5f 90 | flask-login==0.5.0 \ 91 | --hash=sha256:6d33aef15b5bcead780acc339464aae8a6e28f13c90d8b1cf9de8b549d1c0b4b \ 92 | --hash=sha256:7451b5001e17837ba58945aead261ba425fdf7b4f0448777e597ddab39f4fba0 93 | flask-mail==0.9.1 \ 94 | --hash=sha256:22e5eb9a940bf407bcf30410ecc3708f3c56cc44b29c34e1726fe85006935f41 95 | flask-migrate==2.7.0 \ 96 | --hash=sha256:ae2f05671588762dd83a21d8b18c51fe355e86783e24594995ff8d7380dffe38 \ 97 | --hash=sha256:26871836a4e46d2d590cf8e558c6d60039e1c003079b240689d845726b6b57c0 98 | flask-script==2.0.6 \ 99 | --hash=sha256:6425963d91054cfcc185807141c7314a9c5ad46325911bd24dcb489bd0161c65 100 | flask-sqlalchemy==2.5.1; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.4.0") \ 101 | --hash=sha256:2bda44b43e7cacb15d4e05ff3cc1f8bc97936cc464623424102bfc2c35e95912 \ 102 | --hash=sha256:f12c3d4cc5cc7fdcc148b9527ea05671718c3ea45d50c7e732cceb33f574b390 103 | flask-wtf==0.14.3 \ 104 | --hash=sha256:d417e3a0008b5ba583da1763e4db0f55a1269d9dd91dcc3eb3c026d3c5dbd720 \ 105 | --hash=sha256:57b3faf6fe5d6168bda0c36b0df1d05770f8e205e18332d0376ddb954d17aef2 106 | flask==1.1.4; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") \ 107 | --hash=sha256:c34f04500f2cbbea882b1acb02002ad6fe6b7ffa64a6164577995657f50aed22 \ 108 | --hash=sha256:0fbeb6180d383a9186d0d6ed954e0042ad9f18e0e8de088b2b419d526927d196 109 | greenlet==1.1.0; python_version >= "3" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3" \ 110 | --hash=sha256:60848099b76467ef09b62b0f4512e7e6f0a2c977357a036de602b653667f5f4c \ 111 | --hash=sha256:f42ad188466d946f1b3afc0a9e1a266ac8926461ee0786c06baac6bd71f8a6f3 \ 112 | --hash=sha256:76ed710b4e953fc31c663b079d317c18f40235ba2e3d55f70ff80794f7b57922 \ 113 | --hash=sha256:b33b51ab057f8a20b497ffafdb1e79256db0c03ef4f5e3d52e7497200e11f821 \ 114 | --hash=sha256:ed1377feed808c9c1139bdb6a61bcbf030c236dd288d6fca71ac26906ab03ba6 \ 115 | --hash=sha256:da862b8f7de577bc421323714f63276acb2f759ab8c5e33335509f0b89e06b8f \ 116 | --hash=sha256:5f75e7f237428755d00e7460239a2482fa7e3970db56c8935bd60da3f0733e56 \ 117 | --hash=sha256:258f9612aba0d06785143ee1cbf2d7361801c95489c0bd10c69d163ec5254a16 \ 118 | --hash=sha256:5d928e2e3c3906e0a29b43dc26d9b3d6e36921eee276786c4e7ad9ff5665c78a \ 119 | --hash=sha256:cc407b68e0a874e7ece60f6639df46309376882152345508be94da608cc0b831 \ 120 | --hash=sha256:0c557c809eeee215b87e8a7cbfb2d783fb5598a78342c29ade561440abae7d22 \ 121 | --hash=sha256:3d13da093d44dee7535b91049e44dd2b5540c2a0e15df168404d3dd2626e0ec5 \ 122 | --hash=sha256:b3090631fecdf7e983d183d0fad7ea72cfb12fa9212461a9b708ff7907ffff47 \ 123 | --hash=sha256:06ecb43b04480e6bafc45cb1b4b67c785e183ce12c079473359e04a709333b08 \ 124 | --hash=sha256:944fbdd540712d5377a8795c840a97ff71e7f3221d3fddc98769a15a87b36131 \ 125 | --hash=sha256:c767458511a59f6f597bfb0032a1c82a52c29ae228c2c0a6865cfeaeaac4c5f5 \ 126 | --hash=sha256:2325123ff3a8ecc10ca76f062445efef13b6cf5a23389e2df3c02a4a527b89bc \ 127 | --hash=sha256:598bcfd841e0b1d88e32e6a5ea48348a2c726461b05ff057c1b8692be9443c6e \ 128 | --hash=sha256:be9768e56f92d1d7cd94185bab5856f3c5589a50d221c166cc2ad5eb134bd1dc \ 129 | --hash=sha256:dfe7eac0d253915116ed0cd160a15a88981a1d194c1ef151e862a5c7d2f853d3 \ 130 | --hash=sha256:9a6b035aa2c5fcf3dbbf0e3a8a5bc75286fc2d4e6f9cfa738788b433ec894919 \ 131 | --hash=sha256:ca1c4a569232c063615f9e70ff9a1e2fee8c66a6fb5caf0f5e8b21a396deec3e \ 132 | --hash=sha256:3096286a6072553b5dbd5efbefc22297e9d06a05ac14ba017233fedaed7584a8 \ 133 | --hash=sha256:c35872b2916ab5a240d52a94314c963476c989814ba9b519bc842e5b61b464bb \ 134 | --hash=sha256:b97c9a144bbeec7039cca44df117efcbeed7209543f5695201cacf05ba3b5857 \ 135 | --hash=sha256:16183fa53bc1a037c38d75fdc59d6208181fa28024a12a7f64bb0884434c91ea \ 136 | --hash=sha256:6b1d08f2e7f2048d77343279c4d4faa7aef168b3e36039cba1917fffb781a8ed \ 137 | --hash=sha256:14927b15c953f8f2d2a8dffa224aa78d7759ef95284d4c39e1745cf36e8cdd2c \ 138 | --hash=sha256:9bdcff4b9051fb1aa4bba4fceff6a5f770c6be436408efd99b76fc827f2a9319 \ 139 | --hash=sha256:c70c7dd733a4c56838d1f1781e769081a25fade879510c5b5f0df76956abfa05 \ 140 | --hash=sha256:0de64d419b1cb1bfd4ea544bedea4b535ef3ae1e150b0f2609da14bbf48a4a5f \ 141 | --hash=sha256:8833e27949ea32d27f7e96930fa29404dd4f2feb13cce483daf52e8842ec246a \ 142 | --hash=sha256:c1580087ab493c6b43e66f2bdd165d9e3c1e86ef83f6c2c44a29f2869d2c5bd5 \ 143 | --hash=sha256:ad80bb338cf9f8129c049837a42a43451fc7c8b57ad56f8e6d32e7697b115505 \ 144 | --hash=sha256:a9017ff5fc2522e45562882ff481128631bf35da444775bc2776ac5c61d8bcae \ 145 | --hash=sha256:7920e3eccd26b7f4c661b746002f5ec5f0928076bd738d38d894bb359ce51927 \ 146 | --hash=sha256:408071b64e52192869129a205e5b463abda36eff0cebb19d6e63369440e4dc99 \ 147 | --hash=sha256:be13a18cec649ebaab835dff269e914679ef329204704869f2f167b2c163a9da \ 148 | --hash=sha256:22002259e5b7828b05600a762579fa2f8b33373ad95a0ee57b4d6109d0e589ad \ 149 | --hash=sha256:206295d270f702bc27dbdbd7651e8ebe42d319139e0d90217b2074309a200da8 \ 150 | --hash=sha256:096cb0217d1505826ba3d723e8981096f2622cde1eb91af9ed89a17c10aa1f3e \ 151 | --hash=sha256:03f28a5ea20201e70ab70518d151116ce939b412961c33827519ce620957d44c \ 152 | --hash=sha256:7db68f15486d412b8e2cfcd584bf3b3a000911d25779d081cbbae76d71bd1a7e \ 153 | --hash=sha256:70bd1bb271e9429e2793902dfd194b653221904a07cbf207c3139e2672d17959 \ 154 | --hash=sha256:f92731609d6625e1cc26ff5757db4d32b6b810d2a3363b0ff94ff573e5901f6f \ 155 | --hash=sha256:06d7ac89e6094a0a8f8dc46aa61898e9e1aec79b0f8b47b2400dd51a44dbc832 \ 156 | --hash=sha256:adb94a28225005890d4cf73648b5131e885c7b4b17bc762779f061844aabcc11 \ 157 | --hash=sha256:aa4230234d02e6f32f189fd40b59d5a968fe77e80f59c9c933384fe8ba535535 \ 158 | --hash=sha256:c87df8ae3f01ffb4483c796fe1b15232ce2b219f0b18126948616224d3f658ee 159 | gunicorn==19.10.0; (python_version >= "2.6" and python_full_version < "3.0.0") or (python_full_version >= "3.2.0") \ 160 | --hash=sha256:c3930fe8de6778ab5ea716cab432ae6335fa9f03b3f2c3e02529214c476f4bcb \ 161 | --hash=sha256:f9de24e358b841567063629cd0a656b26792a41e23a24d0dcb40224fc3940081 162 | idna==2.10; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.4.0") \ 163 | --hash=sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0 \ 164 | --hash=sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6 165 | importlib-metadata==4.5.0; python_version >= "3.6" and python_full_version < "3.0.0" and python_version < "3.8" or python_full_version >= "3.6.0" and python_version < "3.8" and python_version >= "3.6" \ 166 | --hash=sha256:833b26fb89d5de469b24a390e9df088d4e52e4ba33b01dc5e0e4f41b81a16c00 \ 167 | --hash=sha256:b142cc1dd1342f31ff04bb7d022492b09920cb64fed867cd3ea6f80fe3ebd139 168 | itsdangerous==1.1.0; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.4.0") \ 169 | --hash=sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749 \ 170 | --hash=sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19 171 | jinja2==2.11.3; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") \ 172 | --hash=sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419 \ 173 | --hash=sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6 174 | lxml==4.6.3; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") \ 175 | --hash=sha256:df7c53783a46febb0e70f6b05df2ba104610f2fb0d27023409734a3ecbb78fb2 \ 176 | --hash=sha256:1b7584d421d254ab86d4f0b13ec662a9014397678a7c4265a02a6d7c2b18a75f \ 177 | --hash=sha256:079f3ae844f38982d156efce585bc540c16a926d4436712cf4baee0cce487a3d \ 178 | --hash=sha256:bc4313cbeb0e7a416a488d72f9680fffffc645f8a838bd2193809881c67dd106 \ 179 | --hash=sha256:8157dadbb09a34a6bd95a50690595e1fa0af1a99445e2744110e3dca7831c4ee \ 180 | --hash=sha256:7728e05c35412ba36d3e9795ae8995e3c86958179c9770e65558ec3fdfd3724f \ 181 | --hash=sha256:4bff24dfeea62f2e56f5bab929b4428ae6caba2d1eea0c2d6eb618e30a71e6d4 \ 182 | --hash=sha256:74f7d8d439b18fa4c385f3f5dfd11144bb87c1da034a466c5b5577d23a1d9b51 \ 183 | --hash=sha256:f90ba11136bfdd25cae3951af8da2e95121c9b9b93727b1b896e3fa105b2f586 \ 184 | --hash=sha256:4c61b3a0db43a1607d6264166b230438f85bfed02e8cff20c22e564d0faff354 \ 185 | --hash=sha256:5c8c163396cc0df3fd151b927e74f6e4acd67160d6c33304e805b84293351d16 \ 186 | --hash=sha256:f2380a6376dfa090227b663f9678150ef27543483055cc327555fb592c5967e2 \ 187 | --hash=sha256:c4f05c5a7c49d2fb70223d0d5bcfbe474cf928310ac9fa6a7c6dddc831d0b1d4 \ 188 | --hash=sha256:d2e35d7bf1c1ac8c538f88d26b396e73dd81440d59c1ef8522e1ea77b345ede4 \ 189 | --hash=sha256:289e9ca1a9287f08daaf796d96e06cb2bc2958891d7911ac7cae1c5f9e1e0ee3 \ 190 | --hash=sha256:bccbfc27563652de7dc9bdc595cb25e90b59c5f8e23e806ed0fd623755b6565d \ 191 | --hash=sha256:d916d31fd85b2f78c76400d625076d9124de3e4bda8b016d25a050cc7d603f24 \ 192 | --hash=sha256:820628b7b3135403540202e60551e741f9b6d3304371712521be939470b454ec \ 193 | --hash=sha256:c47ff7e0a36d4efac9fd692cfa33fbd0636674c102e9e8d9b26e1b93a94e7617 \ 194 | --hash=sha256:5a0a14e264069c03e46f926be0d8919f4105c1623d620e7ec0e612a2e9bf1c04 \ 195 | --hash=sha256:92e821e43ad382332eade6812e298dc9701c75fe289f2a2d39c7960b43d1e92a \ 196 | --hash=sha256:efd7a09678fd8b53117f6bae4fa3825e0a22b03ef0a932e070c0bdbb3a35e654 \ 197 | --hash=sha256:efac139c3f0bf4f0939f9375af4b02c5ad83a622de52d6dfa8e438e8e01d0eb0 \ 198 | --hash=sha256:0fbcf5565ac01dff87cbfc0ff323515c823081c5777a9fc7703ff58388c258c3 \ 199 | --hash=sha256:36108c73739985979bf302006527cf8a20515ce444ba916281d1c43938b8bb96 \ 200 | --hash=sha256:122fba10466c7bd4178b07dba427aa516286b846b2cbd6f6169141917283aae2 \ 201 | --hash=sha256:cdaf11d2bd275bf391b5308f86731e5194a21af45fbaaaf1d9e8147b9160ea92 \ 202 | --hash=sha256:3439c71103ef0e904ea0a1901611863e51f50b5cd5e8654a151740fde5e1cade \ 203 | --hash=sha256:4289728b5e2000a4ad4ab8da6e1db2e093c63c08bdc0414799ee776a3f78da4b \ 204 | --hash=sha256:b007cbb845b28db4fb8b6a5cdcbf65bacb16a8bd328b53cbc0698688a68e1caa \ 205 | --hash=sha256:76fa7b1362d19f8fbd3e75fe2fb7c79359b0af8747e6f7141c338f0bee2f871a \ 206 | --hash=sha256:26e761ab5b07adf5f555ee82fb4bfc35bf93750499c6c7614bd64d12aaa67927 \ 207 | --hash=sha256:e1cbd3f19a61e27e011e02f9600837b921ac661f0c40560eefb366e4e4fb275e \ 208 | --hash=sha256:66e575c62792c3f9ca47cb8b6fab9e35bab91360c783d1606f758761810c9791 \ 209 | --hash=sha256:1b38116b6e628118dea5b2186ee6820ab138dbb1e24a13e478490c7db2f326ae \ 210 | --hash=sha256:89b8b22a5ff72d89d48d0e62abb14340d9e99fd637d046c27b8b257a01ffbe28 \ 211 | --hash=sha256:2a9d50e69aac3ebee695424f7dbd7b8c6d6eb7de2a2eb6b0f6c7db6aa41e02b7 \ 212 | --hash=sha256:ce256aaa50f6cc9a649c51be3cd4ff142d67295bfc4f490c9134d0f9f6d58ef0 \ 213 | --hash=sha256:7610b8c31688f0b1be0ef882889817939490a36d0ee880ea562a4e1399c447a1 \ 214 | --hash=sha256:f8380c03e45cf09f8557bdaa41e1fa7c81f3ae22828e1db470ab2a6c96d8bc23 \ 215 | --hash=sha256:3082c518be8e97324390614dacd041bb1358c882d77108ca1957ba47738d9d59 \ 216 | --hash=sha256:884ab9b29feaca361f7f88d811b1eea9bfca36cf3da27768d28ad45c3ee6f969 \ 217 | --hash=sha256:6f12e1427285008fd32a6025e38e977d44d6382cf28e7201ed10d6c1698d2a9a \ 218 | --hash=sha256:33bb934a044cf32157c12bfcfbb6649807da20aa92c062ef51903415c704704f \ 219 | --hash=sha256:542d454665a3e277f76954418124d67516c5f88e51a900365ed54a9806122b83 \ 220 | --hash=sha256:39b78571b3b30645ac77b95f7c69d1bffc4cf8c3b157c435a34da72e78c82468 221 | mako==1.1.4; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" \ 222 | --hash=sha256:aea166356da44b9b830c8023cd9b557fa856bd8b4035d6de771ca027dfc5cc6e \ 223 | --hash=sha256:17831f0b7087c313c0ffae2bcbbd3c1d5ba9eeac9c38f2eb7b50e8c99fe9d5ab 224 | markupsafe==1.1.1; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.4.0") \ 225 | --hash=sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161 \ 226 | --hash=sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7 \ 227 | --hash=sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183 \ 228 | --hash=sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b \ 229 | --hash=sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e \ 230 | --hash=sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f \ 231 | --hash=sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1 \ 232 | --hash=sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5 \ 233 | --hash=sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1 \ 234 | --hash=sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735 \ 235 | --hash=sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21 \ 236 | --hash=sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235 \ 237 | --hash=sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b \ 238 | --hash=sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f \ 239 | --hash=sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905 \ 240 | --hash=sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1 \ 241 | --hash=sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d \ 242 | --hash=sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff \ 243 | --hash=sha256:d53bc011414228441014aa71dbec320c66468c1030aae3a6e29778a3382d96e5 \ 244 | --hash=sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473 \ 245 | --hash=sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e \ 246 | --hash=sha256:3b8a6499709d29c2e2399569d96719a1b21dcd94410a586a18526b143ec8470f \ 247 | --hash=sha256:84dee80c15f1b560d55bcfe6d47b27d070b4681c699c572af2e3c7cc90a3b8e0 \ 248 | --hash=sha256:b1dba4527182c95a0db8b6060cc98ac49b9e2f5e64320e2b56e47cb2831978c7 \ 249 | --hash=sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66 \ 250 | --hash=sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5 \ 251 | --hash=sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d \ 252 | --hash=sha256:bf5aa3cbcfdf57fa2ee9cd1822c862ef23037f5c832ad09cfea57fa846dec193 \ 253 | --hash=sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e \ 254 | --hash=sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6 \ 255 | --hash=sha256:6fffc775d90dcc9aed1b89219549b329a9250d918fd0b8fa8d93d154918422e1 \ 256 | --hash=sha256:a6a744282b7718a2a62d2ed9d993cad6f5f585605ad352c11de459f4108df0a1 \ 257 | --hash=sha256:195d7d2c4fbb0ee8139a6cf67194f3973a6b3042d742ebe0a9ed36d8b6f0c07f \ 258 | --hash=sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2 \ 259 | --hash=sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c \ 260 | --hash=sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15 \ 261 | --hash=sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2 \ 262 | --hash=sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42 \ 263 | --hash=sha256:acf08ac40292838b3cbbb06cfe9b2cb9ec78fce8baca31ddb87aaac2e2dc3bc2 \ 264 | --hash=sha256:d9be0ba6c527163cbed5e0857c451fcd092ce83947944d6c14bc95441203f032 \ 265 | --hash=sha256:caabedc8323f1e93231b52fc32bdcde6db817623d33e100708d9a68e1f53b26b \ 266 | --hash=sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b \ 267 | --hash=sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be \ 268 | --hash=sha256:d73a845f227b0bfe8a7455ee623525ee656a9e2e749e4742706d80a6065d5e2c \ 269 | --hash=sha256:98bae9582248d6cf62321dcb52aaf5d9adf0bad3b40582925ef7c7f0ed85fceb \ 270 | --hash=sha256:2beec1e0de6924ea551859edb9e7679da6e4870d32cb766240ce17e0a0ba2014 \ 271 | --hash=sha256:7fed13866cf14bba33e7176717346713881f56d9d2bcebab207f7a036f41b850 \ 272 | --hash=sha256:6f1e273a344928347c1290119b493a1f0303c52f5a5eae5f16d74f48c15d4a85 \ 273 | --hash=sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621 \ 274 | --hash=sha256:22c178a091fc6630d0d045bdb5992d2dfe14e3259760e713c490da5323866c39 \ 275 | --hash=sha256:b7d644ddb4dbd407d31ffb699f1d140bc35478da613b441c582aeb7c43838dd8 \ 276 | --hash=sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b 277 | psycopg2-binary==2.9.1; python_version >= "3.6" \ 278 | --hash=sha256:b0221ca5a9837e040ebf61f48899926b5783668b7807419e4adae8175a31f773 \ 279 | --hash=sha256:c250a7ec489b652c892e4f0a5d122cc14c3780f9f643e1a326754aedf82d9a76 \ 280 | --hash=sha256:aef9aee84ec78af51107181d02fe8773b100b01c5dfde351184ad9223eab3698 \ 281 | --hash=sha256:123c3fb684e9abfc47218d3784c7b4c47c8587951ea4dd5bc38b6636ac57f616 \ 282 | --hash=sha256:995fc41ebda5a7a663a254a1dcac52638c3e847f48307b5416ee373da15075d7 \ 283 | --hash=sha256:fbb42a541b1093385a2d8c7eec94d26d30437d0e77c1d25dae1dcc46741a385e \ 284 | --hash=sha256:20f1ab44d8c352074e2d7ca67dc00843067788791be373e67a0911998787ce7d \ 285 | --hash=sha256:f6fac64a38f6768e7bc7b035b9e10d8a538a9fadce06b983fb3e6fa55ac5f5ce \ 286 | --hash=sha256:1e3a362790edc0a365385b1ac4cc0acc429a0c0d662d829a50b6ce743ae61b5a \ 287 | --hash=sha256:f8559617b1fcf59a9aedba2c9838b5b6aa211ffedecabca412b92a1ff75aac1a \ 288 | --hash=sha256:a36c7eb6152ba5467fb264d73844877be8b0847874d4822b7cf2d3c0cb8cdcb0 \ 289 | --hash=sha256:2f62c207d1740b0bde5c4e949f857b044818f734a3d57f1d0d0edc65050532ed \ 290 | --hash=sha256:cfc523edecddaef56f6740d7de1ce24a2fdf94fd5e704091856a201872e37f9f \ 291 | --hash=sha256:1e85b74cbbb3056e3656f1cc4781294df03383127a8114cbc6531e8b8367bf1e \ 292 | --hash=sha256:1473c0215b0613dd938db54a653f68251a45a78b05f6fc21af4326f40e8360a2 \ 293 | --hash=sha256:35c4310f8febe41f442d3c65066ca93cccefd75013df3d8c736c5b93ec288140 \ 294 | --hash=sha256:8c13d72ed6af7fd2c8acbd95661cf9477f94e381fce0792c04981a8283b52917 \ 295 | --hash=sha256:14db1752acdd2187d99cb2ca0a1a6dfe57fc65c3281e0f20e597aac8d2a5bd90 \ 296 | --hash=sha256:aed4a9a7e3221b3e252c39d0bf794c438dc5453bc2963e8befe9d4cd324dff72 \ 297 | --hash=sha256:da113b70f6ec40e7d81b43d1b139b9db6a05727ab8be1ee559f3a69854a69d34 \ 298 | --hash=sha256:4235f9d5ddcab0b8dbd723dca56ea2922b485ea00e1dafacf33b0c7e840b3d32 \ 299 | --hash=sha256:988b47ac70d204aed01589ed342303da7c4d84b56c2f4c4b8b00deda123372bf \ 300 | --hash=sha256:7360647ea04db2e7dff1648d1da825c8cf68dc5fbd80b8fb5b3ee9f068dcd21a \ 301 | --hash=sha256:ca86db5b561b894f9e5f115d6a159fff2a2570a652e07889d8a383b5fae66eb4 \ 302 | --hash=sha256:5ced67f1e34e1a450cdb48eb53ca73b60aa0af21c46b9b35ac3e581cf9f00e31 \ 303 | --hash=sha256:0f2e04bd2a2ab54fa44ee67fe2d002bb90cee1c0f1cc0ebc3148af7b02034cbd \ 304 | --hash=sha256:3242b9619de955ab44581a03a64bdd7d5e470cc4183e8fcadd85ab9d3756ce7a \ 305 | --hash=sha256:0b7dae87f0b729922e06f85f667de7bf16455d411971b2043bbd9577af9d1975 \ 306 | --hash=sha256:b4d7679a08fea64573c969f6994a2631908bb2c0e69a7235648642f3d2e39a68 307 | pycparser==2.20; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.4.0") \ 308 | --hash=sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705 \ 309 | --hash=sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0 310 | python-dateutil==2.8.1; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" \ 311 | --hash=sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c \ 312 | --hash=sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a 313 | python-editor==1.0.4; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" \ 314 | --hash=sha256:51fda6bcc5ddbbb7063b2af7509e43bd84bfc32a4ff71349ec7847713882327b \ 315 | --hash=sha256:5f98b069316ea1c2ed3f67e7f5df6c0d8f10b689964a4a811ff64f0106819ec8 \ 316 | --hash=sha256:ea87e17f6ec459e780e4221f295411462e0d0810858e055fc514684350a2f522 \ 317 | --hash=sha256:1bf6e860a8ad52a14c3ee1252d5dc25b2030618ed80c022598f00176adc8367d \ 318 | --hash=sha256:c3da2053dbab6b29c94e43c486ff67206eafbe7eb52dbec7390b5e2fb05aac77 319 | requests==2.25.1; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") \ 320 | --hash=sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e \ 321 | --hash=sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804 322 | sgmllib3k==1.0.0; python_version >= "3.6" \ 323 | --hash=sha256:7868fb1c8bfa764c1ac563d3cf369c381d1325d36124933a726f29fcdaa812e9 324 | six==1.16.0; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.3.0") \ 325 | --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254 \ 326 | --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 327 | soupsieve==2.2.1; python_version >= "3.6" \ 328 | --hash=sha256:c2c1c2d44f158cdbddab7824a9af8c4f83c76b1e23e049479aa432feb6c4c23b \ 329 | --hash=sha256:052774848f448cf19c7e959adf5566904d525f33a3f8b6ba6f6f8f26ec7de0cc 330 | sqlalchemy==1.4.18; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.6.0") \ 331 | --hash=sha256:d76abceeb6f7c564fdbc304b1ce17ec59664ca7ed0fe6dbc6fc6a960c91370e3 \ 332 | --hash=sha256:4cdc91bb3ee5b10e24ec59303131b791f3f82caa4dd8b36064d1918b0f4d0de4 \ 333 | --hash=sha256:3690fc0fc671419debdae9b33df1434ac9253155fd76d0f66a01f7b459d56ee6 \ 334 | --hash=sha256:5b827d3d1d982b38d2bab551edf9893c4734b5db9b852b28d3bc809ea7e179f6 \ 335 | --hash=sha256:495cce8174c670f1d885e2259d710b0120888db2169ea14fc32d1f72e7950642 \ 336 | --hash=sha256:60cfe1fb59a34569816907cb25bb256c9490824679c46777377bcc01f6813a81 \ 337 | --hash=sha256:f3357948fa439eb5c7241a8856738605d7ab9d9f276ca5c5cc3220455a5f8e6c \ 338 | --hash=sha256:93394d68f02ecbf8c0a4355b6452793000ce0ee7aef79d2c85b491da25a88af7 \ 339 | --hash=sha256:56958dd833145f1aa75f8987dfe0cf6f149e93aa31967b7004d4eb9cb579fefc \ 340 | --hash=sha256:664c6cc84a5d2bad2a4a3984d146b6201b850ba0a7125b2fcd29ca06cddac4b1 \ 341 | --hash=sha256:77549e5ae996de50ad9f69f863c91daf04842b14233e133335b900b152bffb07 \ 342 | --hash=sha256:e2aa39fdf5bff1c325a8648ac1957a0320c66763a3fa5f0f4a02457b2afcf372 \ 343 | --hash=sha256:ffb18eb56546aa66640fef831e5d0fe1a8dfbf11cdf5b00803826a01dbbbf3b1 \ 344 | --hash=sha256:cc474d0c40cef94d9b68980155d686d5ad43a9ca0834a8729052d3585f289d57 \ 345 | --hash=sha256:5d4b2c23d20acf631456e645227cef014e7f84a111118d530cfa1d6053fd05a9 \ 346 | --hash=sha256:45bbb935b305e381bcb542bf4d952232282ba76881e3458105e4733ba0976060 \ 347 | --hash=sha256:3a6afb7a55374329601c8fcad277f0a47793386255764431c8f6a231a6947ee9 \ 348 | --hash=sha256:9a62b06ad450386a2e671d0bcc5cd430690b77a5cd41c54ede4e4bf46d7a4978 \ 349 | --hash=sha256:70674f2ff315a74061da7af1225770578d23f4f6f74dd2e1964493abd8d804bc \ 350 | --hash=sha256:4f375c52fed5f2ecd06be18756f121b3167a1fdc4543d877961fba04b1713214 \ 351 | --hash=sha256:eba098a4962e1ab0d446c814ae67e30da82c446b382cf718306cc90d4e2ad85f \ 352 | --hash=sha256:ee3428f6100ff2b07e7ecec6357d865a4d604c801760094883587ecdbf8a3533 \ 353 | --hash=sha256:5c62fff70348e3f8e4392540d31f3b8c251dc8eb830173692e5d61896d4309d6 \ 354 | --hash=sha256:8924d552decf1a50d57dca4984ebd0778a55ca2cb1c0ef16df8c1fed405ff290 \ 355 | --hash=sha256:284b6df04bc30e886998e0fdbd700ef9ffb83bcb484ffc54d4084959240dce91 \ 356 | --hash=sha256:146af9e67d0f821b28779d602372e65d019db01532d8f7101e91202d447c14ec \ 357 | --hash=sha256:2129d33b54da4d4771868a3639a07f461adc5887dbd9e0a80dbf560272245525 \ 358 | --hash=sha256:0653d444d52f2b9a0cba1ea5cd0fc64e616ee3838ee86c1863781b2a8670fc0c \ 359 | --hash=sha256:c824d14b52000597dfcced0a4e480fd8664b09fed606e746a2c67fe5fbe8dfd9 \ 360 | --hash=sha256:d25210f5f1a6b7b6b357d8fa199fc1d5be828c67cc1af517600c02e5b2727e4c 361 | typing-extensions==3.10.0.0; python_version >= "3.6" and python_full_version < "3.0.0" and python_version < "3.8" or python_full_version >= "3.6.0" and python_version < "3.8" and python_version >= "3.6" \ 362 | --hash=sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497 \ 363 | --hash=sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84 \ 364 | --hash=sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342 365 | urllib3==1.26.5; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0" and python_version < "4") \ 366 | --hash=sha256:753a0374df26658f99d826cfe40394a686d05985786d946fbe4165b5148f5a7c \ 367 | --hash=sha256:a7acd0977125325f516bda9735fa7142b909a8d01e8b2e4c8108d0984e6e0098 368 | werkzeug==1.0.1; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") \ 369 | --hash=sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43 \ 370 | --hash=sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c 371 | wtforms==2.3.3 \ 372 | --hash=sha256:7b504fc724d0d1d4d5d5c114e778ec88c37ea53144683e084215eed5155ada4c \ 373 | --hash=sha256:81195de0ac94fbc8368abbaf9197b88c4f3ffd6c2719b5bf5fc9da744f3d829c 374 | zipp==3.4.1; python_version >= "3.6" and python_full_version < "3.0.0" and python_version < "3.8" or python_full_version >= "3.6.0" and python_version < "3.8" and python_version >= "3.6" \ 375 | --hash=sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098 \ 376 | --hash=sha256:3607921face881ba3e026887d8150cca609d517579abe052ac81fc5aeffdbd76 377 | --------------------------------------------------------------------------------