structables 0.3.12__tar.gz → 0.3.14__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {structables-0.3.12 → structables-0.3.14}/PKG-INFO +1 -1
- {structables-0.3.12 → structables-0.3.14}/pyproject.toml +1 -1
- {structables-0.3.12 → structables-0.3.14}/src/structables/main.py +1 -3
- structables-0.3.14/src/structables/routes/contest.py +174 -0
- {structables-0.3.12 → structables-0.3.14}/src/structables/routes/main.py +8 -9
- {structables-0.3.12 → structables-0.3.14}/src/structables/static/css/style.css +48 -120
- structables-0.3.14/src/structables/templates/archives.html +38 -0
- structables-0.3.14/src/structables/templates/contest.html +23 -0
- structables-0.3.14/src/structables/templates/contests.html +21 -0
- structables-0.3.12/src/structables/routes/contest.py +0 -177
- structables-0.3.12/src/structables/templates/archives.html +0 -31
- structables-0.3.12/src/structables/templates/contest.html +0 -23
- structables-0.3.12/src/structables/templates/contests.html +0 -40
- {structables-0.3.12 → structables-0.3.14}/.env.example +0 -0
- {structables-0.3.12 → structables-0.3.14}/.forgejo/workflows/docker.yml +0 -0
- {structables-0.3.12 → structables-0.3.14}/.forgejo/workflows/release.yml +0 -0
- {structables-0.3.12 → structables-0.3.14}/.gitignore +0 -0
- {structables-0.3.12 → structables-0.3.14}/Dockerfile +0 -0
- {structables-0.3.12 → structables-0.3.14}/LICENSE +0 -0
- {structables-0.3.12 → structables-0.3.14}/README.md +0 -0
- {structables-0.3.12 → structables-0.3.14}/cronjob.txt +0 -0
- {structables-0.3.12 → structables-0.3.14}/docker-compose-example-dev.yml +0 -0
- {structables-0.3.12 → structables-0.3.14}/docker-compose-example.yml +0 -0
- {structables-0.3.12 → structables-0.3.14}/entrypoint.sh +0 -0
- {structables-0.3.12 → structables-0.3.14}/src/structables/__init__.py +0 -0
- {structables-0.3.12 → structables-0.3.14}/src/structables/config.py +0 -0
- {structables-0.3.12 → structables-0.3.14}/src/structables/routes/__init__.py +0 -0
- {structables-0.3.12 → structables-0.3.14}/src/structables/routes/category.py +0 -0
- {structables-0.3.12 → structables-0.3.14}/src/structables/routes/member.py +0 -0
- {structables-0.3.12 → structables-0.3.14}/src/structables/routes/proxy.py +0 -0
- {structables-0.3.12 → structables-0.3.14}/src/structables/static/dist/css/bootstrap.min.css +0 -0
- {structables-0.3.12 → structables-0.3.14}/src/structables/static/dist/css/bootstrap.min.css.map +0 -0
- {structables-0.3.12 → structables-0.3.14}/src/structables/static/img/logo.png +0 -0
- {structables-0.3.12 → structables-0.3.14}/src/structables/static/img/logo_lg.png +0 -0
- {structables-0.3.12 → structables-0.3.14}/src/structables/static/img/magnifying-glass-solid.svg +0 -0
- {structables-0.3.12 → structables-0.3.14}/src/structables/templates/400.html +0 -0
- {structables-0.3.12 → structables-0.3.14}/src/structables/templates/404.html +0 -0
- {structables-0.3.12 → structables-0.3.14}/src/structables/templates/429.html +0 -0
- {structables-0.3.12 → structables-0.3.14}/src/structables/templates/500.html +0 -0
- {structables-0.3.12 → structables-0.3.14}/src/structables/templates/article-review.html +0 -0
- {structables-0.3.12 → structables-0.3.14}/src/structables/templates/article.html +0 -0
- {structables-0.3.12 → structables-0.3.14}/src/structables/templates/base.html +0 -0
- {structables-0.3.12 → structables-0.3.14}/src/structables/templates/category.html +0 -0
- {structables-0.3.12 → structables-0.3.14}/src/structables/templates/collection.html +0 -0
- {structables-0.3.12 → structables-0.3.14}/src/structables/templates/footer.html +0 -0
- {structables-0.3.12 → structables-0.3.14}/src/structables/templates/header.html +0 -0
- {structables-0.3.12 → structables-0.3.14}/src/structables/templates/iframe.html +0 -0
- {structables-0.3.12 → structables-0.3.14}/src/structables/templates/index.html +0 -0
- {structables-0.3.12 → structables-0.3.14}/src/structables/templates/member-instructables.html +0 -0
- {structables-0.3.12 → structables-0.3.14}/src/structables/templates/member.html +0 -0
- {structables-0.3.12 → structables-0.3.14}/src/structables/templates/privacypolicy.html +0 -0
- {structables-0.3.12 → structables-0.3.14}/src/structables/templates/projects.html +0 -0
- {structables-0.3.12 → structables-0.3.14}/src/structables/templates/sitemap.html +0 -0
- {structables-0.3.12 → structables-0.3.14}/src/structables/templates/style.html +0 -0
- {structables-0.3.12 → structables-0.3.14}/src/structables/utils/data.py +0 -0
- {structables-0.3.12 → structables-0.3.14}/src/structables/utils/helpers.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: structables
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.14
|
|
4
4
|
Summary: A simple frontend for Instructables
|
|
5
5
|
Project-URL: Homepage, https://git.private.coffee/privatecoffee/structables
|
|
6
6
|
Project-URL: Bug Tracker, https://git.private.coffee/privatecoffee/structables/issues
|
|
@@ -14,6 +14,7 @@ app.config.from_object(Config)
|
|
|
14
14
|
app.typesense_api_key = get_typesense_api_key()
|
|
15
15
|
|
|
16
16
|
init_routes(app)
|
|
17
|
+
update_data(app)
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
def background_update_data(app):
|
|
@@ -40,6 +41,3 @@ def main():
|
|
|
40
41
|
|
|
41
42
|
if __name__ == "__main__":
|
|
42
43
|
main()
|
|
43
|
-
else:
|
|
44
|
-
# Even if we're using a wsgi server, we still have to initialize global_ibles
|
|
45
|
-
update_data(app)
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
from flask import render_template, request, abort, url_for
|
|
2
|
+
from urllib.request import urlopen, Request
|
|
3
|
+
from urllib.error import HTTPError
|
|
4
|
+
from ..utils.helpers import proxy
|
|
5
|
+
from bs4 import BeautifulSoup
|
|
6
|
+
import json
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def init_contest_routes(app):
|
|
10
|
+
@app.route("/contest/archive/")
|
|
11
|
+
def route_contest_archive():
|
|
12
|
+
# Default pagination settings
|
|
13
|
+
limit = 10
|
|
14
|
+
page = request.args.get("page", default=1, type=int)
|
|
15
|
+
offset = (page - 1) * limit
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
# Fetch data using urlopen
|
|
19
|
+
url = f"https://www.instructables.com/json-api/getClosedContests?limit={limit}&offset={offset}"
|
|
20
|
+
response = urlopen(url)
|
|
21
|
+
data = json.loads(response.read().decode())
|
|
22
|
+
except HTTPError as e:
|
|
23
|
+
abort(e.code)
|
|
24
|
+
except Exception as e:
|
|
25
|
+
abort(500) # Handle other exceptions like JSON decode errors
|
|
26
|
+
|
|
27
|
+
contests = data.get("contests", [])
|
|
28
|
+
full_list_size = data.get("fullListSize", 0)
|
|
29
|
+
|
|
30
|
+
contest_list = []
|
|
31
|
+
for contest in contests:
|
|
32
|
+
contest_details = {
|
|
33
|
+
"title": contest["title"],
|
|
34
|
+
"link": url_for("route_contest", contest=contest["urlString"]),
|
|
35
|
+
"deadline": contest["deadline"],
|
|
36
|
+
"startDate": contest["startDate"],
|
|
37
|
+
"numEntries": contest["numEntries"],
|
|
38
|
+
"state": contest["state"],
|
|
39
|
+
"bannerUrl": proxy(contest["bannerUrlMedium"]),
|
|
40
|
+
}
|
|
41
|
+
contest_list.append(contest_details)
|
|
42
|
+
|
|
43
|
+
# Calculate total pages
|
|
44
|
+
total_pages = (full_list_size + limit - 1) // limit
|
|
45
|
+
|
|
46
|
+
# Create pagination
|
|
47
|
+
pagination = {
|
|
48
|
+
"current_page": page,
|
|
49
|
+
"total_pages": total_pages,
|
|
50
|
+
"has_prev": page > 1,
|
|
51
|
+
"has_next": page < total_pages,
|
|
52
|
+
"limit": limit,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return render_template(
|
|
56
|
+
"archives.html",
|
|
57
|
+
title=f"Contest Archives (Page {page})",
|
|
58
|
+
page=page,
|
|
59
|
+
pagination=pagination,
|
|
60
|
+
contest_list=contest_list,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
def get_entries(contest):
|
|
64
|
+
base_url = f"https://www.instructables.com/api_proxy/search/collections/projects/documents/search"
|
|
65
|
+
headers = {"x-typesense-api-key": app.typesense_api_key}
|
|
66
|
+
page, per_page = 1, 100
|
|
67
|
+
all_entries = []
|
|
68
|
+
|
|
69
|
+
while True:
|
|
70
|
+
try:
|
|
71
|
+
url = f"{base_url}?q=*&filter_by=contestPath:{contest}&sort_by=contestEntryDate:desc&per_page={per_page}&page={page}"
|
|
72
|
+
request = Request(url, headers=headers)
|
|
73
|
+
response = urlopen(request)
|
|
74
|
+
data = json.loads(response.read().decode())
|
|
75
|
+
except HTTPError as e:
|
|
76
|
+
abort(e.code)
|
|
77
|
+
|
|
78
|
+
hits = data.get("hits", [])
|
|
79
|
+
if not hits:
|
|
80
|
+
break
|
|
81
|
+
|
|
82
|
+
all_entries.extend(hits)
|
|
83
|
+
if len(hits) < per_page:
|
|
84
|
+
break
|
|
85
|
+
page += 1
|
|
86
|
+
|
|
87
|
+
return all_entries
|
|
88
|
+
|
|
89
|
+
@app.route("/contest/<contest>/")
|
|
90
|
+
def route_contest(contest):
|
|
91
|
+
try:
|
|
92
|
+
data = urlopen(f"https://www.instructables.com/contest/{contest}/")
|
|
93
|
+
html = data.read().decode()
|
|
94
|
+
soup = BeautifulSoup(html, "html.parser")
|
|
95
|
+
|
|
96
|
+
title_tag = soup.find("h1")
|
|
97
|
+
title = title_tag.get_text() if title_tag else "Contest"
|
|
98
|
+
|
|
99
|
+
img_tag = soup.find("img", alt=lambda x: x and "Banner" in x)
|
|
100
|
+
img = img_tag.get("src") if img_tag else "default.jpg"
|
|
101
|
+
|
|
102
|
+
entry_count = len(get_entries(contest))
|
|
103
|
+
prizes_items = soup.select("article")
|
|
104
|
+
prizes = len(prizes_items) if prizes_items else 0
|
|
105
|
+
|
|
106
|
+
overview_section = soup.find("section", id="overview")
|
|
107
|
+
info = (
|
|
108
|
+
overview_section.decode_contents()
|
|
109
|
+
if overview_section
|
|
110
|
+
else "No Overview"
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
except HTTPError as e:
|
|
114
|
+
abort(e.code)
|
|
115
|
+
|
|
116
|
+
entry_list = []
|
|
117
|
+
entries = get_entries(contest)
|
|
118
|
+
for entry in entries:
|
|
119
|
+
doc = entry["document"]
|
|
120
|
+
entry_details = {
|
|
121
|
+
"link": url_for("route_article", article=doc["urlString"]),
|
|
122
|
+
"entry_img": doc["coverImageUrl"],
|
|
123
|
+
"entry_title": doc["title"],
|
|
124
|
+
"author": doc["screenName"],
|
|
125
|
+
"author_link": url_for("route_member", member=doc["screenName"]),
|
|
126
|
+
"channel": doc["channel"][0],
|
|
127
|
+
"channel_link": f"/{doc['primaryClassification']}",
|
|
128
|
+
"views": doc.get("views", 0),
|
|
129
|
+
}
|
|
130
|
+
entry_list.append(entry_details)
|
|
131
|
+
|
|
132
|
+
return render_template(
|
|
133
|
+
"contest.html",
|
|
134
|
+
title=title,
|
|
135
|
+
img=img,
|
|
136
|
+
entry_count=entry_count,
|
|
137
|
+
prizes=prizes,
|
|
138
|
+
info=info,
|
|
139
|
+
entry_list=entry_list,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
@app.route("/contest/")
|
|
143
|
+
def route_contests():
|
|
144
|
+
try:
|
|
145
|
+
# Fetch current contests from the JSON API
|
|
146
|
+
response = urlopen(
|
|
147
|
+
"https://www.instructables.com/json-api/getCurrentContests?limit=50&offset=0"
|
|
148
|
+
)
|
|
149
|
+
data = json.loads(response.read().decode())
|
|
150
|
+
except HTTPError as e:
|
|
151
|
+
abort(e.code)
|
|
152
|
+
except Exception as e:
|
|
153
|
+
abort(500) # Handle other exceptions such as JSON decode errors
|
|
154
|
+
|
|
155
|
+
contests = data.get("contests", [])
|
|
156
|
+
contest_list = []
|
|
157
|
+
for contest in contests:
|
|
158
|
+
contest_details = {
|
|
159
|
+
"link": url_for("route_contest", contest=contest["urlString"]),
|
|
160
|
+
"img": proxy(contest["bannerUrlMedium"]),
|
|
161
|
+
"alt": contest["title"],
|
|
162
|
+
"title": contest["title"],
|
|
163
|
+
"deadline": contest["deadline"],
|
|
164
|
+
"prizes": contest["prizeCount"],
|
|
165
|
+
"entries": contest["numEntries"],
|
|
166
|
+
}
|
|
167
|
+
contest_list.append(contest_details)
|
|
168
|
+
|
|
169
|
+
return render_template(
|
|
170
|
+
"contests.html",
|
|
171
|
+
title="Contests",
|
|
172
|
+
contest_count=len(contest_list),
|
|
173
|
+
contests=contest_list,
|
|
174
|
+
)
|
|
@@ -357,17 +357,16 @@ def init_main_routes(app):
|
|
|
357
357
|
elif pathlib.Path("privacy.txt").exists():
|
|
358
358
|
path = "privacy.txt"
|
|
359
359
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
print(path, content)
|
|
360
|
+
if path:
|
|
361
|
+
try:
|
|
362
|
+
with pathlib.Path(path).open() as f:
|
|
363
|
+
content = f.read()
|
|
365
364
|
|
|
366
|
-
|
|
367
|
-
|
|
365
|
+
if path.endswith(".md"):
|
|
366
|
+
content = Markdown().convert(content)
|
|
368
367
|
|
|
369
|
-
|
|
370
|
-
|
|
368
|
+
except OSError:
|
|
369
|
+
pass
|
|
371
370
|
|
|
372
371
|
return render_template(
|
|
373
372
|
"privacypolicy.html", title="Privacy Policy", content=content
|
|
@@ -130,144 +130,67 @@ blockquote {
|
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
.
|
|
134
|
-
display: inline-block;
|
|
135
|
-
vertical-align: top;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
.ible-small {
|
|
139
|
-
font-size: 0.7em;
|
|
140
|
-
font-weight: thin;
|
|
141
|
-
line-height: 1em;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
.step-section {
|
|
145
|
-
background-color: var(--primary-bg);
|
|
146
|
-
border: 1px solid var(--border-color);
|
|
147
|
-
border-radius: 8px;
|
|
148
|
-
box-shadow: 0 2px 4px var(--shadow-color);
|
|
149
|
-
padding: 1.5rem;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
.step-header h2 {
|
|
153
|
-
font-size: 2em;
|
|
154
|
-
color: var(--heading-color);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
.step-images img,
|
|
158
|
-
.step-videos video,
|
|
159
|
-
.step-iframes iframe {
|
|
160
|
-
border-radius: 8px;
|
|
161
|
-
box-shadow: 0 2px 4px var(--shadow-color);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
.step-text {
|
|
165
|
-
font-size: 1.1em;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
.reply-button,
|
|
169
|
-
.replies {
|
|
170
|
-
display: none;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
.reply-button+label {
|
|
174
|
-
position: relative;
|
|
175
|
-
display: block;
|
|
176
|
-
cursor: pointer;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
input.reply-button:checked+label+.replies {
|
|
133
|
+
.contest-list {
|
|
180
134
|
display: flex;
|
|
181
135
|
flex-direction: column;
|
|
182
|
-
gap:
|
|
183
|
-
margin-top: 1rem;
|
|
136
|
+
gap: 20px;
|
|
184
137
|
}
|
|
185
138
|
|
|
186
|
-
.
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
.ible-list-item {
|
|
193
|
-
display: inline-block;
|
|
194
|
-
max-width: 350px;
|
|
195
|
-
vertical-align: top;
|
|
196
|
-
margin-bottom: 2rem;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
.contest-list-item {
|
|
200
|
-
display: inline-block;
|
|
201
|
-
max-width: 500px;
|
|
202
|
-
vertical-align: top;
|
|
203
|
-
margin-bottom: 2rem;
|
|
139
|
+
.contest-item {
|
|
140
|
+
background-color: #fff;
|
|
141
|
+
padding: 20px;
|
|
142
|
+
border: 1px solid var(--border-color);
|
|
143
|
+
border-radius: 8px;
|
|
144
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
204
145
|
}
|
|
205
146
|
|
|
206
|
-
.
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
147
|
+
.contest-item img {
|
|
148
|
+
max-width: 100%;
|
|
149
|
+
height: auto;
|
|
150
|
+
border-radius: 4px;
|
|
151
|
+
margin-top: 10px;
|
|
210
152
|
}
|
|
211
153
|
|
|
212
|
-
.
|
|
154
|
+
.pagination {
|
|
213
155
|
display: flex;
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
justify-content: space-between;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
.archive {
|
|
221
|
-
margin-bottom: -20px;
|
|
156
|
+
justify-content: center;
|
|
157
|
+
padding: 10px 0;
|
|
158
|
+
list-style-type: none;
|
|
222
159
|
}
|
|
223
160
|
|
|
224
|
-
|
|
161
|
+
.pagination-list {
|
|
225
162
|
display: flex;
|
|
226
|
-
justify-content: space-around;
|
|
227
|
-
padding: 0 33vw;
|
|
228
|
-
list-style-type: none;
|
|
229
163
|
align-items: center;
|
|
164
|
+
gap: 10px;
|
|
230
165
|
}
|
|
231
166
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
ul.pagination li.active a:hover,
|
|
235
|
-
ul.pagination li.disabled a:hover {
|
|
236
|
-
color: #bbc2cf;
|
|
237
|
-
text-decoration: none;
|
|
167
|
+
.pagination-list li {
|
|
168
|
+
display: inline-block;
|
|
238
169
|
}
|
|
239
170
|
|
|
240
|
-
.
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
171
|
+
.pagination-list a {
|
|
172
|
+
color: var(--main-color);
|
|
173
|
+
text-decoration: none;
|
|
174
|
+
padding: 5px 10px;
|
|
175
|
+
border-radius: 4px;
|
|
176
|
+
border: 1px solid transparent;
|
|
177
|
+
transition: all 0.3s ease;
|
|
247
178
|
}
|
|
248
179
|
|
|
249
|
-
.
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
display: inline-block;
|
|
253
|
-
vertical-align: top;
|
|
254
|
-
padding: 0 10px;
|
|
255
|
-
font-size: 0.8em;
|
|
256
|
-
text-overflow: ellipsis;
|
|
257
|
-
white-space: nowrap;
|
|
258
|
-
overflow: hidden;
|
|
180
|
+
.pagination-list a:hover {
|
|
181
|
+
border-color: var(--link-color);
|
|
182
|
+
color: var(--link-color);
|
|
259
183
|
}
|
|
260
184
|
|
|
261
|
-
.
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
text-align: left;
|
|
266
|
-
vertical-align: top;
|
|
185
|
+
.pagination-list li.active a {
|
|
186
|
+
background-color: var(--link-color);
|
|
187
|
+
color: #fff;
|
|
188
|
+
border-color: var(--link-color);
|
|
267
189
|
}
|
|
268
190
|
|
|
269
|
-
.
|
|
270
|
-
|
|
191
|
+
.pagination-list li.disabled a {
|
|
192
|
+
color: #ccc;
|
|
193
|
+
cursor: not-allowed;
|
|
271
194
|
}
|
|
272
195
|
|
|
273
196
|
.container {
|
|
@@ -280,7 +203,6 @@ header {
|
|
|
280
203
|
|
|
281
204
|
.go_here_link {
|
|
282
205
|
background-color: #4caf50;
|
|
283
|
-
/* Green */
|
|
284
206
|
border: none;
|
|
285
207
|
color: white;
|
|
286
208
|
padding: 15px 32px;
|
|
@@ -299,10 +221,6 @@ header {
|
|
|
299
221
|
overflow-wrap: break-word;
|
|
300
222
|
}
|
|
301
223
|
|
|
302
|
-
.navbar-logo {
|
|
303
|
-
height: 64px;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
224
|
.img-fluid {
|
|
307
225
|
border-radius: 8px;
|
|
308
226
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
@@ -322,7 +240,17 @@ iframe {
|
|
|
322
240
|
color: #6c757d !important;
|
|
323
241
|
}
|
|
324
242
|
|
|
243
|
+
.sitemap-group {
|
|
244
|
+
margin-top: 2em;
|
|
245
|
+
display: inline-block;
|
|
246
|
+
width: 30vw;
|
|
247
|
+
text-align: left;
|
|
248
|
+
vertical-align: top;
|
|
249
|
+
}
|
|
325
250
|
|
|
251
|
+
.sitemap-group h2 {
|
|
252
|
+
text-align: center;
|
|
253
|
+
}
|
|
326
254
|
|
|
327
255
|
.sitemap-group .card {
|
|
328
256
|
background-color: var(--primary-bg);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{% extends "base.html" %}
|
|
2
|
+
|
|
3
|
+
{% block content %}
|
|
4
|
+
<center>
|
|
5
|
+
<h1>Past Contests</h1>
|
|
6
|
+
<p>Total Contests: {{ pagination.total_pages * pagination.limit }}</p>
|
|
7
|
+
<p><a href="/contest/">See running contests</a></p>
|
|
8
|
+
</center>
|
|
9
|
+
<div class="contest-list">
|
|
10
|
+
{% for contest in contest_list %}
|
|
11
|
+
<div class="contest-item">
|
|
12
|
+
<hr>
|
|
13
|
+
<h2>{{ contest.title }}</h2>
|
|
14
|
+
<p>
|
|
15
|
+
<a href="{{ contest.link }}">{{ contest.title }}</a><br>
|
|
16
|
+
Start Date: {{ contest.startDate }}<br>
|
|
17
|
+
Deadline: {{ contest.deadline }}<br>
|
|
18
|
+
Entries: {{ contest.numEntries }}<br>
|
|
19
|
+
Status: {{ contest.state }}<br>
|
|
20
|
+
</p>
|
|
21
|
+
<img src="{{ contest.bannerUrl }}" alt="{{ contest.title }} banner" style="max-width:100%;">
|
|
22
|
+
<hr>
|
|
23
|
+
</div>
|
|
24
|
+
{% endfor %}
|
|
25
|
+
</div>
|
|
26
|
+
<hr>
|
|
27
|
+
<div class="pagination">
|
|
28
|
+
<ul class="pagination-list">
|
|
29
|
+
{% if pagination.has_prev %}
|
|
30
|
+
<li><a href="?page={{ pagination.current_page - 1 }}">Previous</a></li>
|
|
31
|
+
{% endif %}
|
|
32
|
+
<li>Page {{ pagination.current_page }} of {{ pagination.total_pages }}</li>
|
|
33
|
+
{% if pagination.has_next %}
|
|
34
|
+
<li><a href="?page={{ pagination.current_page + 1 }}">Next</a></li>
|
|
35
|
+
{% endif %}
|
|
36
|
+
</ul>
|
|
37
|
+
</div>
|
|
38
|
+
{% endblock %}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{% extends "base.html" %}
|
|
2
|
+
|
|
3
|
+
{% block content %}
|
|
4
|
+
<center>
|
|
5
|
+
<h1>{{ title }}</h1>
|
|
6
|
+
<img src="{{ img }}" alt="{{ title }}" style="max-width:98vw;">
|
|
7
|
+
<p>{{ entry_count }} Entries, {{ prizes }} Prizes</p>
|
|
8
|
+
<br>
|
|
9
|
+
{{ info|safe }}
|
|
10
|
+
<div class="ible-list">
|
|
11
|
+
{% for ible in entry_list %}
|
|
12
|
+
<div class="ible-list-item">
|
|
13
|
+
<a href="{{ ible.link }}" style="color:#bbc2cf;">
|
|
14
|
+
<img style="max-width:350px;" src="{{ ible.entry_img }}" alt="{{ ible.entry_title }}">
|
|
15
|
+
<p>{{ ible.entry_title }}</p>
|
|
16
|
+
</a>
|
|
17
|
+
<p>by <a href="{{ ible.author_link }}">{{ ible.author }}</a> in <a href="{{ ible.channel_link }}">{{ ible.channel }}</a></p>
|
|
18
|
+
<p>{{ ible.views }} Views</p>
|
|
19
|
+
</div>
|
|
20
|
+
{% endfor %}
|
|
21
|
+
</div>
|
|
22
|
+
</center>
|
|
23
|
+
{% endblock %}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{% extends "base.html" %}
|
|
2
|
+
|
|
3
|
+
{% block content %}
|
|
4
|
+
<center>
|
|
5
|
+
<h1>{{ title }}</h1>
|
|
6
|
+
<p>Total Running Contests: {{ contest_count }}</p>
|
|
7
|
+
<br>
|
|
8
|
+
<div class="contest-list">
|
|
9
|
+
{% for contest in contests %}
|
|
10
|
+
<div class="contest-list-item">
|
|
11
|
+
<a href="{{ contest.link }}">
|
|
12
|
+
<img src="{{ contest.img }}" alt="{{ contest.alt }}">
|
|
13
|
+
</a>
|
|
14
|
+
<h2>{{ contest.title }}</h2>
|
|
15
|
+
<p>Closes: {{ contest.deadline }}</p>
|
|
16
|
+
<p class="ible-small">{{ contest.prizes }} Prizes, {{ contest.entries }} Entries</p>
|
|
17
|
+
</div>
|
|
18
|
+
{% endfor %}
|
|
19
|
+
</div>
|
|
20
|
+
</center>
|
|
21
|
+
{% endblock %}
|
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
from flask import render_template, request, abort
|
|
2
|
-
from urllib.request import urlopen
|
|
3
|
-
from urllib.error import HTTPError
|
|
4
|
-
from ..utils.helpers import proxy
|
|
5
|
-
from bs4 import BeautifulSoup
|
|
6
|
-
|
|
7
|
-
def init_contest_routes(app):
|
|
8
|
-
@app.route("/contest/archive/")
|
|
9
|
-
def route_contest_archive():
|
|
10
|
-
page = 1
|
|
11
|
-
if request.args.get("page") is not None:
|
|
12
|
-
page = request.args.get("page")
|
|
13
|
-
|
|
14
|
-
try:
|
|
15
|
-
data = urlopen(f"https://www.instructables.com/contest/archive/?page={page}")
|
|
16
|
-
except HTTPError as e:
|
|
17
|
-
abort(e.code)
|
|
18
|
-
|
|
19
|
-
soup = BeautifulSoup(data.read().decode(), "html.parser")
|
|
20
|
-
|
|
21
|
-
main = soup.select("div#contest-archive-wrapper")[0]
|
|
22
|
-
|
|
23
|
-
contest_count = main.select("p.contest-count")[0].text
|
|
24
|
-
|
|
25
|
-
contest_list = []
|
|
26
|
-
for index, year in enumerate(main.select("div.contest-archive-list h2")):
|
|
27
|
-
year_list = main.select(
|
|
28
|
-
"div.contest-archive-list div.contest-archive-list-year"
|
|
29
|
-
)[index]
|
|
30
|
-
year_name = year.text
|
|
31
|
-
month_list = []
|
|
32
|
-
for month in year_list.select("div.contest-archive-list-month"):
|
|
33
|
-
month_name = month.select("h3")[0].text
|
|
34
|
-
month_contest_list = []
|
|
35
|
-
for p in month.select("p"):
|
|
36
|
-
date = p.select("span")[0].text
|
|
37
|
-
link = p.select("a")[0].get("href")
|
|
38
|
-
title = p.select("a")[0].text
|
|
39
|
-
month_contest_list.append([date, link, title])
|
|
40
|
-
month_list.append([month_name, month_contest_list])
|
|
41
|
-
contest_list.append([year_name, month_list])
|
|
42
|
-
|
|
43
|
-
pagination = main.select("nav.pagination ul.pagination")[0]
|
|
44
|
-
|
|
45
|
-
return render_template(
|
|
46
|
-
"archives.html",
|
|
47
|
-
title=f"Contest Archives (Page {page})",
|
|
48
|
-
page=page,
|
|
49
|
-
contest_count=contest_count,
|
|
50
|
-
pagination=pagination,
|
|
51
|
-
contest_list=contest_list,
|
|
52
|
-
)
|
|
53
|
-
|
|
54
|
-
@app.route("/contest/<contest>/")
|
|
55
|
-
def route_contest(contest):
|
|
56
|
-
try:
|
|
57
|
-
data = urlopen(f"https://www.instructables.com/contest/{contest}/")
|
|
58
|
-
except HTTPError as e:
|
|
59
|
-
abort(e.code)
|
|
60
|
-
|
|
61
|
-
soup = BeautifulSoup(data.read().decode(), "html.parser")
|
|
62
|
-
|
|
63
|
-
title = soup.select('meta[property="og:title"]')[0].get("content")
|
|
64
|
-
|
|
65
|
-
body = soup.select("div#contest-wrapper")[0]
|
|
66
|
-
|
|
67
|
-
img = proxy(body.select("div#contest-masthead img")[0].get("src"))
|
|
68
|
-
|
|
69
|
-
entry_count = body.select("li.entries-nav-btn")[0].text.split(" ")[0]
|
|
70
|
-
prizes = body.select("li.prizes-nav-btn")[0].text.split(" ")[0]
|
|
71
|
-
|
|
72
|
-
info = body.select("div.contest-body-column-left")[0]
|
|
73
|
-
info.select("div#site-announcements-page")[0].decompose()
|
|
74
|
-
info.select("h3")[0].decompose()
|
|
75
|
-
info.select("div#contest-body-nav")[0].decompose()
|
|
76
|
-
info = str(info).replace("https://www.instructables.com", "/")
|
|
77
|
-
|
|
78
|
-
body.select("span.contest-entity-count")[0].text
|
|
79
|
-
|
|
80
|
-
entry_list = []
|
|
81
|
-
for entry in body.select("div.contest-entries-list div.contest-entries-list-ible"):
|
|
82
|
-
link = entry.a["href"]
|
|
83
|
-
entry_img = proxy(entry.select("a noscript img")[0].get("src"))
|
|
84
|
-
entry_title = entry.select("a.ible-title")[0].text
|
|
85
|
-
author = entry.select("div span.ible-author a")[0].text
|
|
86
|
-
author_link = entry.select("div span.ible-author a")[0].get("href")
|
|
87
|
-
channel = entry.select("div span.ible-channel a")[0].text
|
|
88
|
-
channel_link = entry.select("div span.ible-channel a")[0].get("href")
|
|
89
|
-
views = entry.select(".ible-views")[0].text
|
|
90
|
-
|
|
91
|
-
entry_list.append(
|
|
92
|
-
{
|
|
93
|
-
"link": link,
|
|
94
|
-
"entry_img": entry_img,
|
|
95
|
-
"entry_title": entry_title,
|
|
96
|
-
"author": author,
|
|
97
|
-
"author_link": author_link,
|
|
98
|
-
"channel": channel,
|
|
99
|
-
"channel_link": channel_link,
|
|
100
|
-
"views": views,
|
|
101
|
-
}
|
|
102
|
-
)
|
|
103
|
-
|
|
104
|
-
return render_template(
|
|
105
|
-
"contest.html",
|
|
106
|
-
title=title,
|
|
107
|
-
img=img,
|
|
108
|
-
entry_count=entry_count,
|
|
109
|
-
prizes=prizes,
|
|
110
|
-
info=info,
|
|
111
|
-
entry_list=entry_list,
|
|
112
|
-
)
|
|
113
|
-
|
|
114
|
-
@app.route("/contest/")
|
|
115
|
-
def route_contests():
|
|
116
|
-
try:
|
|
117
|
-
data = urlopen("https://www.instructables.com/contest/")
|
|
118
|
-
except HTTPError as e:
|
|
119
|
-
abort(e.code)
|
|
120
|
-
|
|
121
|
-
soup = BeautifulSoup(data.read().decode(), "html.parser")
|
|
122
|
-
|
|
123
|
-
contest_count = str(soup.select("p.contest-count")[0])
|
|
124
|
-
|
|
125
|
-
contests = []
|
|
126
|
-
for contest in soup.select("div#cur-contests div.row-fluid div.contest-banner"):
|
|
127
|
-
link = contest.select("div.contest-banner-inner a")[0].get("href")
|
|
128
|
-
img = proxy(contest.select("div.contest-banner-inner a img")[0].get("src"))
|
|
129
|
-
alt = contest.select("div.contest-banner-inner a img")[0].get("alt")
|
|
130
|
-
deadline = contest.select("span.contest-meta-deadline")[0].get("data-deadline")
|
|
131
|
-
prizes = contest.select("span.contest-meta-count")[0].text
|
|
132
|
-
entries = contest.select("span.contest-meta-count")[1].text
|
|
133
|
-
|
|
134
|
-
contests.append(
|
|
135
|
-
{
|
|
136
|
-
"link": link,
|
|
137
|
-
"img": img,
|
|
138
|
-
"alt": alt,
|
|
139
|
-
"deadline": deadline,
|
|
140
|
-
"prizes": prizes,
|
|
141
|
-
"entries": entries,
|
|
142
|
-
}
|
|
143
|
-
)
|
|
144
|
-
|
|
145
|
-
closed = []
|
|
146
|
-
for display in soup.select("div.contest-winner-display"):
|
|
147
|
-
link = display.select("div.contest-banner-inner a")[0].get("href")
|
|
148
|
-
img = proxy(display.select("div.contest-banner-inner a img")[0].get("src"))
|
|
149
|
-
alt = display.select("div.contest-banner-inner a img")[0].get("alt")
|
|
150
|
-
featured_items = []
|
|
151
|
-
for featured_item in display.select("ul.featured-items li"):
|
|
152
|
-
item_link = featured_item.select("div.ible-thumb a")[0].get("href")
|
|
153
|
-
item_img = proxy(featured_item.select("div.ible-thumb a img")[0].get("src"))
|
|
154
|
-
item_title = featured_item.select("a.title")[0].text
|
|
155
|
-
item_author = featured_item.select("a.author")[0].text
|
|
156
|
-
item_author_link = featured_item.select("a.author")[0].get("href")
|
|
157
|
-
|
|
158
|
-
featured_items.append(
|
|
159
|
-
{
|
|
160
|
-
"link": item_link,
|
|
161
|
-
"img": item_img,
|
|
162
|
-
"title": item_title,
|
|
163
|
-
"author": item_author,
|
|
164
|
-
"author_link": item_author_link,
|
|
165
|
-
}
|
|
166
|
-
)
|
|
167
|
-
closed.append(
|
|
168
|
-
{"link": link, "img": img, "alt": alt, "featured_items": featured_items}
|
|
169
|
-
)
|
|
170
|
-
|
|
171
|
-
return render_template(
|
|
172
|
-
"contests.html",
|
|
173
|
-
title="Contests",
|
|
174
|
-
contest_count=contest_count,
|
|
175
|
-
contests=contests,
|
|
176
|
-
closed=closed,
|
|
177
|
-
)
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
{% extends "base.html" %}
|
|
2
|
-
|
|
3
|
-
{% block content %}
|
|
4
|
-
<center>
|
|
5
|
-
<h1>Past Contests</h1>
|
|
6
|
-
<p>{{ contest_count }}</p>
|
|
7
|
-
<p><a href="/contest/">See running contests</a></p>
|
|
8
|
-
</center>
|
|
9
|
-
{% for year in contest_list %}
|
|
10
|
-
<div class="archive-year">
|
|
11
|
-
<hr>
|
|
12
|
-
<h2>{{ year[0] }}</h2>
|
|
13
|
-
<hr>
|
|
14
|
-
{% for month in year[1] %}
|
|
15
|
-
<div class="archive-month-wrapper">
|
|
16
|
-
<div class="archive-month">
|
|
17
|
-
<h3>{{ month[0] }}</h3>
|
|
18
|
-
{% for contest in month[1] %}
|
|
19
|
-
<div class="archive">
|
|
20
|
-
<p>{{ contest[0] }}<a href="{{ contest[1] }}">{{ contest[2] }}</a></p>
|
|
21
|
-
</div>
|
|
22
|
-
{% endfor %}
|
|
23
|
-
</div>
|
|
24
|
-
</div>
|
|
25
|
-
{% endfor %}
|
|
26
|
-
<br>
|
|
27
|
-
</div>
|
|
28
|
-
{% endfor %}
|
|
29
|
-
<hr>
|
|
30
|
-
{{ pagination|safe }}
|
|
31
|
-
{% endblock %}
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
{% extends "base.html" %}
|
|
3
|
-
|
|
4
|
-
{% block content %}
|
|
5
|
-
<center>
|
|
6
|
-
<img src="{{ img }}" alt="{{ title }}" style="max-width:98vw;">
|
|
7
|
-
<p>{{ entry_count }} Entries, {{ prizes }} Prizes</p>
|
|
8
|
-
<br>
|
|
9
|
-
{{ info|safe }}
|
|
10
|
-
<div class="ible-list">
|
|
11
|
-
{% for ible in entries %}
|
|
12
|
-
<div class="ible-list-item">
|
|
13
|
-
<a href="{{ ible.link }}" style="color:#bbc2cf;">
|
|
14
|
-
<img style="max-width:350px;" src="{{ ible.entry_img }}" alt="{{ ible.entry_title }}">
|
|
15
|
-
<p>{{ ible.entry_title }}</p>
|
|
16
|
-
</a>
|
|
17
|
-
<p>by <a href="{{ ible.author_link }}">{{ ible.author }}</a> in <a href="{{ ible.channel_link }}">{{ ible.channel }}</a></p>
|
|
18
|
-
<p>{{ ible.views }} Views</p>
|
|
19
|
-
</div>
|
|
20
|
-
{% endfor %}
|
|
21
|
-
</div>
|
|
22
|
-
</center>
|
|
23
|
-
{% endblock %}
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
{% extends "base.html" %}
|
|
2
|
-
|
|
3
|
-
{% block content %}
|
|
4
|
-
<center>
|
|
5
|
-
<h1>{{ title }}</h1>
|
|
6
|
-
{{ contest_count|safe }}
|
|
7
|
-
<br>
|
|
8
|
-
<div class="contest-list">
|
|
9
|
-
{% for contest in contests %}
|
|
10
|
-
<div class="contest-list-item">
|
|
11
|
-
<a href="{{ contest.link }}">
|
|
12
|
-
<img src="{{ contest.img }}" alt="{{ contest.alt }}" style="max-width:500px;">
|
|
13
|
-
</a>
|
|
14
|
-
<p>Closes {{ contest.deadline }}</p>
|
|
15
|
-
<p class="ible-small">{{ contest.prizes }} Prizes, {{ contest.entries }} Entries</p>
|
|
16
|
-
</div>
|
|
17
|
-
{% endfor %}
|
|
18
|
-
</div>
|
|
19
|
-
<div class="closed-contests" id="contest-winners">
|
|
20
|
-
<h2>Winner's Circle</h2>
|
|
21
|
-
{% for closed in closed %}
|
|
22
|
-
<div class="closed-contest">
|
|
23
|
-
<a href="{{ closed.link }}"><img class="closed-contest-contest" src="{{ closed.img }}"
|
|
24
|
-
alt="{{ closed.alt }}"></a>
|
|
25
|
-
{% for featured_items in closed.featured_items %}
|
|
26
|
-
<div class="closed-contest-winner">
|
|
27
|
-
<a href="{{ featured_items.link }}">
|
|
28
|
-
<img class="closed-contest-winner-img" src="{{ featured_items.img }}"
|
|
29
|
-
alt="{{ featured_items.title}}">
|
|
30
|
-
<br>
|
|
31
|
-
<b>{{ featured_items.title }}</b>
|
|
32
|
-
</a>
|
|
33
|
-
<p>by <a href="{{ featured_items.author_link }}">{{ featured_items.author }}</a></p>
|
|
34
|
-
</div>
|
|
35
|
-
{% endfor %}
|
|
36
|
-
</div>
|
|
37
|
-
{% endfor %}
|
|
38
|
-
</div>
|
|
39
|
-
</center>
|
|
40
|
-
{% endblock %}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{structables-0.3.12 → structables-0.3.14}/src/structables/static/dist/css/bootstrap.min.css.map
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{structables-0.3.12 → structables-0.3.14}/src/structables/static/img/magnifying-glass-solid.svg
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{structables-0.3.12 → structables-0.3.14}/src/structables/templates/member-instructables.html
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|