mirror of
https://github.com/LukeHagar/pypistats.org.git
synced 2025-12-06 12:47:48 +00:00
update user model; templates for api; setup user page and github oauth
This commit is contained in:
35
migrations/versions/e65ba8f3cdcf_.py
Normal file
35
migrations/versions/e65ba8f3cdcf_.py
Normal file
@@ -0,0 +1,35 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: e65ba8f3cdcf
|
||||
Revises: c81b3715b9e5
|
||||
Create Date: 2018-04-06 17:58:19.643259
|
||||
|
||||
"""
|
||||
# flake8: noqa
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'e65ba8f3cdcf'
|
||||
down_revision = 'c81b3715b9e5'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('users', sa.Column('avatar_url', sa.String(length=256), nullable=True))
|
||||
op.add_column('users', sa.Column('uid', sa.Integer(), nullable=True))
|
||||
op.drop_constraint('users_username_key', 'users', type_='unique')
|
||||
op.create_unique_constraint(None, 'users', ['uid'])
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_constraint(None, 'users', type_='unique')
|
||||
op.create_unique_constraint('users_username_key', 'users', ['username'])
|
||||
op.drop_column('users', 'uid')
|
||||
op.drop_column('users', 'avatar_url')
|
||||
# ### end Alembic commands ###
|
||||
@@ -10,15 +10,20 @@ from pypistats.database import SurrogatePK
|
||||
from pypistats.extensions import db
|
||||
|
||||
|
||||
MAX_FAVORITES = 20
|
||||
|
||||
|
||||
class User(UserMixin, SurrogatePK, Model):
|
||||
"""A user of the app."""
|
||||
|
||||
__tablename__ = 'users'
|
||||
|
||||
username = Column(db.String(39), unique=True, nullable=False)
|
||||
# icon
|
||||
uid = Column(db.Integer(), unique=True)
|
||||
username = Column(db.String(39), nullable=False)
|
||||
avatar_url = Column(db.String(256))
|
||||
token = Column(db.String(256))
|
||||
created_at = Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow)
|
||||
created_at = \
|
||||
Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow)
|
||||
active = Column(db.Boolean(), default=False)
|
||||
is_admin = Column(db.Boolean(), default=False)
|
||||
favorites = Column(ARRAY(db.String(128), dimensions=1))
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
"""Run the application."""
|
||||
from flask import g
|
||||
from flask import session
|
||||
|
||||
from pypistats.application import create_app
|
||||
from pypistats.models.user import User
|
||||
from pypistats.settings import DevConfig
|
||||
from pypistats.settings import ProdConfig
|
||||
from pypistats.settings import TestConfig
|
||||
@@ -7,3 +11,11 @@ from pypistats.settings import TestConfig
|
||||
|
||||
# change this for migrations
|
||||
app = create_app(DevConfig)
|
||||
|
||||
|
||||
@app.before_request
|
||||
def before_request():
|
||||
"""Execute before requests."""
|
||||
g.user = None
|
||||
if "user_id" in session:
|
||||
g.user = User.query.get(session['user_id'])
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import os
|
||||
|
||||
from pypistats.secret import postgresql
|
||||
from pypistats.secret import github
|
||||
|
||||
|
||||
def get_db_uri(env):
|
||||
@@ -22,8 +23,6 @@ class Config(object):
|
||||
SECRET_KEY = os.environ.get("PYPISTATS_SECRET", "secret-key")
|
||||
APP_DIR = os.path.abspath(os.path.dirname(__file__))
|
||||
PROJECT_ROOT = os.path.abspath(os.path.join(APP_DIR, os.pardir))
|
||||
GITHUB_CLIENT_ID = "test"
|
||||
GITHUB_CLIENT_SECRET = "test"
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||
|
||||
|
||||
@@ -33,6 +32,8 @@ class ProdConfig(Config):
|
||||
ENV = "prod"
|
||||
DEBUG = False
|
||||
SQLALCHEMY_DATABASE_URI = get_db_uri(ENV)
|
||||
GITHUB_CLIENT_ID = github[ENV]["client_id"]
|
||||
GITHUB_CLIENT_SECRET = github[ENV]["client_secret"]
|
||||
|
||||
|
||||
class DevConfig(Config):
|
||||
@@ -41,6 +42,8 @@ class DevConfig(Config):
|
||||
ENV = "dev"
|
||||
DEBUG = True
|
||||
SQLALCHEMY_DATABASE_URI = get_db_uri(ENV)
|
||||
GITHUB_CLIENT_ID = github[ENV]["client_id"]
|
||||
GITHUB_CLIENT_SECRET = github[ENV]["client_secret"]
|
||||
|
||||
|
||||
class TestConfig(Config):
|
||||
@@ -51,3 +54,5 @@ class TestConfig(Config):
|
||||
DEBUG = True
|
||||
SQLALCHEMY_DATABASE_URI = get_db_uri(ENV)
|
||||
WTF_CSRF_ENABLED = False # Allows form testing
|
||||
GITHUB_CLIENT_ID = github[ENV]["client_id"]
|
||||
GITHUB_CLIENT_SECRET = github[ENV]["client_secret"]
|
||||
|
||||
@@ -4,15 +4,19 @@
|
||||
<h1>About PyPI Stats</h1>
|
||||
<hr>
|
||||
<h3>Goal</h3>
|
||||
<p>PyPI Stats aims to provide aggregate download information on python packages available from the Python Package Index.</p>
|
||||
<p>PyPI Stats aims to provide aggregate download information on python packages available from the Python Package Index in lieu of having to execute queries against raw download records in Google BigQuery.</p>
|
||||
<h3>Data</h3>
|
||||
<p>Download stats are sourced from the Python Software Foundation's publicly available
|
||||
<a href="https://bigquery.cloud.google.com/table/the-psf:pypi.downloads">download stats</a>
|
||||
on Google BigQuery. All aggregate download stats ignore known PyPI mirrors (such as
|
||||
<a href="{{url_for('general.package', package='bandersnatch')}}">bandersnatch</a>) unless noted otherwise.</p>
|
||||
<p>PyPI Stats attempts to operate within the free tier in AWS. For this reason, aggregate data is only retained for 30 days.</p>
|
||||
<p>PyPI Stats attempts to operate within the free tier of its hosted services. For this reason, aggregate data is only retained for 30 days.</p>
|
||||
<h3>API</h3>
|
||||
<p>A simple
|
||||
<a href="{{url_for('api.api')}}">JSON API</a>
|
||||
is available for aggregate download stats and time series for packages.</p>
|
||||
<h3>Tech</h3>
|
||||
<p>PyPI Stats is a project developed using Python 3.6.
|
||||
<p>PyPI Stats is a project developed using Python 3.6. Here are some of the tools used to create it:
|
||||
<ul>
|
||||
<li>Framework:
|
||||
<a href="{{url_for('general.package', package='flask')}}">Flask</a>
|
||||
|
||||
256
pypistats/templates/api.html
Normal file
256
pypistats/templates/api.html
Normal file
@@ -0,0 +1,256 @@
|
||||
{% extends "layout.html" %}
|
||||
{% block title %}PyPI Stats{% endblock %}
|
||||
{% block body %}
|
||||
<h1>PyPI Stats API</h1>
|
||||
<hr>
|
||||
<p>
|
||||
PyPI Stats provides a simple JSON API for retrieving aggregate download stats and time series for packages. The following are the valid endpoints using host:
|
||||
<code>https://pypistats.org/</code>
|
||||
</p>
|
||||
<h3>NOTES</h3>
|
||||
<p>
|
||||
<ul>
|
||||
<li>All download stats exclude known mirrors (such as
|
||||
<a href="{{ url_for('general.package', package='bandersnatch')}}">bandersnatch</a>) unless noted otherwise.</li>
|
||||
<li>Time series data is retained only for 30 days.</li>
|
||||
</ul>
|
||||
</p>
|
||||
<h2>Endpoints</h2>
|
||||
<h3>/api/<package>/recent</h3>
|
||||
<p>Retrieve the aggregate download quantities for the last day/week/month. Query arguments:
|
||||
<ul>
|
||||
<li>
|
||||
<b>period</b>
|
||||
(optional):
|
||||
<code>day</code>
|
||||
or
|
||||
<code>week</code>
|
||||
or
|
||||
<code>month</code>
|
||||
</li>
|
||||
</ul>
|
||||
Example response:
|
||||
<pre><code>{
|
||||
"data": {
|
||||
"last_day": 1,
|
||||
"last_month": 2,
|
||||
"last_week": 3
|
||||
},
|
||||
"package": "package_name",
|
||||
"type": "recent_downloads"
|
||||
}</code></pre>
|
||||
</p>
|
||||
<h3>/api/<package>/overall</h3>
|
||||
<p>Retrieve the aggregate daily download time series with or without mirror downloads. Query arguments:
|
||||
<ul>
|
||||
<li>
|
||||
<b>mirrors</b>
|
||||
(optional):
|
||||
<code>true</code>
|
||||
or
|
||||
<code>false</code>
|
||||
</li>
|
||||
<li>
|
||||
<b>start_date</b>
|
||||
(optional): starting date of time series in format
|
||||
<code>YYYY-MM-DD</code>
|
||||
</li>
|
||||
<li>
|
||||
<b>end_date</b>
|
||||
(optional): ending date of time series in format
|
||||
<code>YYYY-MM-DD</code>
|
||||
</li>
|
||||
</ul>
|
||||
Example response:
|
||||
<pre><code>{
|
||||
"data": [
|
||||
{
|
||||
"category": "with_mirrors",
|
||||
"date": "2018-02-08",
|
||||
"downloads": 1
|
||||
},
|
||||
{
|
||||
"category": "without_mirrors",
|
||||
"date": "2018-02-08",
|
||||
"downloads": 1
|
||||
}
|
||||
],
|
||||
"package": "package_name",
|
||||
"type": "overall_downloads"
|
||||
}</code></pre>
|
||||
</p>
|
||||
<h3>/api/<package>/python_major</h3>
|
||||
<p>Retrieve the aggregate daily download time series by Python major version number. Query arguments:
|
||||
<ul>
|
||||
<li>
|
||||
<b>version</b>
|
||||
(optional): the Python major version number, e.g.
|
||||
<code>2</code>
|
||||
or
|
||||
<code>3</code>
|
||||
</li>
|
||||
<li>
|
||||
<b>start_date</b>
|
||||
(optional): starting date of time series in format
|
||||
<code>YYYY-MM-DD</code>
|
||||
</li>
|
||||
<li>
|
||||
<b>end_date</b>
|
||||
(optional): ending date of time series in format
|
||||
<code>YYYY-MM-DD</code>
|
||||
</li>
|
||||
</ul>
|
||||
Example response:
|
||||
<pre><code>{
|
||||
"data": [
|
||||
{
|
||||
"category": "2",
|
||||
"date": "2018-02-08",
|
||||
"downloads": 1
|
||||
},
|
||||
{
|
||||
"category": "3",
|
||||
"date": "2018-02-08",
|
||||
"downloads": 1
|
||||
},
|
||||
{
|
||||
"category": "null",
|
||||
"date": "2018-02-08",
|
||||
"downloads": 1
|
||||
}
|
||||
],
|
||||
"package": "package_name",
|
||||
"type": "python_major_downloads"
|
||||
}</code></pre>
|
||||
</p>
|
||||
<h3>/api/<package>/python_minor</h3>
|
||||
<p>Retrieve the aggregate daily download time series by Python minor version number. Query arguments:
|
||||
<ul>
|
||||
<li>
|
||||
<b>version</b>
|
||||
(optional): the Python major version number, e.g.
|
||||
<code>2.7</code>
|
||||
or
|
||||
<code>3.6</code>
|
||||
</li>
|
||||
<li>
|
||||
<b>start_date</b>
|
||||
(optional): starting date of time series in format
|
||||
<code>YYYY-MM-DD</code>
|
||||
</li>
|
||||
<li>
|
||||
<b>end_date</b>
|
||||
(optional): ending date of time series in format
|
||||
<code>YYYY-MM-DD</code>
|
||||
</li>
|
||||
</ul>
|
||||
Example response:
|
||||
<pre><code>{
|
||||
"data": [
|
||||
{
|
||||
"category": "2.6",
|
||||
"date": "2018-02-08",
|
||||
"downloads": 1
|
||||
},
|
||||
{
|
||||
"category": "2.7",
|
||||
"date": "2018-02-08",
|
||||
"downloads": 1
|
||||
},
|
||||
{
|
||||
"category": "3.2",
|
||||
"date": "2018-02-08",
|
||||
"downloads": 1
|
||||
},
|
||||
{
|
||||
"category": "3.3",
|
||||
"date": "2018-02-08",
|
||||
"downloads": 1
|
||||
},
|
||||
{
|
||||
"category": "3.4",
|
||||
"date": "2018-02-08",
|
||||
"downloads": 1
|
||||
},
|
||||
{
|
||||
"category": "3.5",
|
||||
"date": "2018-02-08",
|
||||
"downloads": 1
|
||||
},
|
||||
{
|
||||
"category": "3.6",
|
||||
"date": "2018-02-08",
|
||||
"downloads": 1
|
||||
},
|
||||
{
|
||||
"category": "3.7",
|
||||
"date": "2018-02-08",
|
||||
"downloads": 1
|
||||
},
|
||||
{
|
||||
"category": "null",
|
||||
"date": "2018-02-08",
|
||||
"downloads": 1
|
||||
}
|
||||
],
|
||||
"package": "package_name",
|
||||
"type": "python_minor_downloads"
|
||||
}</code></pre>
|
||||
</p>
|
||||
<h3>/api/<package>/system</h3>
|
||||
<p>Retrieve the aggregate daily download time series by operating system. Query arguments:
|
||||
<ul>
|
||||
<li>
|
||||
<b>os</b>
|
||||
(optional): the operating system name, e.g.
|
||||
<code>windows</code>,
|
||||
<code>linux</code>, or
|
||||
<code>darwin</code>
|
||||
(Mac OSX).
|
||||
</li>
|
||||
<li>
|
||||
<b>start_date</b>
|
||||
(optional): starting date of time series in format
|
||||
<code>YYYY-MM-DD</code>
|
||||
</li>
|
||||
<li>
|
||||
<b>end_date</b>
|
||||
(optional): ending date of time series in format
|
||||
<code>YYYY-MM-DD</code>
|
||||
</li>
|
||||
</ul>
|
||||
Example response:
|
||||
<pre><code>{
|
||||
"data": [
|
||||
{
|
||||
"category": "darwin",
|
||||
"date": "2018-02-08",
|
||||
"downloads": 1
|
||||
},
|
||||
{
|
||||
"category": "linux",
|
||||
"date": "2018-02-08",
|
||||
"downloads": 1
|
||||
},
|
||||
{
|
||||
"category": "null",
|
||||
"date": "2018-02-08",
|
||||
"downloads": 1
|
||||
},
|
||||
{
|
||||
"category": "other",
|
||||
"date": "2018-02-08",
|
||||
"downloads": 1
|
||||
},
|
||||
{
|
||||
"category": "windows",
|
||||
"date": "2018-02-08",
|
||||
"downloads": 1
|
||||
}
|
||||
],
|
||||
"package": "package_name",
|
||||
"type": "system_downloads"
|
||||
}</code></pre>
|
||||
</p>
|
||||
|
||||
{% endblock %}
|
||||
@@ -53,10 +53,14 @@
|
||||
<br>
|
||||
<a href="{{ url_for('general.package', package='__all__') }}">__all__</a>
|
||||
<br>
|
||||
<a href="{{ url_for('general.top') }}">Top</a>
|
||||
<a href="{{ url_for('general.top') }}">__top__</a>
|
||||
<br>
|
||||
<br>
|
||||
<a href="{{ url_for('user.login')}}">Personal</a>
|
||||
{% if user %}
|
||||
<a href="{{ url_for('user.user')}}">{{ user.username }}'s Packages</a>
|
||||
{% else %}
|
||||
<a href="{{ url_for('user.user')}}">My Packages</a>
|
||||
{% endif %}
|
||||
<br>
|
||||
<a href="{{ url_for('user.logout') }}">Logout</a>
|
||||
</p>
|
||||
@@ -67,10 +71,10 @@
|
||||
</section>
|
||||
<footer>
|
||||
<p>
|
||||
<a href="{{ url_for('api.api') }}">API</a>
|
||||
<br>
|
||||
<a href="{{ url_for('general.about') }}">About</a>
|
||||
<br>
|
||||
|
||||
<small>PyPI Stats.<br></small>
|
||||
</p>
|
||||
</p>
|
||||
</footer>
|
||||
|
||||
@@ -5,9 +5,19 @@
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js"></script>
|
||||
{% endblock %}
|
||||
{% block body %}
|
||||
<h1>Downloads for package:
|
||||
{{package}}</h1>
|
||||
<h1>{{package}}</h1>
|
||||
<hr>
|
||||
{% if user %}
|
||||
{% if user.favorites and package in user.favorites %}
|
||||
<p>
|
||||
<img src="{{ user.avatar_url }}" height="20" width="20">
|
||||
<a href="{{ url_for('user.user_package', package=package)}}">REMOVE from my packages</a><br></p>
|
||||
{% else %}
|
||||
<p>
|
||||
<img src="{{ user.avatar_url }}" height="20" width="20">
|
||||
<a href="{{ url_for('user.user_package', package=package)}}">ADD to my packages</a><br></p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if package == "__all__" %}
|
||||
<p>Download stats for __all__ indicate downloads across all packages on PyPI.</p>
|
||||
<br>
|
||||
|
||||
33
pypistats/templates/user.html
Normal file
33
pypistats/templates/user.html
Normal file
@@ -0,0 +1,33 @@
|
||||
{% extends "layout.html" %}
|
||||
{% block title %}PyPI Stats{% endblock %}
|
||||
{% block body %}
|
||||
{% if user %}
|
||||
|
||||
<h1><img src="{{ user.avatar_url }}" height="30" width="30">
|
||||
{{ user.username }}'s Packages</h1>
|
||||
<hr>
|
||||
<p>Currently saved packages.</p>
|
||||
{% if user.favorites %}
|
||||
<p>
|
||||
<ul>
|
||||
{% for package in user.favorites %}
|
||||
<li>
|
||||
<a href="{{ url_for('general.package', package=package) }}">{{ package }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<!-- <table> {% for package in user.favorites %} <tr> <td> <a href="{{ url_for('general.package', package=package) }}">{{ package }}</a> </td> <td> <a href="{{ url_for('user.user_package', package=package) }}">REMOVE</a> </td> </tr> {% endfor %}
|
||||
</table> -->
|
||||
</p>
|
||||
{% else %}
|
||||
<p>Not tracking any packages.</p>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<h1>My Packages</h1>
|
||||
<hr>
|
||||
<p>Login with GitHub OAuth to track your own set of packages.</p>
|
||||
<p>
|
||||
<a href="{{ url_for('user.login') }}">Login</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -1,7 +1,9 @@
|
||||
"""JSON API routes."""
|
||||
from flask import abort
|
||||
from flask import Blueprint
|
||||
from flask import g
|
||||
from flask import jsonify
|
||||
from flask import render_template
|
||||
from flask import request
|
||||
|
||||
from pypistats.models.download import OverallDownloadCount
|
||||
@@ -15,6 +17,12 @@ from pypistats.models.download import SystemDownloadCount
|
||||
blueprint = Blueprint('api', __name__, url_prefix='/api')
|
||||
|
||||
|
||||
@blueprint.route("/")
|
||||
def api():
|
||||
"""Get API documentation."""
|
||||
return render_template("api.html", user=g.user)
|
||||
|
||||
|
||||
@blueprint.route("/<package>/recent")
|
||||
def api_downloads_recent(package):
|
||||
"""Get the recent downloads of a package."""
|
||||
@@ -99,7 +107,7 @@ def generic_downloads(model, package, arg, name):
|
||||
category = request.args.get(f"{arg}")
|
||||
if category is not None:
|
||||
downloads = model.query.\
|
||||
filter_by(package=package, category=category).\
|
||||
filter_by(package=package, category=category.lower()).\
|
||||
order_by(model.date).all()
|
||||
else:
|
||||
downloads = model.query.\
|
||||
|
||||
@@ -4,6 +4,7 @@ import os
|
||||
|
||||
from flask import Blueprint
|
||||
from flask import current_app
|
||||
from flask import g
|
||||
from flask import json
|
||||
from flask import redirect
|
||||
from flask import render_template
|
||||
@@ -44,7 +45,7 @@ def index():
|
||||
if form.validate_on_submit():
|
||||
package = form.name.data
|
||||
return redirect(f"/search/{package}")
|
||||
return render_template("index.html", form=form)
|
||||
return render_template("index.html", form=form, user=g.user)
|
||||
|
||||
|
||||
@blueprint.route("/search/<package>", methods=("GET", "POST"))
|
||||
@@ -61,14 +62,14 @@ def search(package):
|
||||
limit(20).all()
|
||||
packages = [r.package for r in results]
|
||||
return render_template(
|
||||
"search.html", search=True, form=form, packages=packages
|
||||
"search.html", search=True, form=form, packages=packages, user=g.user
|
||||
)
|
||||
|
||||
|
||||
@blueprint.route("/about")
|
||||
def about():
|
||||
"""Render the about page."""
|
||||
return render_template("about.html")
|
||||
return render_template("about.html", user=g.user)
|
||||
|
||||
|
||||
@blueprint.route("/package/<package>")
|
||||
@@ -125,7 +126,8 @@ def package(package):
|
||||
package=package,
|
||||
plots=plots,
|
||||
metadata=metadata,
|
||||
recent=recent
|
||||
recent=recent,
|
||||
user=g.user
|
||||
)
|
||||
|
||||
|
||||
@@ -159,7 +161,7 @@ def top():
|
||||
"downloads": d.downloads,
|
||||
} for d in downloads]
|
||||
})
|
||||
return render_template("top.html", top=top)
|
||||
return render_template("top.html", top=top, user=g.user)
|
||||
|
||||
|
||||
@blueprint.route("/status")
|
||||
|
||||
@@ -1,61 +1,21 @@
|
||||
"""User page for tracking packages."""
|
||||
from flask import abort
|
||||
from flask import Blueprint
|
||||
from flask import flash
|
||||
from flask import g
|
||||
from flask import redirect
|
||||
from flask import render_template
|
||||
from flask import request
|
||||
from flask import session
|
||||
from flask import url_for
|
||||
|
||||
from pypistats.extensions import db
|
||||
from pypistats.extensions import github
|
||||
from pypistats.models.download import RecentDownloadCount
|
||||
from pypistats.models.user import MAX_FAVORITES
|
||||
from pypistats.models.user import User
|
||||
|
||||
blueprint = Blueprint('user', __name__, template_folder='templates')
|
||||
|
||||
|
||||
@blueprint.route("/user/<user>")
|
||||
def user(user):
|
||||
"""Render the user's personal page."""
|
||||
return user + "'s page"
|
||||
|
||||
|
||||
@blueprint.route("/user/<user>/package/<package>", methods=['POST', 'DELETE'])
|
||||
def user_package(user):
|
||||
"""Handle adding and deleting packages to user's list."""
|
||||
return "SOMETHING"
|
||||
|
||||
|
||||
@blueprint.route('/login')
|
||||
def login():
|
||||
"""Login."""
|
||||
return github.authorize()
|
||||
|
||||
|
||||
@blueprint.route('/logout')
|
||||
def logout():
|
||||
"""Logout."""
|
||||
session.pop('user_id', None)
|
||||
return redirect(url_for('general.index'))
|
||||
|
||||
|
||||
@blueprint.route('/github-callback')
|
||||
@github.authorized_handler
|
||||
def authorized(oauth_token):
|
||||
"""Github authorization callback."""
|
||||
next_url = request.args.get('next') or url_for('index')
|
||||
if oauth_token is None:
|
||||
flash("Authorization failed.")
|
||||
return redirect(next_url)
|
||||
|
||||
user = User.query.filter_by(token=oauth_token).first()
|
||||
if user is None:
|
||||
user = User(oauth_token)
|
||||
db.add(user)
|
||||
|
||||
user.github_access_token = oauth_token
|
||||
db.commit()
|
||||
return redirect(next_url)
|
||||
blueprint = Blueprint("user", __name__, template_folder="templates")
|
||||
|
||||
|
||||
@github.access_token_getter
|
||||
@@ -63,4 +23,113 @@ def token_getter():
|
||||
"""Get the token for a user."""
|
||||
user = g.user
|
||||
if user is not None:
|
||||
return user.github_access_token
|
||||
return user.token
|
||||
|
||||
|
||||
@blueprint.route("/github-callback")
|
||||
@github.authorized_handler
|
||||
def authorized(oauth_token):
|
||||
"""Github authorization callback."""
|
||||
next_url = request.args.get("next") or url_for("user.user")
|
||||
if oauth_token is None:
|
||||
flash("Authorization failed.")
|
||||
return redirect(next_url)
|
||||
|
||||
# Ensure a user with token doesn't already exist
|
||||
user = User.query.filter_by(token=oauth_token).first()
|
||||
if user is None:
|
||||
user = User(token=oauth_token)
|
||||
|
||||
# Set this to use API to get user data
|
||||
g.user = user
|
||||
user_data = github.get("user")
|
||||
|
||||
# extract data
|
||||
uid = user_data["id"]
|
||||
username = user_data["login"]
|
||||
avatar_url = user_data["avatar_url"]
|
||||
|
||||
# Create/update the user
|
||||
user = User.query.filter_by(uid=uid).first()
|
||||
if user is None:
|
||||
user = User(
|
||||
token=oauth_token,
|
||||
uid=uid,
|
||||
username=username,
|
||||
avatar_url=avatar_url,
|
||||
)
|
||||
else:
|
||||
user.username = username
|
||||
user.avatar_url = avatar_url
|
||||
user.token = oauth_token
|
||||
|
||||
user.save()
|
||||
|
||||
session["username"] = user.username
|
||||
session["user_id"] = user.id
|
||||
g.user = user
|
||||
|
||||
return redirect(next_url)
|
||||
|
||||
|
||||
@blueprint.route("/login")
|
||||
def login():
|
||||
"""Login via GitHub OAuth."""
|
||||
if session.get("user_id", None) is None:
|
||||
return github.authorize()
|
||||
else:
|
||||
return redirect(url_for("user.user"))
|
||||
|
||||
|
||||
@blueprint.route("/logout")
|
||||
def logout():
|
||||
"""Logout."""
|
||||
session.pop("user_id", None)
|
||||
session.pop("username", None)
|
||||
g.user = None
|
||||
return redirect(url_for("general.index"))
|
||||
|
||||
|
||||
@blueprint.route("/user")
|
||||
def user():
|
||||
"""Render the user's personal page."""
|
||||
return render_template("user.html", user=g.user)
|
||||
|
||||
|
||||
@blueprint.route("/user/package/<package>")
|
||||
def user_package(package):
|
||||
"""Handle adding and deleting packages to user's list."""
|
||||
if g.user:
|
||||
# Ensure package is valid.
|
||||
downloads = RecentDownloadCount.query.filter_by(package=package).all()
|
||||
if downloads is None:
|
||||
return abort(400)
|
||||
|
||||
# Handle add/remove to favorites
|
||||
if g.user.favorites is None:
|
||||
g.user.favorites = [package]
|
||||
g.user.update()
|
||||
return redirect(url_for("user.user"))
|
||||
elif package in g.user.favorites:
|
||||
favorites = g.user.favorites
|
||||
favorites.remove(package)
|
||||
# Workaround for sqlalchemy mutable ARRAY types
|
||||
g.user.favorites = None
|
||||
g.user.save()
|
||||
g.user.favorites = favorites
|
||||
g.user.save()
|
||||
return redirect(url_for("user.user"))
|
||||
else:
|
||||
if len(g.user.favorites) < MAX_FAVORITES:
|
||||
favorites = g.user.favorites
|
||||
favorites.append(package)
|
||||
favorites = sorted(favorites)
|
||||
# Workaround for sqlalchemy mutable ARRAY types
|
||||
g.user.favorites = None
|
||||
g.user.save()
|
||||
g.user.favorites = favorites
|
||||
g.user.save()
|
||||
return redirect(url_for("user.user"))
|
||||
else:
|
||||
return f"Maximum package number reached ({MAX_FAVORITES})."
|
||||
return abort(400)
|
||||
|
||||
Reference in New Issue
Block a user