26
loading...
This website collects cookies to deliver better user experience
class ProxyConnection(db.Model):
__tablename__ = "proxyconn"
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(
db.Integer,
db.ForeignKey("user.id", ondelete="CASCADE"),
index=True,
nullable=False,
)
proxy_url = db.Column(db.String, nullable=False)
username = db.Column(db.String, nullable=False)
password = db.Column(db.String, nullable=False)
# Can this proxy support multiple parallel requests?
allow_parallel = db.Column(
db.Boolean, default=False, server_default="f", nullable=False
)
success_count = db.Column(db.Integer, default=0, server_default="0")
block_count = db.Column(db.Integer, default=0, server_default="0")
no_result_count = db.Column(db.Integer, default=0, server_default="0")
consecutive_fails = db.Column(db.Integer, default=0, server_default="0")
# Proxy is currently in use (only applicable when allow_parallel = 'f').
engaged = db.Column(db.Boolean, default=False, server_default="f")
# Must wait at least this long before allowing another request.
min_wait_time = db.Column(db.Integer, default=0, server_default="0", nullable=False)
# Use random delay when proxying with a static IP to avoid blocks.
random_delay = db.Column(db.Integer, default=0, server_default="0", nullable=False)
last_used = db.Column(db.DateTime, index=True, nullable=True)
user = db.relationship("User")
from marshmallow_sqlalchemy import SQLAlchemyAutoSchema, auto_field
from app.models.proxyconn import ProxyConnection
class ProxySchema(SQLAlchemyAutoSchema):
class Meta:
model = ProxyConnection
load_instance = True
# Set password to load_only so that it is accepted during form
# submissions, but never dumped back into JSON format.
password = auto_field(load_only=True)
SQLAlchemyAutoSchema
class is a great convenience, because it automatically maps the model class to Marshmallow fields. When we need to treat a certain field differently, such as password here, it's easy enough to override the functionality.docker exec -it openranktracker_app_1 python manage.py shell
>> db.create_all()
ProxiesView
handles creating new proxies, as well as returning all proxies belonging to a specific user.from flask import request, g, abort
from marshmallow import ValidationError
from app.api.auth import AuthenticatedView
from app.models.proxyconn import ProxyConnection
from app.serde.proxy import ProxySchema
from app import db
class ProxiesView(AuthenticatedView):
def get(self):
return (
ProxySchema().dump(
ProxyConnection.query.filter_by(user_id=g.user.id)
.order_by(ProxyConnection.id)
.all(),
many=True,
),
200,
)
def post(self):
try:
proxy = ProxySchema().load(request.get_json(), session=db.session)
proxy.user = g.user
except ValidationError:
abort(400)
db.session.add(proxy)
db.session.commit()
return ProxySchema().dump(proxy), 201
ProxyView
will handle deletion of proxy connections.from flask import g, abort
from app.api.auth import AuthenticatedView
from app.models.proxyconn import ProxyConnection
from app import db
class ProxyView(AuthenticatedView):
def delete(self, proxy_id):
proxy = ProxyConnection.query.get(proxy_id)
if proxy.user_id != g.user.id:
abort(403)
db.session.delete(proxy)
db.session.commit()
return "", 200
app/api/__init__.py
to associate the new handlers with API routes.api.add_resource(ProxyView, "/proxies/<int:proxy_id>/")
api.add_resource(ProxiesView, "/proxies/")
ProxyPopup.js
module to see how that's done.import { Formik, Form, Field } from "formik";
import * as Yup from "yup";
const defaultProxy = {
proxy_url: "",
username: "",
password: "",
min_wait_time: 60,
random_delay: 10
};
const proxySchema = Yup.object().shape({
proxy_url: Yup.string().required(),
username: Yup.string().required(),
password: Yup.string().required(),
min_wait_time: Yup.number()
.positive()
.integer()
.required(),
random_delay: Yup.number()
.positive()
.integer()
.required()
});
Formik
component that expects a function as its child. We'll define our form inside that function, and Formik will pass arguments that include a values object, as well as touched and errors objects. touched
and errors
objects to flag the username field as an error. The password input isn't flagged, even though it's required, because the touched
object indicates that it hasn't experienced a blur event yet. The errors
object is updated automatically according to the Yup schema we provided. Formik simplifies tracking all of this state information.<Formik
initialValues={defaultProxy}
onSubmit={onSubmit}
validationSchema={proxySchema}
validateOnMount
>
{({
values,
errors,
touched,
handleChange,
handleBlur,
handleSubmit,
isValid
}) => (
<Form onSubmit={handleSubmit}>
<div className="formGroup">
<label className="formLabel">Proxy URL</label>
<Input
name="proxy_url"
onChange={handleChange}
onBlur={handleBlur}
value={values.proxy_url}
border={
touched.proxy_url &&
errors.proxy_url &&
`1px solid ${COLORS.warning}`
}
style={{ width: "100%" }}
/>
</div>
<div className="formGroup">
<label className="formLabel">
Proxy Username
</label>
<Input
name="username"
onBlur={handleBlur}
onChange={handleChange}
value={values.username}
border={
touched.username &&
errors.username &&
`1px solid ${COLORS.warning}`
}
style={{ width: "100%" }}
/>
</div>
</Form>
)}
</Formik>
Input
instead of normal HTML inputs. These are simply convenience classes created using styled components. I have created a handful of these commonly required elements in order to avoid redefining their CSS over and over.util/controls.js
module.import styled from "styled-components";
import { BORDER_RADIUS, COLORS, PAD_XS, PAD_SM } from "./constants";
export const Input = styled.input`
color: ${COLORS.fg1};
background-color: ${COLORS.bg4};
box-sizing: border-box;
padding: ${PAD_XS} ${PAD_SM};
outline: none;
border-radius: ${BORDER_RADIUS};
border: ${props => props.border || "none"};
`;
export const Button = styled.button`
background: none;
border: none;
border-radius: ${BORDER_RADIUS};
outline: none;
cursor: pointer;
&:disabled {
filter: brightness(50%);
cursor: default;
}
`;
<div className={styles.container}>
<div className={styles.buttonRow}>
<PrimaryButton
style={{ padding: PAD_SM, marginLeft: "auto" }}
onClick={addProxyServer}
>
Add Proxy Server
</PrimaryButton>
</div>
<div className={styles.proxyList}>
{proxies.map(proxy => (
<div key={proxy.id} className={styles.proxyContainer}>
<ProxyConnection proxy={proxy} onDelete={deleteProxy} />
</div>
))}
</div>
</div>
float: right
here, it's possible to use margin-left: auto
to achieve the same result. The proxyList class is also a flex container, of course, but with the flex-wrap
property added. nowrap
default of flex-wrap means items spill outside of their container when there isn't enough space. By changing to wrap
, the children are instead allowed to break to the next line..container {
padding: var(--pad-md);
padding-top: var(--pad-sm);
box-sizing: border-box;
}
.buttonRow {
display: flex;
margin-bottom: var(--margin-md);
}
.proxyList {
display: flex;
flex-wrap: wrap;
}
.proxyContainer {
margin-right: var(--margin-sm);
margin-bottom: var(--margin-sm);
}
box-sizing: border-box
prevents that added padding from creating scrollbars.DonutChart
component that works with any kind of data having up to 3 categories. The component expects a category prop that has positive, neutral, and negative keys that map to integer values.componentDidUpdate
to determine if a re-render is required.componentDidUpdate(prevProps) {
if (prevProps.category != this.props.category) {
this.drawChart();
}
}
drawChart
method contains the actual D3 rendering logic.drawChart() {
const svg = d3.select(this.svgRef.current).select("g");
const radius = Math.min(this.width, this.height) / 2;
const donutWidth = 10;
const arc = d3
.arc()
.padAngle(0.05)
.innerRadius(radius - donutWidth)
.outerRadius(radius)
.cornerRadius(15);
const data = [
this.props.category.POSITIVE,
this.props.category.NEGATIVE,
this.props.category.NEUTRAL
];
const pie = d3
.pie()
.value(d => d)
.sort(null);
// Select all existing SVG path elements and associate them with
// the positive, neutral, and negative sections of the donut
// chart.
const path = svg.selectAll("path").data(pie(data));
// The enter() and append() methods take into account any existing
// SVG paths (i.e. drawChart was already called) and appends
// additional path elements if necessary.
path.enter()
.append("path")
.merge(path)
.attr("d", arc)
.attr("fill", (d, i) => {
return [COLORS.success, COLORS.warning, COLORS.caution][i];
})
.attr("transform", "translate(0, 0)");
// The exit() method defines what should happen if there are more
// SVG path elements than data elements. In this case, we simply
// remove the extra path elements, but we can do more here, such
// as adding transition effects.
path.exit().remove();
}