Compare commits
73 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ed409df323 | ||
|
|
b8decafd75 | ||
|
|
5aaee010c1 | ||
|
|
a01fe4043e | ||
|
|
e0ef0e018d | ||
|
|
0210a3e601 | ||
|
|
36000b1592 | ||
|
|
b296b9b299 | ||
|
|
dd6257a0a0 | ||
|
|
23b324cc9c | ||
|
|
f61f9e8654 | ||
|
|
286207ffa2 | ||
|
|
a3e82ad42f | ||
|
|
404200b8f0 | ||
|
|
dfecf470fa | ||
|
|
c737f58fc0 | ||
|
|
ab59b7f4ba | ||
|
|
514a24e2c4 | ||
|
|
742a327cbb | ||
|
|
864e067412 | ||
|
|
1c7a192854 | ||
|
|
c298f64295 | ||
|
|
e82166f87e | ||
|
|
909a2b4ce9 | ||
|
|
df8d05f09d | ||
|
|
8c3b1c8c95 | ||
|
|
ecdb755dd3 | ||
|
|
901e115a21 | ||
|
|
d4c2166019 | ||
|
|
cbc98ef624 | ||
|
|
794bc8a018 | ||
|
|
34900222dc | ||
|
|
f9a1d25c57 | ||
|
|
8fe7bcfb71 | ||
|
|
28ee65809e | ||
|
|
1b42f3310a | ||
|
|
8d2144895e | ||
|
|
13837ce88b | ||
|
|
73c65e3561 | ||
|
|
67a229b8a3 | ||
|
|
9dd3570a52 | ||
|
|
a6c8b12cdd | ||
|
|
7813c766ac | ||
|
|
9fc9826d30 | ||
|
|
19e1ed8b32 | ||
|
|
eb6b1d6375 | ||
|
|
8c6d352d07 | ||
|
|
cfa7654efc | ||
|
|
87af23248e | ||
|
|
ba08becd3a | ||
|
|
68b7a5e922 | ||
|
|
e8cc685f89 | ||
|
|
86dd137f75 | ||
|
|
b48f684c0a | ||
|
|
e0e6f3392d | ||
|
|
b1c349cc35 | ||
|
|
40aac38d43 | ||
|
|
051df7ab87 | ||
|
|
bb1f6702f6 | ||
|
|
c9542427b4 | ||
|
|
8601c5e075 | ||
|
|
3d97eca387 | ||
|
|
99b21f996c | ||
|
|
700cd7ce1f | ||
|
|
8d9da5a750 | ||
|
|
9a36bb7d72 | ||
|
|
e424dc57e7 | ||
|
|
7d60e2f671 | ||
|
|
8b2018852e | ||
|
|
0aeefa2387 | ||
|
|
4420f489ac | ||
|
|
aad4fd2a70 | ||
|
|
d2bda0fded |
@@ -37,5 +37,9 @@ csrf = CsrfProtect(app)
|
|||||||
mail = Mail(app)
|
mail = Mail(app)
|
||||||
pages = FlatPages(app)
|
pages = FlatPages(app)
|
||||||
|
|
||||||
|
if not app.debug:
|
||||||
|
from .maillogger import register_mail_error_handler
|
||||||
|
register_mail_error_handler(app, mail)
|
||||||
|
|
||||||
from . import models, tasks
|
from . import models, tasks
|
||||||
from .views import *
|
from .views import *
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ title: Ranks and Permissions
|
|||||||
<th>N</th>
|
<th>N</th>
|
||||||
<th>Y</th>
|
<th>Y</th>
|
||||||
<th>N</th>
|
<th>N</th>
|
||||||
|
<th>Y</th>
|
||||||
|
<th>N</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -58,7 +60,7 @@ title: Ranks and Permissions
|
|||||||
<th></th>
|
<th></th>
|
||||||
<th></th> <!-- member -->
|
<th></th> <!-- member -->
|
||||||
<th></th>
|
<th></th>
|
||||||
<th>✓</th> <!-- trusted member -->
|
<th></th> <!-- trusted member -->
|
||||||
<th></th>
|
<th></th>
|
||||||
<th>✓</th> <!-- editor -->
|
<th>✓</th> <!-- editor -->
|
||||||
<th>✓</th>
|
<th>✓</th>
|
||||||
@@ -101,7 +103,7 @@ title: Ranks and Permissions
|
|||||||
<td>Approve Screenshot</td>
|
<td>Approve Screenshot</td>
|
||||||
<th></th> <!-- new -->
|
<th></th> <!-- new -->
|
||||||
<th></th>
|
<th></th>
|
||||||
<th>✓</th> <!-- member -->
|
<th></th> <!-- member -->
|
||||||
<th></th>
|
<th></th>
|
||||||
<th>✓</th> <!-- trusted member -->
|
<th>✓</th> <!-- trusted member -->
|
||||||
<th></th>
|
<th></th>
|
||||||
@@ -187,6 +189,21 @@ title: Ranks and Permissions
|
|||||||
<th>✓</th> <!-- admin -->
|
<th>✓</th> <!-- admin -->
|
||||||
<th>✓</th>
|
<th>✓</th>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>See Private Thread</td>
|
||||||
|
<th>✓</th> <!-- new -->
|
||||||
|
<th></th>
|
||||||
|
<th>✓</th> <!-- member -->
|
||||||
|
<th></th>
|
||||||
|
<th>✓</th> <!-- trusted member -->
|
||||||
|
<th></th>
|
||||||
|
<th>✓</th> <!-- editor -->
|
||||||
|
<th>✓</th>
|
||||||
|
<th>✓</th> <!-- moderator -->
|
||||||
|
<th>✓</th>
|
||||||
|
<th>✓</th> <!-- admin -->
|
||||||
|
<th>✓</th>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Set Email</td>
|
<td>Set Email</td>
|
||||||
<th>✓</th> <!-- new -->
|
<th>✓</th> <!-- new -->
|
||||||
|
|||||||
42
app/flatpages/help/wtfpl.md
Normal file
42
app/flatpages/help/wtfpl.md
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
title: WTFPL is a terrible license
|
||||||
|
no_h1: true
|
||||||
|
|
||||||
|
<div id="warning" class="box box_grey alert alert-warning">
|
||||||
|
<span class="icon_message"></span>
|
||||||
|
|
||||||
|
Please reconsider the choice of WTFPL as a license.
|
||||||
|
|
||||||
|
<script src="/static/jquery.min.js"></script>
|
||||||
|
<script>
|
||||||
|
// @author rubenwardy
|
||||||
|
// @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3-or-Later
|
||||||
|
|
||||||
|
var params = new URLSearchParams(location.search);
|
||||||
|
var r = params.get("r");
|
||||||
|
if (r)
|
||||||
|
document.write("<a class='alert_right button' href='" + r + "'>Okay</a>");
|
||||||
|
else
|
||||||
|
$("#warning").hide();
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
# WTFPL is a terrible license
|
||||||
|
|
||||||
|
The use of WTFPL as a license is discouraged for multiple reasons.
|
||||||
|
|
||||||
|
* **No Warranty disclaimer:** This could open you up to being sued.<sup>[1]</sup>
|
||||||
|
* **Swearing:** This prevents settings like schools from using your content.
|
||||||
|
* **Not OSI Approved:** Same as public domain?
|
||||||
|
|
||||||
|
The Open Source Initiative chose not to approve the license as an open-source
|
||||||
|
license, saying:<sup>[3]</sup>
|
||||||
|
|
||||||
|
> It's no different from dedication to the public domain.
|
||||||
|
> Author has submitted license approval request – author is free to make public domain dedication.
|
||||||
|
> Although he agrees with the recommendation, Mr. Michlmayr notes that public domain doesn't exist in Europe. Recommend: Reject.
|
||||||
|
|
||||||
|
## Sources
|
||||||
|
|
||||||
|
1. [WTFPL is harmful to software developers](https://cubicspot.blogspot.com/2017/04/wtfpl-is-harmful-to-software-developers.html)
|
||||||
|
2. [FSF](https://www.gnu.org/licenses/license-list.en.html)
|
||||||
|
3. [OSI](https://opensource.org/minutes20090304)
|
||||||
@@ -4,6 +4,20 @@ title: Package Inclusion Policy and Guidance
|
|||||||
<b>Note:</b> This is a draft
|
<b>Note:</b> This is a draft
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
## 0. Overview
|
||||||
|
|
||||||
|
ContentDB is for the community, and as such listings should be useful to the
|
||||||
|
community. To help with this, there are a few rules to improve the quality of
|
||||||
|
the listings and to combat abuse.
|
||||||
|
|
||||||
|
* No inappropriate content.
|
||||||
|
* Content must be playable/useful, but not necessarily finished.
|
||||||
|
* Don't use the name of another mod unless your mod is a fork or reimplementation.
|
||||||
|
* Licenses must allow derivatives, redistribution, and must not discriminate.
|
||||||
|
* Don't put promotions are advertisements in package listings, except for
|
||||||
|
donation and personal website links which are permitted in the long description.
|
||||||
|
|
||||||
|
|
||||||
## 1. General
|
## 1. General
|
||||||
|
|
||||||
It is not permitted to submit abusive, obscene, vulgar, slanderous, hateful,
|
It is not permitted to submit abusive, obscene, vulgar, slanderous, hateful,
|
||||||
@@ -16,13 +30,23 @@ including ones not covered by this document, and to ban users who abuse this ser
|
|||||||
Also see the [help page on tags](/help/package_tags/).
|
Also see the [help page on tags](/help/package_tags/).
|
||||||
|
|
||||||
|
|
||||||
## 2. State of Completion
|
## 2. Accepted Content and State of Completion
|
||||||
|
|
||||||
ContentDB should only currently contain playable content, ie: stuff that would
|
The submission of malware is strictly prohibited. This includes software which
|
||||||
be in Mod Releases and Game Releases. Please don't upload any Work In Progress (WIP)
|
does not do as it advertises, for example if it posts telemetry without stating
|
||||||
things.
|
clearly that it does in the package meta.
|
||||||
|
|
||||||
This will probably change in future.
|
ContentDB should only currently contain playable content - content which is
|
||||||
|
sufficiently complete to be useful to end users. It's fine to add stuff which
|
||||||
|
is still a work in progress (WIP) as long as it adds sufficient value -
|
||||||
|
Mineclone 2 is a good example of a WIP package which may break between releases
|
||||||
|
but still has value. Note that this doesn't mean that you should add a thing
|
||||||
|
you started working on yesterday, it's worth adding all the basic stuff to
|
||||||
|
make your package useful.
|
||||||
|
|
||||||
|
Adding non-player facing mods, such as libraries and server tools, is perfectly fine
|
||||||
|
and encouraged. ContentDB isn't just for player-facing things, and adding
|
||||||
|
libraries allows them to be installed when a mod depends on it.
|
||||||
|
|
||||||
|
|
||||||
## 3. Technical Names
|
## 3. Technical Names
|
||||||
@@ -32,7 +56,7 @@ This will probably change in future.
|
|||||||
The first package to use a name based on the creation of its forum topic or
|
The first package to use a name based on the creation of its forum topic or
|
||||||
contentdb submission has the right to the technical name. The use of a package
|
contentdb submission has the right to the technical name. The use of a package
|
||||||
on a server or in private doesn't reserve its name. No other packages of the same
|
on a server or in private doesn't reserve its name. No other packages of the same
|
||||||
type may use the same name, except for the exception given by 2.2.
|
type may use the same name, except for the exception given by 3.2.
|
||||||
|
|
||||||
If it turns out that we made a mistake by approving a package and that the
|
If it turns out that we made a mistake by approving a package and that the
|
||||||
name should have been given to another package, then we *may* unapprove the
|
name should have been given to another package, then we *may* unapprove the
|
||||||
@@ -41,6 +65,8 @@ package and give the name to the correct one.
|
|||||||
If you submit a package where you don't have the right to the name you will be asked
|
If you submit a package where you don't have the right to the name you will be asked
|
||||||
to change the name of the package, or your package won't be accepted.
|
to change the name of the package, or your package won't be accepted.
|
||||||
|
|
||||||
|
We reserve the right to issue exceptions for this where we feel necessary.
|
||||||
|
|
||||||
### 3.2 Mod Forks and Reimplementations
|
### 3.2 Mod Forks and Reimplementations
|
||||||
|
|
||||||
An exception to the above is that mods are allowed to have the same name as a
|
An exception to the above is that mods are allowed to have the same name as a
|
||||||
@@ -59,33 +85,43 @@ Please ensure that you correctly credit any resources (code, assets, or otherwis
|
|||||||
that you have used in your package.
|
that you have used in your package.
|
||||||
|
|
||||||
**The use of licenses which do not allow derivatives or redistribution is not
|
**The use of licenses which do not allow derivatives or redistribution is not
|
||||||
permitted. This includes CC-ND (No-Derivatives) and lots of closed source licenses.**
|
permitted. This includes CC-ND (No-Derivatives) and lots of closed source licenses.
|
||||||
|
The use of licenses which discriminate between groups of people or forbid the use
|
||||||
|
of the content on servers or singleplayer is also not permitted.**
|
||||||
|
|
||||||
However, closed sourced licenses are allowed if they allow the above.
|
However, closed sourced licenses are allowed if they allow the above.
|
||||||
|
|
||||||
If the license you use is not on the list then please choose the correct "Other"
|
If the license you use is not on the list then please select "Other", and we'll
|
||||||
option.
|
get around to adding it.
|
||||||
|
|
||||||
Please note that the definitions of "free" and "non-free" is the same as that
|
Please note that the definitions of "free" and "non-free" is the same as that
|
||||||
of the [Free Software Foundation](https://www.gnu.org/philosophy/free-sw.en.html).
|
of the [Free Software Foundation](https://www.gnu.org/philosophy/free-sw.en.html).
|
||||||
|
|
||||||
### 4.2 Recommended Licenses
|
### 4.2 Recommended Licenses
|
||||||
|
|
||||||
|
It is highly recommended that you use a free and open source software license.
|
||||||
|
FOSS licenses result in a sharing community and will increase the number of potential users your package has.
|
||||||
|
Using a closed source license will result in your package being massively penalised in the search results and package lists.
|
||||||
|
|
||||||
It is recommended that you use a proper license for code with a warranty
|
It is recommended that you use a proper license for code with a warranty
|
||||||
disclaimer, such as the (L)GPL or MIT. You should also use a proper media license
|
disclaimer, such as the (L)GPL or MIT. You should also use a proper media license
|
||||||
for media, such as a Creative Commons license.
|
for media, such as a Creative Commons license.
|
||||||
|
|
||||||
The use of WTFPL is discouraged as it doesn't contain a valid warranty disclaimer,
|
The use of WTFPL is discouraged as it doesn't contain a [valid warranty disclaimer](https://cubicspot.blogspot.com/2017/04/wtfpl-is-harmful-to-software-developers.html),
|
||||||
and also includes swearing which dissuades teachers from using your content.
|
and also includes swearing which prevents settings like schools from using your content.
|
||||||
|
[Read more](/help/wtfpl/).
|
||||||
|
|
||||||
Public domain is not a valid license in many countries, please use CC0 or MIT instead.
|
Public domain is not a valid license in many countries, please use CC0 or MIT instead.
|
||||||
|
|
||||||
|
|
||||||
## 5. Advertisements (inc. asking for donations)
|
## 5. Promotions and Advertisements (inc. asking for donations)
|
||||||
|
|
||||||
Any other information than the long description - including screenshots - must
|
Any information other than the long description - including screenshots - must
|
||||||
not contain any promotions or advertisements. This includes asking for donations
|
not contain any promotions or advertisements. This includes asking for donations,
|
||||||
or promoting online market stores.
|
promoting online shops, or linking to personal websites and social media.
|
||||||
|
|
||||||
|
ContentDB is for the community. We may remove any promotions if we feel that
|
||||||
|
they're inappropriate.
|
||||||
|
|
||||||
Paid promotions are not allowed at all, anywhere.
|
Paid promotions are not allowed at all, anywhere.
|
||||||
|
|
||||||
|
|||||||
109
app/maillogger.py
Normal file
109
app/maillogger.py
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import logging
|
||||||
|
from enum import Enum
|
||||||
|
from app.tasks.emails import sendEmailRaw
|
||||||
|
|
||||||
|
def _has_newline(line):
|
||||||
|
"""Used by has_bad_header to check for \\r or \\n"""
|
||||||
|
if line and ("\r" in line or "\n" in line):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _is_bad_subject(subject):
|
||||||
|
"""Copied from: flask_mail.py class Message def has_bad_headers"""
|
||||||
|
if _has_newline(subject):
|
||||||
|
for linenum, line in enumerate(subject.split("\r\n")):
|
||||||
|
if not line:
|
||||||
|
return True
|
||||||
|
if linenum > 0 and line[0] not in "\t ":
|
||||||
|
return True
|
||||||
|
if _has_newline(line):
|
||||||
|
return True
|
||||||
|
if len(line.strip()) == 0:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class FlaskMailSubjectFormatter(logging.Formatter):
|
||||||
|
def format(self, record):
|
||||||
|
record.message = record.getMessage()
|
||||||
|
if self.usesTime():
|
||||||
|
record.asctime = self.formatTime(record, self.datefmt)
|
||||||
|
s = self.formatMessage(record)
|
||||||
|
return s
|
||||||
|
|
||||||
|
class FlaskMailTextFormatter(logging.Formatter):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# TODO: hier nog niet tevreden over (vooral logger.error(..., exc_info, stack_info))
|
||||||
|
class FlaskMailHTMLFormatter(logging.Formatter):
|
||||||
|
pre_template = "<h1>%s</h1><pre>%s</pre>"
|
||||||
|
def formatException(self, exc_info):
|
||||||
|
formatted_exception = logging.Handler.formatException(self, exc_info)
|
||||||
|
return FlaskMailHTMLFormatter.pre_template % ("Exception information", formatted_exception)
|
||||||
|
def formatStack(self, stack_info):
|
||||||
|
return FlaskMailHTMLFormatter.pre_template % ("<h1>Stack information</h1><pre>%s</pre>", stack_info)
|
||||||
|
|
||||||
|
|
||||||
|
# see: https://github.com/python/cpython/blob/3.6/Lib/logging/__init__.py (class Handler)
|
||||||
|
|
||||||
|
class FlaskMailHandler(logging.Handler):
|
||||||
|
def __init__(self, mailer, subject_template, level=logging.NOTSET):
|
||||||
|
logging.Handler.__init__(self, level)
|
||||||
|
self.mailer = mailer
|
||||||
|
self.send_to = mailer.app.config["MAIL_UTILS_ERROR_SEND_TO"]
|
||||||
|
self.subject_template = subject_template
|
||||||
|
self.html_formatter = None
|
||||||
|
|
||||||
|
def setFormatter(self, text_fmt, html_fmt=None):
|
||||||
|
"""
|
||||||
|
Set the formatters for this handler. Provide at least one formatter.
|
||||||
|
When no text_fmt is provided, no text-part is created for the email body.
|
||||||
|
"""
|
||||||
|
assert (text_fmt, html_fmt) != (None, None), "At least one formatter should be provided"
|
||||||
|
if type(text_fmt)==str:
|
||||||
|
text_fmt = FlaskMailTextFormatter(text_fmt)
|
||||||
|
self.formatter = text_fmt
|
||||||
|
if type(html_fmt)==str:
|
||||||
|
html_fmt = FlaskMailHTMLFormatter(html_fmt)
|
||||||
|
self.html_formatter = html_fmt
|
||||||
|
|
||||||
|
def getSubject(self, record):
|
||||||
|
fmt = FlaskMailSubjectFormatter(self.subject_template)
|
||||||
|
subject = fmt.format(record)
|
||||||
|
#Since templates can cause header problems, and we rather have a incomplete email then an error, we fix this
|
||||||
|
if _is_bad_subject(subject):
|
||||||
|
subject="FlaskMailHandler log-entry from %s [original subject is replaced, because it would result in a bad header]" % self.mailer.app.name
|
||||||
|
return subject
|
||||||
|
|
||||||
|
def emit(self, record):
|
||||||
|
text = self.format(record) if self.formatter else None
|
||||||
|
html = self.html_formatter.format(record) if self.html_formatter else None
|
||||||
|
sendEmailRaw.delay(self.send_to, self.getSubject(record), text, html)
|
||||||
|
|
||||||
|
|
||||||
|
def register_mail_error_handler(app, mailer):
|
||||||
|
subject_template = "ContentDB crashed (%(module)s > %(funcName)s)"
|
||||||
|
text_template = """
|
||||||
|
Message type: %(levelname)s
|
||||||
|
Location: %(pathname)s:%(lineno)d
|
||||||
|
Module: %(module)s
|
||||||
|
Function: %(funcName)s
|
||||||
|
Time: %(asctime)s
|
||||||
|
Message:
|
||||||
|
%(message)s"""
|
||||||
|
html_template = """
|
||||||
|
<style>th { text-align: right}</style><table>
|
||||||
|
<tr><th>Message type:</th><td>%(levelname)s</td></tr>
|
||||||
|
<tr> <th>Location:</th><td>%(pathname)s:%(lineno)d</td></tr>
|
||||||
|
<tr> <th>Module:</th><td>%(module)s</td></tr>
|
||||||
|
<tr> <th>Function:</th><td>%(funcName)s</td></tr>
|
||||||
|
<tr> <th>Time:</th><td>%(asctime)s</td></tr>
|
||||||
|
</table>
|
||||||
|
<h2>Message</h2>
|
||||||
|
<pre>%(message)s</pre>"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
mail_handler = FlaskMailHandler(mailer, subject_template)
|
||||||
|
mail_handler.setLevel(logging.ERROR)
|
||||||
|
mail_handler.setFormatter(text_template, html_template)
|
||||||
|
app.logger.addHandler(mail_handler)
|
||||||
219
app/models.py
219
app/models.py
@@ -76,6 +76,8 @@ class Permission(enum.Enum):
|
|||||||
CHANGE_RANK = "CHANGE_RANK"
|
CHANGE_RANK = "CHANGE_RANK"
|
||||||
CHANGE_EMAIL = "CHANGE_EMAIL"
|
CHANGE_EMAIL = "CHANGE_EMAIL"
|
||||||
EDIT_EDITREQUEST = "EDIT_EDITREQUEST"
|
EDIT_EDITREQUEST = "EDIT_EDITREQUEST"
|
||||||
|
SEE_THREAD = "SEE_THREAD"
|
||||||
|
CREATE_THREAD = "CREATE_THREAD"
|
||||||
|
|
||||||
# Only return true if the permission is valid for *all* contexts
|
# Only return true if the permission is valid for *all* contexts
|
||||||
# See Package.checkPerm for package-specific contexts
|
# See Package.checkPerm for package-specific contexts
|
||||||
@@ -84,14 +86,14 @@ class Permission(enum.Enum):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
if self == Permission.APPROVE_NEW or \
|
if self == Permission.APPROVE_NEW or \
|
||||||
self == Permission.APPROVE_CHANGES or \
|
self == Permission.APPROVE_CHANGES or \
|
||||||
self == Permission.APPROVE_RELEASE or \
|
self == Permission.APPROVE_RELEASE or \
|
||||||
self == Permission.APPROVE_SCREENSHOT:
|
self == Permission.APPROVE_SCREENSHOT or \
|
||||||
|
self == Permission.SEE_THREAD:
|
||||||
return user.rank.atLeast(UserRank.EDITOR)
|
return user.rank.atLeast(UserRank.EDITOR)
|
||||||
else:
|
else:
|
||||||
raise Exception("Non-global permission checked globally. Use Package.checkPerm or User.checkPerm instead.")
|
raise Exception("Non-global permission checked globally. Use Package.checkPerm or User.checkPerm instead.")
|
||||||
|
|
||||||
|
|
||||||
class User(db.Model, UserMixin):
|
class User(db.Model, UserMixin):
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
|
||||||
@@ -120,6 +122,8 @@ class User(db.Model, UserMixin):
|
|||||||
# causednotifs = db.relationship("Notification", backref="causer", lazy="dynamic")
|
# causednotifs = db.relationship("Notification", backref="causer", lazy="dynamic")
|
||||||
packages = db.relationship("Package", backref="author", lazy="dynamic")
|
packages = db.relationship("Package", backref="author", lazy="dynamic")
|
||||||
requests = db.relationship("EditRequest", backref="author", lazy="dynamic")
|
requests = db.relationship("EditRequest", backref="author", lazy="dynamic")
|
||||||
|
threads = db.relationship("Thread", backref="author", lazy="dynamic")
|
||||||
|
replies = db.relationship("ThreadReply", backref="author", lazy="dynamic")
|
||||||
|
|
||||||
def __init__(self, username, active=False, email=None, password=None):
|
def __init__(self, username, active=False, email=None, password=None):
|
||||||
import datetime
|
import datetime
|
||||||
@@ -207,6 +211,13 @@ class PackageType(enum.Enum):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get(cls, name):
|
||||||
|
try:
|
||||||
|
return PackageType[name.upper()]
|
||||||
|
except KeyError:
|
||||||
|
return None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def choices(cls):
|
def choices(cls):
|
||||||
return [(choice, choice.value) for choice in cls]
|
return [(choice, choice.value) for choice in cls]
|
||||||
@@ -337,6 +348,11 @@ class Package(db.Model):
|
|||||||
approved = db.Column(db.Boolean, nullable=False, default=False)
|
approved = db.Column(db.Boolean, nullable=False, default=False)
|
||||||
soft_deleted = db.Column(db.Boolean, nullable=False, default=False)
|
soft_deleted = db.Column(db.Boolean, nullable=False, default=False)
|
||||||
|
|
||||||
|
score = db.Column(db.Float, nullable=False, default=0)
|
||||||
|
|
||||||
|
review_thread_id = db.Column(db.Integer, db.ForeignKey("thread.id"), nullable=True, default=None)
|
||||||
|
review_thread = db.relationship("Thread", foreign_keys=[review_thread_id])
|
||||||
|
|
||||||
# Downloads
|
# Downloads
|
||||||
repo = db.Column(db.String(200), nullable=True)
|
repo = db.Column(db.String(200), nullable=True)
|
||||||
website = db.Column(db.String(200), nullable=True)
|
website = db.Column(db.String(200), nullable=True)
|
||||||
@@ -371,7 +387,7 @@ class Package(db.Model):
|
|||||||
for e in PackagePropertyKey:
|
for e in PackagePropertyKey:
|
||||||
setattr(self, e.name, getattr(package, e.name))
|
setattr(self, e.name, getattr(package, e.name))
|
||||||
|
|
||||||
def getAsDictionary(self, base_url):
|
def getAsDictionaryShort(self, base_url):
|
||||||
tnurl = self.getThumbnailURL()
|
tnurl = self.getThumbnailURL()
|
||||||
return {
|
return {
|
||||||
"name": self.name,
|
"name": self.name,
|
||||||
@@ -379,12 +395,38 @@ class Package(db.Model):
|
|||||||
"author": self.author.display_name,
|
"author": self.author.display_name,
|
||||||
"shortDesc": self.shortDesc,
|
"shortDesc": self.shortDesc,
|
||||||
"type": self.type.toName(),
|
"type": self.type.toName(),
|
||||||
|
"release": self.getDownloadRelease().id if self.getDownloadRelease() is not None else None,
|
||||||
|
"thumbnail": (base_url + tnurl) if tnurl is not None else None,
|
||||||
|
"score": round(self.score * 10) / 10
|
||||||
|
}
|
||||||
|
|
||||||
|
def getAsDictionary(self, base_url):
|
||||||
|
tnurl = self.getThumbnailURL()
|
||||||
|
return {
|
||||||
|
"author": self.author.display_name,
|
||||||
|
"name": self.name,
|
||||||
|
"title": self.title,
|
||||||
|
"shortDesc": self.shortDesc,
|
||||||
|
"desc": self.desc,
|
||||||
|
"type": self.type.toName(),
|
||||||
|
"createdAt": self.created_at,
|
||||||
|
|
||||||
"license": self.license.name,
|
"license": self.license.name,
|
||||||
|
"mediaLicense": self.media_license.name,
|
||||||
|
|
||||||
"repo": self.repo,
|
"repo": self.repo,
|
||||||
|
"website": self.website,
|
||||||
|
"issueTracker": self.issueTracker,
|
||||||
|
"forums": self.forums,
|
||||||
|
|
||||||
|
"provides": [x.name for x in self.provides],
|
||||||
|
"thumbnail": (base_url + tnurl) if tnurl is not None else None,
|
||||||
|
"screenshots": [base_url + ss.url for ss in self.screenshots],
|
||||||
|
|
||||||
"url": base_url + self.getDownloadURL(),
|
"url": base_url + self.getDownloadURL(),
|
||||||
"release": self.getDownloadRelease().id if self.getDownloadRelease() is not None else None,
|
"release": self.getDownloadRelease().id if self.getDownloadRelease() is not None else None,
|
||||||
"screenshots": [base_url + ss.url for ss in self.screenshots],
|
|
||||||
"thumbnail": (base_url + tnurl) if tnurl is not None else None
|
"score": round(self.score * 10) / 10
|
||||||
}
|
}
|
||||||
|
|
||||||
def getThumbnailURL(self):
|
def getThumbnailURL(self):
|
||||||
@@ -434,26 +476,6 @@ class Package(db.Model):
|
|||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def canImportScreenshot(self):
|
|
||||||
if self.repo is None:
|
|
||||||
return False
|
|
||||||
|
|
||||||
url = urlparse(self.repo)
|
|
||||||
if url.netloc == "github.com":
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
def canMakeReleaseFromVCS(self):
|
|
||||||
if self.repo is None:
|
|
||||||
return False
|
|
||||||
|
|
||||||
url = urlparse(self.repo)
|
|
||||||
if url.netloc == "github.com":
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
def checkPerm(self, user, perm):
|
def checkPerm(self, user, perm):
|
||||||
if not user.is_authenticated:
|
if not user.is_authenticated:
|
||||||
return False
|
return False
|
||||||
@@ -466,7 +488,7 @@ class Package(db.Model):
|
|||||||
isOwner = user == self.author
|
isOwner = user == self.author
|
||||||
|
|
||||||
# Members can edit their own packages, and editors can edit any packages
|
# Members can edit their own packages, and editors can edit any packages
|
||||||
if perm == Permission.MAKE_RELEASE or perm == Permission.ADD_SCREENSHOTS:
|
if perm == Permission.MAKE_RELEASE or perm == Permission.ADD_SCREENSHOTS or perm == Permission.CREATE_THREAD:
|
||||||
return isOwner or user.rank.atLeast(UserRank.EDITOR)
|
return isOwner or user.rank.atLeast(UserRank.EDITOR)
|
||||||
|
|
||||||
if perm == Permission.EDIT_PACKAGE or perm == Permission.APPROVE_CHANGES:
|
if perm == Permission.EDIT_PACKAGE or perm == Permission.APPROVE_CHANGES:
|
||||||
@@ -475,12 +497,11 @@ class Package(db.Model):
|
|||||||
else:
|
else:
|
||||||
return user.rank.atLeast(UserRank.EDITOR)
|
return user.rank.atLeast(UserRank.EDITOR)
|
||||||
|
|
||||||
# Editors can change authors
|
# Editors can change authors and approve new packages
|
||||||
elif perm == Permission.CHANGE_AUTHOR:
|
elif perm == Permission.APPROVE_NEW or perm == Permission.CHANGE_AUTHOR:
|
||||||
return user.rank.atLeast(UserRank.EDITOR)
|
return user.rank.atLeast(UserRank.EDITOR)
|
||||||
|
|
||||||
elif perm == Permission.APPROVE_NEW or perm == Permission.APPROVE_RELEASE \
|
elif perm == Permission.APPROVE_RELEASE or perm == Permission.APPROVE_SCREENSHOT:
|
||||||
or perm == Permission.APPROVE_SCREENSHOT:
|
|
||||||
return user.rank.atLeast(UserRank.TRUSTED_MEMBER if isOwner else UserRank.EDITOR)
|
return user.rank.atLeast(UserRank.TRUSTED_MEMBER if isOwner else UserRank.EDITOR)
|
||||||
|
|
||||||
# Moderators can delete packages
|
# Moderators can delete packages
|
||||||
@@ -490,6 +511,25 @@ class Package(db.Model):
|
|||||||
else:
|
else:
|
||||||
raise Exception("Permission {} is not related to packages".format(perm.name))
|
raise Exception("Permission {} is not related to packages".format(perm.name))
|
||||||
|
|
||||||
|
def recalcScore(self):
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
self.score = 10
|
||||||
|
|
||||||
|
if self.forums is not None:
|
||||||
|
topic = ForumTopic.query.get(self.forums)
|
||||||
|
if topic:
|
||||||
|
days = (datetime.datetime.now() - topic.created_at).days
|
||||||
|
months = days / 30
|
||||||
|
years = days / 365
|
||||||
|
self.score = topic.views / max(years, 0.0416) + 80*min(max(months, 0.5), 6)
|
||||||
|
|
||||||
|
if self.getMainScreenshotURL() is None:
|
||||||
|
self.score *= 0.8
|
||||||
|
|
||||||
|
if not self.license.is_foss or not self.media_license.is_foss:
|
||||||
|
self.score *= 0.1
|
||||||
|
|
||||||
class MetaPackage(db.Model):
|
class MetaPackage(db.Model):
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
name = db.Column(db.String(100), unique=True, nullable=False)
|
name = db.Column(db.String(100), unique=True, nullable=False)
|
||||||
@@ -563,6 +603,7 @@ class PackageRelease(db.Model):
|
|||||||
url = db.Column(db.String(200), nullable=False)
|
url = db.Column(db.String(200), nullable=False)
|
||||||
approved = db.Column(db.Boolean, nullable=False, default=False)
|
approved = db.Column(db.Boolean, nullable=False, default=False)
|
||||||
task_id = db.Column(db.String(37), nullable=True)
|
task_id = db.Column(db.String(37), nullable=True)
|
||||||
|
commit_hash = db.Column(db.String(41), nullable=True, default=None)
|
||||||
|
|
||||||
|
|
||||||
def getEditURL(self):
|
def getEditURL(self):
|
||||||
@@ -571,9 +612,24 @@ class PackageRelease(db.Model):
|
|||||||
name=self.package.name,
|
name=self.package.name,
|
||||||
id=self.id)
|
id=self.id)
|
||||||
|
|
||||||
|
def getDownloadURL(self):
|
||||||
|
return url_for("download_release_page",
|
||||||
|
author=self.package.author.username,
|
||||||
|
name=self.package.name,
|
||||||
|
id=self.id)
|
||||||
|
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.releaseDate = datetime.now()
|
self.releaseDate = datetime.now()
|
||||||
|
|
||||||
|
|
||||||
|
class PackageReview(db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
package_id = db.Column(db.Integer, db.ForeignKey("package.id"))
|
||||||
|
thread_id = db.Column(db.Integer, db.ForeignKey("thread.id"), nullable=False)
|
||||||
|
recommend = db.Column(db.Boolean, nullable=False, default=True)
|
||||||
|
|
||||||
|
|
||||||
class PackageScreenshot(db.Model):
|
class PackageScreenshot(db.Model):
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
package_id = db.Column(db.Integer, db.ForeignKey("package.id"))
|
package_id = db.Column(db.Integer, db.ForeignKey("package.id"))
|
||||||
@@ -679,22 +735,107 @@ class EditRequestChange(db.Model):
|
|||||||
setattr(package, self.key.name, self.newValue)
|
setattr(package, self.key.name, self.newValue)
|
||||||
|
|
||||||
|
|
||||||
|
watchers = db.Table("watchers",
|
||||||
|
db.Column("user_id", db.Integer, db.ForeignKey("user.id"), primary_key=True),
|
||||||
|
db.Column("thread_id", db.Integer, db.ForeignKey("thread.id"), primary_key=True)
|
||||||
|
)
|
||||||
|
|
||||||
class KrockForumTopic(db.Model):
|
class Thread(db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
|
||||||
|
package_id = db.Column(db.Integer, db.ForeignKey("package.id"), nullable=True)
|
||||||
|
package = db.relationship("Package", foreign_keys=[package_id])
|
||||||
|
|
||||||
|
author_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
|
||||||
|
title = db.Column(db.String(100), nullable=False)
|
||||||
|
private = db.Column(db.Boolean, server_default="0")
|
||||||
|
|
||||||
|
created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
|
||||||
|
|
||||||
|
replies = db.relationship("ThreadReply", backref="thread", lazy="dynamic")
|
||||||
|
|
||||||
|
watchers = db.relationship("User", secondary=watchers, lazy="subquery", \
|
||||||
|
backref=db.backref("watching", lazy=True))
|
||||||
|
|
||||||
|
|
||||||
|
def getSubscribeURL(self):
|
||||||
|
return url_for("thread_subscribe_page",
|
||||||
|
id=self.id)
|
||||||
|
|
||||||
|
def getUnsubscribeURL(self):
|
||||||
|
return url_for("thread_unsubscribe_page",
|
||||||
|
id=self.id)
|
||||||
|
|
||||||
|
def checkPerm(self, user, perm):
|
||||||
|
if not user.is_authenticated:
|
||||||
|
return not self.private
|
||||||
|
|
||||||
|
if type(perm) == str:
|
||||||
|
perm = Permission[perm]
|
||||||
|
elif type(perm) != Permission:
|
||||||
|
raise Exception("Unknown permission given to Thread.checkPerm()")
|
||||||
|
|
||||||
|
isOwner = user == self.author or (self.package is not None and self.package.author == user)
|
||||||
|
|
||||||
|
if perm == Permission.SEE_THREAD:
|
||||||
|
return not self.private or isOwner or user.rank.atLeast(UserRank.EDITOR)
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise Exception("Permission {} is not related to threads".format(perm.name))
|
||||||
|
|
||||||
|
class ThreadReply(db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
thread_id = db.Column(db.Integer, db.ForeignKey("thread.id"), nullable=False)
|
||||||
|
comment = db.Column(db.String(500), nullable=False)
|
||||||
|
author_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
|
||||||
|
created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
|
||||||
|
|
||||||
|
|
||||||
|
REPO_BLACKLIST = [".zip", "mediafire.com", "dropbox.com", "weebly.com", \
|
||||||
|
"minetest.net", "dropboxusercontent.com", "4shared.com", \
|
||||||
|
"digitalaudioconcepts.com", "hg.intevation.org", "www.wtfpl.net", \
|
||||||
|
"imageshack.com", "imgur.com"]
|
||||||
|
|
||||||
|
class ForumTopic(db.Model):
|
||||||
topic_id = db.Column(db.Integer, primary_key=True, autoincrement=False)
|
topic_id = db.Column(db.Integer, primary_key=True, autoincrement=False)
|
||||||
author_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
|
author_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
|
||||||
author = db.relationship("User")
|
author = db.relationship("User")
|
||||||
|
|
||||||
ttype = db.Column(db.Integer, nullable=False)
|
wip = db.Column(db.Boolean, server_default="0")
|
||||||
|
|
||||||
|
type = db.Column(db.Enum(PackageType), nullable=False)
|
||||||
title = db.Column(db.String(200), nullable=False)
|
title = db.Column(db.String(200), nullable=False)
|
||||||
name = db.Column(db.String(30), nullable=True)
|
name = db.Column(db.String(30), nullable=True)
|
||||||
link = db.Column(db.String(200), nullable=True)
|
link = db.Column(db.String(200), nullable=True)
|
||||||
|
|
||||||
def getType(self):
|
posts = db.Column(db.Integer, nullable=False)
|
||||||
if self.ttype == 1 or self.ttype == 2:
|
views = db.Column(db.Integer, nullable=False)
|
||||||
return PackageType.MOD
|
|
||||||
elif self.ttype == 6:
|
created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
|
||||||
return PackageType.GAME
|
|
||||||
|
def getRepoURL(self):
|
||||||
|
if self.link is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
for item in REPO_BLACKLIST:
|
||||||
|
if item in self.link:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return self.link.replace("repo.or.cz/w/", "repo.or.cz/")
|
||||||
|
|
||||||
|
def getAsDictionary(self):
|
||||||
|
return {
|
||||||
|
"author": self.author.username,
|
||||||
|
"name": self.name,
|
||||||
|
"type": self.type.toName(),
|
||||||
|
"title": self.title,
|
||||||
|
"id": self.topic_id,
|
||||||
|
"link": self.link,
|
||||||
|
"posts": self.posts,
|
||||||
|
"views": self.views,
|
||||||
|
"is_wip": self.wip,
|
||||||
|
"created_at": self.created_at.isoformat(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# Setup Flask-User
|
# Setup Flask-User
|
||||||
|
|||||||
@@ -9,19 +9,11 @@ $(function() {
|
|||||||
$(".pkg_meta").show()
|
$(".pkg_meta").show()
|
||||||
}
|
}
|
||||||
|
|
||||||
function repoIsSupported(url) {
|
|
||||||
try {
|
|
||||||
return URI(url).hostname() == "github.com"
|
|
||||||
} catch(e) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$(".pkg_meta").hide()
|
$(".pkg_meta").hide()
|
||||||
$(".pkg_wiz_1").show()
|
$(".pkg_wiz_1").show()
|
||||||
$("#pkg_wiz_1_next").click(function() {
|
$("#pkg_wiz_1_next").click(function() {
|
||||||
const repoURL = $("#repo").val();
|
const repoURL = $("#repo").val();
|
||||||
if (repoIsSupported(repoURL)) {
|
if (repoURL.trim() != "") {
|
||||||
$(".pkg_wiz_1").hide()
|
$(".pkg_wiz_1").hide()
|
||||||
$(".pkg_wiz_2").show()
|
$(".pkg_wiz_2").show()
|
||||||
$(".pkg_repo").hide()
|
$(".pkg_repo").hide()
|
||||||
@@ -35,19 +27,24 @@ $(function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
performTask("/tasks/getmeta/new/?url=" + encodeURI(repoURL)).then(function(result) {
|
performTask("/tasks/getmeta/new/?url=" + encodeURI(repoURL)).then(function(result) {
|
||||||
$("#name").val(result.name || "")
|
$("#name").val(result.name)
|
||||||
setSpecial("#provides_str", result.name || "")
|
setSpecial("#provides_str", result.provides)
|
||||||
$("#title").val(result.title || "")
|
$("#title").val(result.title)
|
||||||
$("#repo").val(result.repo || repoURL)
|
$("#repo").val(result.repo || repoURL)
|
||||||
$("#issueTracker").val(result.issueTracker || "")
|
$("#issueTracker").val(result.issueTracker)
|
||||||
$("#desc").val(result.description || "")
|
$("#desc").val(result.description)
|
||||||
$("#shortDesc").val(result.short_description || "")
|
$("#shortDesc").val(result.short_description)
|
||||||
setSpecial("#harddep_str", result.depends || "")
|
setSpecial("#harddep_str", result.depends)
|
||||||
setSpecial("#softdep_str", result.optional_depends || "")
|
setSpecial("#softdep_str", result.optional_depends)
|
||||||
$("#shortDesc").val(result.short_description || "")
|
$("#shortDesc").val(result.short_description)
|
||||||
if (result.forumId) {
|
if (result.forumId) {
|
||||||
$("#forums").val(result.forumId)
|
$("#forums").val(result.forumId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (result.type && result.type.length > 2) {
|
||||||
|
$("#type").val(result.type)
|
||||||
|
}
|
||||||
|
|
||||||
finish()
|
finish()
|
||||||
}).catch(function(e) {
|
}).catch(function(e) {
|
||||||
alert(e)
|
alert(e)
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ function pollTask(poll_url, disableTimeout) {
|
|||||||
var tries = 0;
|
var tries = 0;
|
||||||
function retry() {
|
function retry() {
|
||||||
tries++;
|
tries++;
|
||||||
if (!disableTimeout && tries > 10) {
|
if (!disableTimeout && tries > 30) {
|
||||||
reject("timeout")
|
reject("timeout")
|
||||||
} else {
|
} else {
|
||||||
const interval = Math.min(tries*100, 1000)
|
const interval = Math.min(tries*100, 1000)
|
||||||
|
|||||||
49
app/scss/comments.scss
Normal file
49
app/scss/comments.scss
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
.comments, .comments li {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
border: 1px solid #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comments {
|
||||||
|
border-radius: 5px;
|
||||||
|
margin: 15px 0;
|
||||||
|
background: #333;
|
||||||
|
|
||||||
|
.info_strip, .msg {
|
||||||
|
display: block;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info_strip {
|
||||||
|
padding: 0.2em 1em;
|
||||||
|
border-bottom: 1px solid #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msg {
|
||||||
|
padding: 1em;
|
||||||
|
background: #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
.author {
|
||||||
|
font-weight: bold;
|
||||||
|
float: left;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info_strip span {
|
||||||
|
float: right;
|
||||||
|
display: inline-block;
|
||||||
|
color: #bbb;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment_form {
|
||||||
|
margin: 1em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment_form textarea {
|
||||||
|
min-width: 60%;
|
||||||
|
max-width: 100%;
|
||||||
|
margin: 0 0 1em 0;
|
||||||
|
}
|
||||||
@@ -99,6 +99,10 @@ a:hover {
|
|||||||
font-size: 100%;
|
font-size: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
select > * {
|
||||||
|
color: #222;
|
||||||
|
}
|
||||||
|
|
||||||
input[type=text], input[type=password], textarea, select, .bulletselector {
|
input[type=text], input[type=password], textarea, select, .bulletselector {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
@@ -315,6 +319,11 @@ select:not([multiple]) {
|
|||||||
border: 1px solid #c96;
|
border: 1px solid #c96;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.alert-primary {
|
||||||
|
background: #339;
|
||||||
|
border: 1px solid #66a;
|
||||||
|
}
|
||||||
|
|
||||||
.alert-success {
|
.alert-success {
|
||||||
background: #161;
|
background: #161;
|
||||||
border: 1px solid #393;
|
border: 1px solid #393;
|
||||||
@@ -446,3 +455,7 @@ table.fancyTable tfoot td {
|
|||||||
.table-topalign td {
|
.table-topalign td {
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wiptopic a {
|
||||||
|
color: #7ac;
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,3 +3,4 @@
|
|||||||
@import "nav.scss";
|
@import "nav.scss";
|
||||||
@import "packages.scss";
|
@import "packages.scss";
|
||||||
@import "packagegrid.scss";
|
@import "packagegrid.scss";
|
||||||
|
@import "comments.scss";
|
||||||
|
|||||||
@@ -22,7 +22,17 @@ from app.tasks import celery
|
|||||||
|
|
||||||
@celery.task()
|
@celery.task()
|
||||||
def sendVerifyEmail(newEmail, token):
|
def sendVerifyEmail(newEmail, token):
|
||||||
msg = Message("Verify email address", recipients=[newEmail])
|
msg = Message("Verify email address", recipients=[newEmail])
|
||||||
msg.body = "This is a verification email!"
|
msg.body = "This is a verification email!"
|
||||||
msg.html = render_template("emails/verify.html", token=token)
|
msg.html = render_template("emails/verify.html", token=token)
|
||||||
mail.send(msg)
|
mail.send(msg)
|
||||||
|
|
||||||
|
@celery.task()
|
||||||
|
def sendEmailRaw(to, subject, text, html):
|
||||||
|
from flask_mail import Message
|
||||||
|
msg = Message(subject, recipients=to)
|
||||||
|
if text:
|
||||||
|
msg.body = text
|
||||||
|
if html:
|
||||||
|
msg.html = html
|
||||||
|
mail.send(msg)
|
||||||
|
|||||||
@@ -15,12 +15,12 @@
|
|||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
import flask, json
|
import flask, json, re
|
||||||
from flask.ext.sqlalchemy import SQLAlchemy
|
from flask.ext.sqlalchemy import SQLAlchemy
|
||||||
from app import app
|
from app import app
|
||||||
from app.models import *
|
from app.models import *
|
||||||
from app.tasks import celery
|
from app.tasks import celery
|
||||||
from .phpbbparser import getProfile
|
from .phpbbparser import getProfile, getTopicsFromForum
|
||||||
import urllib.request
|
import urllib.request
|
||||||
from urllib.parse import urlparse, quote_plus
|
from urllib.parse import urlparse, quote_plus
|
||||||
|
|
||||||
@@ -51,71 +51,91 @@ def checkForumAccount(username, token=None):
|
|||||||
if needsSaving:
|
if needsSaving:
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
@celery.task()
|
|
||||||
def importUsersFromModList():
|
regex_tag = re.compile(r"\[([a-z0-9_]+)\]")
|
||||||
|
BANNED_NAMES = ["mod", "game", "old", "outdated", "wip", "api", "beta", "alpha", "git"]
|
||||||
|
def getNameFromTaglist(taglist):
|
||||||
|
for tag in reversed(regex_tag.findall(taglist)):
|
||||||
|
if len(tag) < 30 and not tag in BANNED_NAMES and \
|
||||||
|
not re.match(r"^[a-z]?[0-9]+$", tag):
|
||||||
|
return tag
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
regex_title = re.compile(r"^((?:\[[^\]]+\] *)*)([^\[]+) *((?:\[[^\]]+\] *)*)[^\[]*$")
|
||||||
|
def parseTitle(title):
|
||||||
|
m = regex_title.match(title)
|
||||||
|
if m is None:
|
||||||
|
print("Invalid title format: " + title)
|
||||||
|
return title, getNameFromTaglist(title)
|
||||||
|
else:
|
||||||
|
return m.group(2).strip(), getNameFromTaglist(m.group(3))
|
||||||
|
|
||||||
|
def getLinksFromModSearch():
|
||||||
|
links = {}
|
||||||
|
|
||||||
contents = urllib.request.urlopen("http://krock-works.16mb.com/MTstuff/modList.php").read().decode("utf-8")
|
contents = urllib.request.urlopen("http://krock-works.16mb.com/MTstuff/modList.php").read().decode("utf-8")
|
||||||
list = json.loads(contents)
|
for x in json.loads(contents):
|
||||||
found = {}
|
link = x.get("link")
|
||||||
imported = []
|
if link is not None:
|
||||||
|
links[int(x["topicId"])] = link
|
||||||
|
|
||||||
for user in User.query.all():
|
return links
|
||||||
found[user.username] = True
|
|
||||||
if user.forums_username is not None:
|
|
||||||
found[user.forums_username] = True
|
|
||||||
|
|
||||||
for x in list:
|
|
||||||
author = x.get("author")
|
|
||||||
if author is not None and not author in found:
|
|
||||||
user = User(author)
|
|
||||||
user.forums_username = author
|
|
||||||
imported.append(author)
|
|
||||||
found[author] = True
|
|
||||||
db.session.add(user)
|
|
||||||
|
|
||||||
db.session.commit()
|
|
||||||
for author in found:
|
|
||||||
checkForumAccount.delay(author, None)
|
|
||||||
|
|
||||||
|
|
||||||
BANNED_NAMES = ["mod", "game", "old", "outdated", "wip", "api"]
|
|
||||||
ALLOWED_TYPES = [1, 2, 6]
|
|
||||||
|
|
||||||
@celery.task()
|
@celery.task()
|
||||||
def importKrocksModList():
|
def importTopicList():
|
||||||
contents = urllib.request.urlopen("http://krock-works.16mb.com/MTstuff/modList.php").read().decode("utf-8")
|
links_by_id = getLinksFromModSearch()
|
||||||
list = json.loads(contents)
|
|
||||||
|
info_by_id = {}
|
||||||
|
getTopicsFromForum(11, out=info_by_id, extra={ 'type': PackageType.MOD, 'wip': False })
|
||||||
|
getTopicsFromForum(9, out=info_by_id, extra={ 'type': PackageType.MOD, 'wip': True })
|
||||||
|
getTopicsFromForum(15, out=info_by_id, extra={ 'type': PackageType.GAME, 'wip': False })
|
||||||
|
getTopicsFromForum(50, out=info_by_id, extra={ 'type': PackageType.GAME, 'wip': True })
|
||||||
|
|
||||||
|
# Caches
|
||||||
username_to_user = {}
|
username_to_user = {}
|
||||||
|
topics_by_id = {}
|
||||||
|
for topic in ForumTopic.query.all():
|
||||||
|
topics_by_id[topic.topic_id] = topic
|
||||||
|
|
||||||
KrockForumTopic.query.delete()
|
# Create or update
|
||||||
|
for info in info_by_id.values():
|
||||||
|
id = int(info["id"])
|
||||||
|
|
||||||
for x in list:
|
# Get author
|
||||||
type = int(x["type"])
|
username = info["author"]
|
||||||
if not type in ALLOWED_TYPES:
|
|
||||||
continue
|
|
||||||
|
|
||||||
username = x["author"]
|
|
||||||
user = username_to_user.get(username)
|
user = username_to_user.get(username)
|
||||||
if user is None:
|
if user is None:
|
||||||
user = User.query.filter_by(forums_username=username).first()
|
user = User.query.filter_by(forums_username=username).first()
|
||||||
assert(user is not None)
|
if user is None:
|
||||||
|
print(username + " not found!")
|
||||||
|
user = User(username)
|
||||||
|
user.forums_username = username
|
||||||
|
db.session.add(user)
|
||||||
username_to_user[username] = user
|
username_to_user[username] = user
|
||||||
|
|
||||||
import re
|
# Get / add row
|
||||||
tags = re.findall("\[([a-z0-9_]+)\]", x["title"])
|
topic = topics_by_id.get(id)
|
||||||
name = None
|
if topic is None:
|
||||||
for tag in reversed(tags):
|
topic = ForumTopic()
|
||||||
if len(tag) < 30 and not tag in BANNED_NAMES and \
|
db.session.add(topic)
|
||||||
not re.match("^([a-z][0-9]+)$", tag):
|
|
||||||
name = tag
|
|
||||||
break
|
|
||||||
|
|
||||||
topic = KrockForumTopic()
|
# Parse title
|
||||||
topic.topic_id = x["topicId"]
|
title, name = parseTitle(info["title"])
|
||||||
topic.author_id = user.id
|
|
||||||
topic.ttype = type
|
# Get link
|
||||||
topic.title = x["title"]
|
link = links_by_id.get(id)
|
||||||
topic.name = name
|
|
||||||
topic.link = x.get("link")
|
# Fill row
|
||||||
db.session.add(topic)
|
topic.topic_id = id
|
||||||
|
topic.author = user
|
||||||
|
topic.type = info["type"]
|
||||||
|
topic.title = title
|
||||||
|
topic.name = name
|
||||||
|
topic.link = link
|
||||||
|
topic.wip = info["wip"]
|
||||||
|
topic.posts = info["posts"]
|
||||||
|
topic.views = info["views"]
|
||||||
|
topic.created_at = info["date"]
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|||||||
@@ -15,16 +15,18 @@
|
|||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
import flask, json, os
|
import flask, json, os, git, tempfile, shutil
|
||||||
|
from git import GitCommandError
|
||||||
from flask.ext.sqlalchemy import SQLAlchemy
|
from flask.ext.sqlalchemy import SQLAlchemy
|
||||||
from urllib.error import HTTPError
|
from urllib.error import HTTPError
|
||||||
import urllib.request
|
import urllib.request
|
||||||
from urllib.parse import urlparse, quote_plus
|
from urllib.parse import urlparse, quote_plus, urlsplit
|
||||||
from app import app
|
from app import app
|
||||||
from app.models import *
|
from app.models import *
|
||||||
from app.tasks import celery, TaskError
|
from app.tasks import celery, TaskError
|
||||||
from app.utils import randomString
|
from app.utils import randomString
|
||||||
|
|
||||||
|
|
||||||
class GithubURLMaker:
|
class GithubURLMaker:
|
||||||
def __init__(self, url):
|
def __init__(self, url):
|
||||||
# Rewrite path
|
# Rewrite path
|
||||||
@@ -46,18 +48,6 @@ class GithubURLMaker:
|
|||||||
def getRepoURL(self):
|
def getRepoURL(self):
|
||||||
return "https://github.com/{}/{}".format(self.user, self.repo)
|
return "https://github.com/{}/{}".format(self.user, self.repo)
|
||||||
|
|
||||||
def getIssueTrackerURL(self):
|
|
||||||
return "https://github.com/{}/{}/issues/".format(self.user, self.repo)
|
|
||||||
|
|
||||||
def getModConfURL(self):
|
|
||||||
return self.baseUrl + "/mod.conf"
|
|
||||||
|
|
||||||
def getDescURL(self):
|
|
||||||
return self.baseUrl + "/description.txt"
|
|
||||||
|
|
||||||
def getDependsURL(self):
|
|
||||||
return self.baseUrl + "/depends.txt"
|
|
||||||
|
|
||||||
def getScreenshotURL(self):
|
def getScreenshotURL(self):
|
||||||
return self.baseUrl + "/screenshot.png"
|
return self.baseUrl + "/screenshot.png"
|
||||||
|
|
||||||
@@ -69,7 +59,6 @@ class GithubURLMaker:
|
|||||||
return "https://github.com/{}/{}/archive/{}.zip" \
|
return "https://github.com/{}/{}/archive/{}.zip" \
|
||||||
.format(self.user, self.repo, commit)
|
.format(self.user, self.repo, commit)
|
||||||
|
|
||||||
|
|
||||||
krock_list_cache = None
|
krock_list_cache = None
|
||||||
krock_list_cache_by_name = None
|
krock_list_cache_by_name = None
|
||||||
def getKrockList():
|
def getKrockList():
|
||||||
@@ -97,9 +86,9 @@ def getKrockList():
|
|||||||
return {
|
return {
|
||||||
"title": x["title"],
|
"title": x["title"],
|
||||||
"author": x["author"],
|
"author": x["author"],
|
||||||
"name": x["name"],
|
"name": x["name"],
|
||||||
"topicId": x["topicId"],
|
"topicId": x["topicId"],
|
||||||
"link": x["link"],
|
"link": x["link"],
|
||||||
}
|
}
|
||||||
|
|
||||||
krock_list_cache = [g(x) for x in list if h(x)]
|
krock_list_cache = [g(x) for x in list if h(x)]
|
||||||
@@ -143,99 +132,208 @@ def parseConf(string):
|
|||||||
return retval
|
return retval
|
||||||
|
|
||||||
|
|
||||||
@celery.task()
|
class PackageTreeNode:
|
||||||
def getMeta(urlstr, author):
|
def __init__(self, baseDir, author=None, repo=None, name=None):
|
||||||
url = urlparse(urlstr)
|
print("Scanning " + baseDir)
|
||||||
|
self.baseDir = baseDir
|
||||||
|
self.author = author
|
||||||
|
self.name = name
|
||||||
|
self.repo = repo
|
||||||
|
self.meta = None
|
||||||
|
self.children = []
|
||||||
|
|
||||||
urlmaker = None
|
# Detect type
|
||||||
if url.netloc == "github.com":
|
type = None
|
||||||
urlmaker = GithubURLMaker(url)
|
is_modpack = False
|
||||||
else:
|
if os.path.isfile(baseDir + "/game.conf"):
|
||||||
raise TaskError("Unsupported repo")
|
type = PackageType.GAME
|
||||||
|
elif os.path.isfile(baseDir + "/init.lua"):
|
||||||
|
type = PackageType.MOD
|
||||||
|
elif os.path.isfile(baseDir + "/modpack.txt"):
|
||||||
|
type = PackageType.MOD
|
||||||
|
is_modpack = True
|
||||||
|
elif os.path.isdir(baseDir + "/mods"):
|
||||||
|
type = PackageType.GAME
|
||||||
|
elif os.listdir(baseDir) == []:
|
||||||
|
# probably a submodule
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
raise TaskError("Unable to detect package type!")
|
||||||
|
|
||||||
if not urlmaker.isValid():
|
self.type = type
|
||||||
raise TaskError("Error! Url maker not valid")
|
self.readMetaFiles()
|
||||||
|
|
||||||
result = {}
|
if self.type == PackageType.GAME:
|
||||||
|
self.addChildrenFromModDir(baseDir + "/mods")
|
||||||
|
elif is_modpack:
|
||||||
|
self.addChildrenFromModDir(baseDir)
|
||||||
|
|
||||||
result["repo"] = urlmaker.getRepoURL()
|
|
||||||
result["issueTracker"] = urlmaker.getIssueTrackerURL()
|
|
||||||
|
|
||||||
try:
|
def readMetaFiles(self):
|
||||||
contents = urllib.request.urlopen(urlmaker.getModConfURL()).read().decode("utf-8")
|
result = {}
|
||||||
conf = parseConf(contents)
|
|
||||||
for key in ["name", "description", "title", "depends", "optional_depends"]:
|
|
||||||
try:
|
|
||||||
result[key] = conf[key]
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
except HTTPError:
|
|
||||||
print("mod.conf does not exist")
|
|
||||||
|
|
||||||
if "name" in result:
|
# .conf file
|
||||||
result["title"] = result["name"].replace("_", " ").title()
|
|
||||||
|
|
||||||
if not "description" in result:
|
|
||||||
try:
|
try:
|
||||||
contents = urllib.request.urlopen(urlmaker.getDescURL()).read().decode("utf-8")
|
with open(self.baseDir + "/mod.conf", "r") as myfile:
|
||||||
result["description"] = contents.strip()
|
conf = parseConf(myfile.read())
|
||||||
except HTTPError:
|
for key in ["name", "description", "title", "depends", "optional_depends"]:
|
||||||
|
try:
|
||||||
|
result[key] = conf[key]
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
except IOError:
|
||||||
print("description.txt does not exist!")
|
print("description.txt does not exist!")
|
||||||
|
|
||||||
import re
|
# description.txt
|
||||||
pattern = re.compile("^([a-z0-9_]+)\??$")
|
if not "description" in result:
|
||||||
if not "depends" in result and not "optional_depends" in result:
|
try:
|
||||||
try:
|
with open(self.baseDir + "/description.txt", "r") as myfile:
|
||||||
contents = urllib.request.urlopen(urlmaker.getDependsURL()).read().decode("utf-8")
|
result["description"] = myfile.read()
|
||||||
soft = []
|
except IOError:
|
||||||
hard = []
|
print("description.txt does not exist!")
|
||||||
for line in contents.split("\n"):
|
|
||||||
line = line.strip()
|
|
||||||
if pattern.match(line):
|
|
||||||
if line[len(line) - 1] == "?":
|
|
||||||
soft.append( line[:-1])
|
|
||||||
else:
|
|
||||||
hard.append(line)
|
|
||||||
|
|
||||||
result["depends"] = ",".join(hard)
|
# depends.txt
|
||||||
result["optional_depends"] = ",".join(soft)
|
import re
|
||||||
|
pattern = re.compile("^([a-z0-9_]+)\??$")
|
||||||
|
if not "depends" in result and not "optional_depends" in result:
|
||||||
|
try:
|
||||||
|
with open(self.baseDir + "/depends.txt", "r") as myfile:
|
||||||
|
contents = myfile.read()
|
||||||
|
soft = []
|
||||||
|
hard = []
|
||||||
|
for line in contents.split("\n"):
|
||||||
|
line = line.strip()
|
||||||
|
if pattern.match(line):
|
||||||
|
if line[len(line) - 1] == "?":
|
||||||
|
soft.append( line[:-1])
|
||||||
|
else:
|
||||||
|
hard.append(line)
|
||||||
|
|
||||||
|
result["depends"] = hard
|
||||||
|
result["optional_depends"] = soft
|
||||||
|
|
||||||
|
except IOError:
|
||||||
|
print("depends.txt does not exist!")
|
||||||
|
|
||||||
|
else:
|
||||||
|
if "depends" in result:
|
||||||
|
result["depends"] = [x.strip() for x in result["depends"].split(",")]
|
||||||
|
if "optional_depends" in result:
|
||||||
|
result["optional_depends"] = [x.strip() for x in result["optional_depends"].split(",")]
|
||||||
|
|
||||||
|
|
||||||
except HTTPError:
|
# Calculate Title
|
||||||
print("depends.txt does not exist!")
|
if "name" in result and not "title" in result:
|
||||||
|
result["title"] = result["name"].replace("_", " ").title()
|
||||||
|
|
||||||
if "description" in result:
|
# Calculate short description
|
||||||
desc = result["description"]
|
if "description" in result:
|
||||||
idx = desc.find(".") + 1
|
desc = result["description"]
|
||||||
cutIdx = min(len(desc), 200 if idx < 5 else idx)
|
idx = desc.find(".") + 1
|
||||||
result["short_description"] = desc[:cutIdx]
|
cutIdx = min(len(desc), 200 if idx < 5 else idx)
|
||||||
|
result["short_description"] = desc[:cutIdx]
|
||||||
|
|
||||||
|
# Get forum ID
|
||||||
|
info = findModInfo(self.author, result.get("name"), self.repo)
|
||||||
|
if info is not None:
|
||||||
|
result["forumId"] = info.get("topicId")
|
||||||
|
|
||||||
|
if "name" in result:
|
||||||
|
self.name = result["name"]
|
||||||
|
del result["name"]
|
||||||
|
|
||||||
|
self.meta = result
|
||||||
|
|
||||||
|
def addChildrenFromModDir(self, dir):
|
||||||
|
for entry in next(os.walk(dir))[1]:
|
||||||
|
path = dir + "/" + entry
|
||||||
|
if not entry.startswith('.') and os.path.isdir(path):
|
||||||
|
self.children.append(PackageTreeNode(path, name=entry))
|
||||||
|
|
||||||
|
|
||||||
info = findModInfo(author, result.get("name"), result["repo"])
|
def fold(self, attr, key=None, acc=None):
|
||||||
if info is not None:
|
if acc is None:
|
||||||
result["forumId"] = info.get("topicId")
|
acc = set()
|
||||||
|
|
||||||
|
if self.meta is None:
|
||||||
|
return acc
|
||||||
|
|
||||||
|
at = getattr(self, attr)
|
||||||
|
value = at if key is None else at.get(key)
|
||||||
|
|
||||||
|
if isinstance(value, list):
|
||||||
|
acc |= set(value)
|
||||||
|
elif value is not None:
|
||||||
|
acc.add(value)
|
||||||
|
|
||||||
|
for child in self.children:
|
||||||
|
child.fold(attr, key, acc)
|
||||||
|
|
||||||
|
return acc
|
||||||
|
|
||||||
|
def get(self, key):
|
||||||
|
return self.meta.get(key)
|
||||||
|
|
||||||
|
def generateGitURL(urlstr):
|
||||||
|
scheme, netloc, path, query, frag = urlsplit(urlstr)
|
||||||
|
|
||||||
|
return "http://:@" + netloc + path + query
|
||||||
|
|
||||||
|
# Clones a repo from an unvalidated URL.
|
||||||
|
# Returns a tuple of path and repo on sucess.
|
||||||
|
# Throws `TaskError` on failure.
|
||||||
|
# Caller is responsible for deleting returned directory.
|
||||||
|
def cloneRepo(urlstr, ref=None, recursive=False):
|
||||||
|
gitDir = tempfile.gettempdir() + "/" + randomString(10)
|
||||||
|
|
||||||
|
err = None
|
||||||
|
try:
|
||||||
|
gitUrl = generateGitURL(urlstr)
|
||||||
|
print("Cloning from " + gitUrl)
|
||||||
|
repo = git.Repo.clone_from(gitUrl, gitDir, \
|
||||||
|
progress=None, env=None, depth=1, recursive=recursive, kill_after_timeout=15)
|
||||||
|
|
||||||
|
if ref is not None:
|
||||||
|
repo.create_head("myhead", ref).checkout()
|
||||||
|
return gitDir, repo
|
||||||
|
except GitCommandError as e:
|
||||||
|
# This is needed to stop the backtrace being weird
|
||||||
|
err = e.stderr
|
||||||
|
|
||||||
|
raise TaskError(err.replace("stderr: ", "") \
|
||||||
|
.replace("Cloning into '" + gitDir + "'...", "") \
|
||||||
|
.strip())
|
||||||
|
|
||||||
|
@celery.task()
|
||||||
|
def getMeta(urlstr, author):
|
||||||
|
gitDir, _ = cloneRepo(urlstr, recursive=True)
|
||||||
|
tree = PackageTreeNode(gitDir, author=author, repo=urlstr)
|
||||||
|
shutil.rmtree(gitDir)
|
||||||
|
|
||||||
|
result = {}
|
||||||
|
result["name"] = tree.name
|
||||||
|
result["provides"] = tree.fold("name")
|
||||||
|
result["type"] = tree.type.name
|
||||||
|
|
||||||
|
for key in ["depends", "optional_depends"]:
|
||||||
|
result[key] = tree.fold("meta", key)
|
||||||
|
|
||||||
|
for key in ["title", "repo", "issueTracker", "forumId", "description", "short_description"]:
|
||||||
|
result[key] = tree.get(key)
|
||||||
|
|
||||||
|
for mod in result["provides"]:
|
||||||
|
result["depends"].discard(mod)
|
||||||
|
result["optional_depends"].discard(mod)
|
||||||
|
|
||||||
|
for key, value in result.items():
|
||||||
|
if isinstance(value, set):
|
||||||
|
result[key] = list(value)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
@celery.task()
|
def makeVCSReleaseFromGithub(id, branch, release, url):
|
||||||
def makeVCSRelease(id, branch):
|
urlmaker = GithubURLMaker(url)
|
||||||
release = PackageRelease.query.get(id)
|
|
||||||
|
|
||||||
if release is None:
|
|
||||||
raise TaskError("No such release!")
|
|
||||||
|
|
||||||
if release.package is None:
|
|
||||||
raise TaskError("No package attached to release")
|
|
||||||
|
|
||||||
url = urlparse(release.package.repo)
|
|
||||||
|
|
||||||
urlmaker = None
|
|
||||||
if url.netloc == "github.com":
|
|
||||||
urlmaker = GithubURLMaker(url)
|
|
||||||
else:
|
|
||||||
raise TaskError("Unsupported repo")
|
|
||||||
|
|
||||||
if not urlmaker.isValid():
|
if not urlmaker.isValid():
|
||||||
raise TaskError("Invalid github repo URL")
|
raise TaskError("Invalid github repo URL")
|
||||||
|
|
||||||
@@ -246,14 +344,47 @@ def makeVCSRelease(id, branch):
|
|||||||
if len(commits) == 0 or not "sha" in commits[0]:
|
if len(commits) == 0 or not "sha" in commits[0]:
|
||||||
raise TaskError("No commits found")
|
raise TaskError("No commits found")
|
||||||
|
|
||||||
release.url = urlmaker.getCommitDownload(commits[0]["sha"])
|
release.url = urlmaker.getCommitDownload(commits[0]["sha"])
|
||||||
|
release.task_id = None
|
||||||
|
release.commit_hash = commits[0]["sha"]
|
||||||
print(release.url)
|
print(release.url)
|
||||||
release.task_id = None
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
return release.url
|
return release.url
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@celery.task()
|
||||||
|
def makeVCSRelease(id, branch):
|
||||||
|
release = PackageRelease.query.get(id)
|
||||||
|
if release is None:
|
||||||
|
raise TaskError("No such release!")
|
||||||
|
elif release.package is None:
|
||||||
|
raise TaskError("No package attached to release")
|
||||||
|
|
||||||
|
urlmaker = None
|
||||||
|
url = urlparse(release.package.repo)
|
||||||
|
if url.netloc == "github.com":
|
||||||
|
return makeVCSReleaseFromGithub(id, branch, release, url)
|
||||||
|
else:
|
||||||
|
gitDir, repo = cloneRepo(release.package.repo, ref=branch, recursive=True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
filename = randomString(10) + ".zip"
|
||||||
|
destPath = os.path.join("app/public/uploads", filename)
|
||||||
|
with open(destPath, "wb") as fp:
|
||||||
|
repo.archive(fp, format="zip")
|
||||||
|
|
||||||
|
release.url = "/uploads/" + filename
|
||||||
|
release.task_id = None
|
||||||
|
release.commit_hash = repo.head.object.hexsha
|
||||||
|
print(release.url)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return release.url
|
||||||
|
finally:
|
||||||
|
shutil.rmtree(gitDir)
|
||||||
|
|
||||||
@celery.task()
|
@celery.task()
|
||||||
def importRepoScreenshot(id):
|
def importRepoScreenshot(id):
|
||||||
package = Package.query.get(id)
|
package = Package.query.get(id)
|
||||||
@@ -261,34 +392,35 @@ def importRepoScreenshot(id):
|
|||||||
raise Exception("Unexpected none package")
|
raise Exception("Unexpected none package")
|
||||||
|
|
||||||
# Get URL Maker
|
# Get URL Maker
|
||||||
url = urlparse(package.repo)
|
|
||||||
urlmaker = None
|
|
||||||
if url.netloc == "github.com":
|
|
||||||
urlmaker = GithubURLMaker(url)
|
|
||||||
else:
|
|
||||||
raise TaskError("Unsupported repo")
|
|
||||||
|
|
||||||
if not urlmaker.isValid():
|
|
||||||
raise TaskError("Error! Url maker not valid")
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
filename = randomString(10) + ".png"
|
gitDir, _ = cloneRepo(package.repo)
|
||||||
imagePath = os.path.join("app/public/uploads", filename)
|
except TaskError as e:
|
||||||
print(imagePath)
|
# ignore download errors
|
||||||
urllib.request.urlretrieve(urlmaker.getScreenshotURL(), imagePath)
|
print(e)
|
||||||
|
return None
|
||||||
|
|
||||||
ss = PackageScreenshot()
|
# Find and import screenshot
|
||||||
ss.approved = True
|
try:
|
||||||
ss.package = package
|
for ext in ["png", "jpg", "jpeg"]:
|
||||||
ss.title = "screenshot.png"
|
sourcePath = gitDir + "/screenshot." + ext
|
||||||
ss.url = "/uploads/" + filename
|
if os.path.isfile(sourcePath):
|
||||||
db.session.add(ss)
|
filename = randomString(10) + "." + ext
|
||||||
db.session.commit()
|
destPath = os.path.join("app/public/uploads", filename)
|
||||||
|
shutil.copyfile(sourcePath, destPath)
|
||||||
|
|
||||||
return "/uploads/" + filename
|
ss = PackageScreenshot()
|
||||||
except HTTPError:
|
ss.approved = True
|
||||||
print("screenshot.png does not exist")
|
ss.package = package
|
||||||
|
ss.title = "screenshot.png"
|
||||||
|
ss.url = "/uploads/" + filename
|
||||||
|
db.session.add(ss)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return "/uploads/" + filename
|
||||||
|
finally:
|
||||||
|
shutil.rmtree(gitDir)
|
||||||
|
|
||||||
|
print("screenshot.png does not exist")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
import urllib, socket
|
import urllib, socket
|
||||||
from bs4 import *
|
from bs4 import *
|
||||||
from urllib.parse import urljoin
|
from urllib.parse import urljoin
|
||||||
|
from datetime import datetime
|
||||||
import urllib.request
|
import urllib.request
|
||||||
import os.path
|
import os.path
|
||||||
import time, re
|
import time, re
|
||||||
@@ -77,3 +78,72 @@ def getProfile(url, username):
|
|||||||
__extract_properties(profile, soup)
|
__extract_properties(profile, soup)
|
||||||
|
|
||||||
return profile
|
return profile
|
||||||
|
|
||||||
|
|
||||||
|
regex_id = re.compile(r"^.*t=([0-9]+).*$")
|
||||||
|
|
||||||
|
def parseForumListPage(id, page, out, extra=None):
|
||||||
|
num_per_page = 30
|
||||||
|
start = page*num_per_page+1
|
||||||
|
print(" - Fetching page {} (topics {}-{})".format(page, start, start+num_per_page))
|
||||||
|
|
||||||
|
url = "https://forum.minetest.net/viewforum.php?f=" + str(id) + "&start=" + str(start)
|
||||||
|
r = urllib.request.urlopen(url).read().decode("utf-8")
|
||||||
|
soup = BeautifulSoup(r, "html.parser")
|
||||||
|
|
||||||
|
for row in soup.find_all("li", class_="row"):
|
||||||
|
classes = row.get("class")
|
||||||
|
if "sticky" in classes or "announce" in classes or "global-announce" in classes:
|
||||||
|
continue
|
||||||
|
|
||||||
|
topic = row.find("dl")
|
||||||
|
|
||||||
|
# Link info
|
||||||
|
link = topic.find(class_="topictitle")
|
||||||
|
id = regex_id.match(link.get("href")).group(1)
|
||||||
|
title = link.find(text=True)
|
||||||
|
|
||||||
|
# Date
|
||||||
|
left = topic.find("dt")
|
||||||
|
date = left.get_text().split("»")[1].strip()
|
||||||
|
date = datetime.strptime(date, "%a %b %d, %Y %H:%M")
|
||||||
|
author = left.find_all("a")[-1].get_text().strip()
|
||||||
|
|
||||||
|
# Get counts
|
||||||
|
posts = topic.find(class_="posts").find(text=True)
|
||||||
|
views = topic.find(class_="views").find(text=True)
|
||||||
|
|
||||||
|
if id in out:
|
||||||
|
print(" - got {} again, title: {}".format(id, title))
|
||||||
|
assert(title == out[id]['title'])
|
||||||
|
return False
|
||||||
|
|
||||||
|
row = {
|
||||||
|
"id" : id,
|
||||||
|
"title" : title,
|
||||||
|
"author": author,
|
||||||
|
"posts" : posts,
|
||||||
|
"views" : views,
|
||||||
|
"date" : date
|
||||||
|
}
|
||||||
|
|
||||||
|
if extra is not None:
|
||||||
|
for key, value in extra.items():
|
||||||
|
row[key] = value
|
||||||
|
|
||||||
|
out[id] = row
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def getTopicsFromForum(id, out={}, extra=None):
|
||||||
|
print("Fetching all topics from forum {}".format(id))
|
||||||
|
page = 0
|
||||||
|
while parseForumListPage(id, page, out, extra):
|
||||||
|
page = page + 1
|
||||||
|
|
||||||
|
return out
|
||||||
|
|
||||||
|
def dumpTitlesToFile(topics, path):
|
||||||
|
with open(path, "w") as out_file:
|
||||||
|
for topic in topics.values():
|
||||||
|
out_file.write(topic["title"] + "\n")
|
||||||
|
|||||||
25
app/templates/admin/licenses/edit.html
Normal file
25
app/templates/admin/licenses/edit.html
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
{% if license %}
|
||||||
|
Edit {{ license.name }}
|
||||||
|
{% else %}
|
||||||
|
New license
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<p>
|
||||||
|
<a href="{{ url_for('license_list_page') }}">Back to list</a> |
|
||||||
|
<a href="{{ url_for('createedit_license_page') }}">New License</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{% from "macros/forms.html" import render_field, render_submit_field %}
|
||||||
|
<form method="POST" action="" enctype="multipart/form-data">
|
||||||
|
{{ form.hidden_tag() }}
|
||||||
|
|
||||||
|
{{ render_field(form.name) }}
|
||||||
|
{{ render_field(form.is_foss) }}
|
||||||
|
{{ render_submit_field(form.submit) }}
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
16
app/templates/admin/licenses/list.html
Normal file
16
app/templates/admin/licenses/list.html
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
Licenses
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<p>
|
||||||
|
<a href="{{ url_for('createedit_license_page') }}">New Tag</a>
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
{% for l in licenses %}
|
||||||
|
<li><a href="{{ url_for('createedit_license_page', name=l.name) }}">{{ l.name }}</a> [{{ l.is_foss and "Free" or "Non-free"}}]</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endblock %}
|
||||||
@@ -1,13 +1,15 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block title %}
|
{% block title %}
|
||||||
Admin Tools
|
Admin
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<ul>
|
<ul>
|
||||||
|
<li><a href="db/">Database</a></li>
|
||||||
<li><a href="{{ url_for('user_list_page') }}">User list</a></li>
|
<li><a href="{{ url_for('user_list_page') }}">User list</a></li>
|
||||||
<li><a href="{{ url_for('tag_list_page') }}">Tag Editor</a></li>
|
<li><a href="{{ url_for('tag_list_page') }}">Tag Editor</a></li>
|
||||||
|
<li><a href="{{ url_for('license_list_page') }}">License Editor</a></li>
|
||||||
<li><a href="{{ url_for('switch_user_page') }}">Sign in as another user</a></li>
|
<li><a href="{{ url_for('switch_user_page') }}">Sign in as another user</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
@@ -17,11 +19,12 @@
|
|||||||
<form method="post" action="" class="box-body">
|
<form method="post" action="" class="box-body">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||||
<select name="action">
|
<select name="action">
|
||||||
<option value="importusers">Create users from mod list</option>
|
<option value="importmodlist" selected>Import forum topics</option>
|
||||||
<option value="importmodlist">Import Krock's mod list</option>
|
<option value="recalcscores">Recalculate package scores</option>
|
||||||
<option value="importscreenshots" selected>Import screenshots from VCS</option>
|
<!-- <option value="importscreenshots">Import screenshots from VCS</option> -->
|
||||||
<option value="importdepends">Import dependencies from downloads</option>
|
<!-- <option value="importdepends">Import dependencies from downloads</option> -->
|
||||||
<option value="modprovides">Set provides to mod name</option>
|
<!-- <option value="modprovides">Set provides to mod name</option> -->
|
||||||
|
<!-- <option value="vcsrelease">Create VCS releases</option> -->
|
||||||
</select>
|
</select>
|
||||||
<input type="submit" value="Perform" />
|
<input type="submit" value="Perform" />
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -65,6 +65,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% if current_user.rank == current_user.rank.MODERATOR %}
|
{% if current_user.rank == current_user.rank.MODERATOR %}
|
||||||
<li><a href="{{ url_for('tag_list_page') }}">Tag Editor</a></li>
|
<li><a href="{{ url_for('tag_list_page') }}">Tag Editor</a></li>
|
||||||
|
<li><a href="{{ url_for('license_list_page') }}">License Editor</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li><a href="{{ url_for('user.logout') }}">Sign out</a></li>
|
<li><a href="{{ url_for('user.logout') }}">Sign out</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -11,10 +11,6 @@ Sign in
|
|||||||
<h2>{%trans%}Sign in{%endtrans%}</h2>
|
<h2>{%trans%}Sign in{%endtrans%}</h2>
|
||||||
|
|
||||||
<form action="" method="POST" class="form box-body" role="form">
|
<form action="" method="POST" class="form box-body" role="form">
|
||||||
<h3>Sign in with Github</h3>
|
|
||||||
<p><a class="button" href="{{ url_for('github_signin_page') }}">GitHub</a></p>
|
|
||||||
|
|
||||||
|
|
||||||
<h3>Sign in with username/password</h3>
|
<h3>Sign in with username/password</h3>
|
||||||
{{ form.hidden_tag() }}
|
{{ form.hidden_tag() }}
|
||||||
|
|
||||||
@@ -38,17 +34,13 @@ Sign in
|
|||||||
{# Password field #}
|
{# Password field #}
|
||||||
{% set field = form.password %}
|
{% set field = form.password %}
|
||||||
<div class="form-group {% if field.errors %}has-error{% endif %}">
|
<div class="form-group {% if field.errors %}has-error{% endif %}">
|
||||||
{# Label on left, "Forgot your Password?" on right #}
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xs-6">
|
<label for="{{ field.id }}" class="control-label">{{ field.label.text }}
|
||||||
<label for="{{ field.id }}" class="control-label">{{ field.label.text }}</label>
|
{% if user_manager.enable_forgot_password %}
|
||||||
</div>
|
|
||||||
<div class="col-xs-6 text-right">
|
|
||||||
{% if user_manager.enable_forgot_password %}
|
|
||||||
<a href="{{ url_for('user.forgot_password') }}" tabindex='195'>
|
<a href="{{ url_for('user.forgot_password') }}" tabindex='195'>
|
||||||
{%trans%}Forgot your Password?{%endtrans%}</a>
|
[{%trans%}Forgot My Password{%endtrans%}]</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
{{ field(class_='form-control', tabindex=120) }}
|
{{ field(class_='form-control', tabindex=120) }}
|
||||||
{% if field.errors %}
|
{% if field.errors %}
|
||||||
@@ -64,7 +56,12 @@ Sign in
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{# Submit button #}
|
{# Submit button #}
|
||||||
{{ render_submit_field(form.submit, tabindex=180) }}
|
<p>
|
||||||
|
{{ render_submit_field(form.submit, tabindex=180) }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3>Sign in with Github</h3>
|
||||||
|
<p><a class="button" href="{{ url_for('github_signin_page') }}">GitHub</a></p>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>{{ page['title'] }}</h1>
|
{% if not page["no_h1"] %}<h1>{{ page['title'] }}</h1>{% endif %}
|
||||||
|
|
||||||
{{ page.html | safe }}
|
{{ page.html | safe }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ Welcome
|
|||||||
<p>
|
<p>
|
||||||
Minetest's official content repository.
|
Minetest's official content repository.
|
||||||
Browse {{ count }} packages,
|
Browse {{ count }} packages,
|
||||||
all available under a free and open source
|
majority of which available under a free and open source
|
||||||
license.
|
license.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -25,6 +25,14 @@ Welcome
|
|||||||
|
|
||||||
<main>
|
<main>
|
||||||
{% from "macros/packagegridtile.html" import render_pkggrid %}
|
{% from "macros/packagegridtile.html" import render_pkggrid %}
|
||||||
{{ render_pkggrid(packages) }}
|
|
||||||
|
<h2>Popular</h2>
|
||||||
|
{{ render_pkggrid(popular) }}
|
||||||
|
|
||||||
|
<a href="{{ url_for('packages_page') }}" class="button">Show More</a>
|
||||||
|
|
||||||
|
<h2 style="margin-top:2em;">Newly Added</h2>
|
||||||
|
{{ render_pkggrid(new) }}
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
36
app/templates/macros/threads.html
Normal file
36
app/templates/macros/threads.html
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
{% macro render_thread(thread, current_user) -%}
|
||||||
|
<ul class="comments">
|
||||||
|
{% for r in thread.replies %}
|
||||||
|
<li>
|
||||||
|
<div class="info_strip">
|
||||||
|
<a class="author {{ r.author.rank.name }}"
|
||||||
|
href="{{ url_for('user_profile_page', username=r.author.username) }}">
|
||||||
|
{{ r.author.display_name }}</a>
|
||||||
|
<span>{{ r.created_at | datetime }}</span>
|
||||||
|
<div class="clearboth"></div>
|
||||||
|
</div>
|
||||||
|
<div class="msg">
|
||||||
|
{{ r.comment | markdown }}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
{% if current_user.is_authenticated %}
|
||||||
|
<form method="post" action="{{ url_for('thread_page', id=thread.id)}}" class="comment_form">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||||
|
<textarea required maxlength=500 name="comment" placeholder="Markdown supported"></textarea><br />
|
||||||
|
<input type="submit" value="Comment" />
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
|
{% macro render_threadlist(threads) -%}
|
||||||
|
<ul>
|
||||||
|
{% for t in threads %}
|
||||||
|
<li>{% if t.private %}🔒 {% endif %}<a href="{{ url_for('thread_page', id=t.id) }}">{{ t.title }}</a> by {{ t.author.display_name }}</li>
|
||||||
|
{% else %}
|
||||||
|
<li><i>No threads found</i></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endmacro %}
|
||||||
52
app/templates/macros/topics.html
Normal file
52
app/templates/macros/topics.html
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
{% macro render_topics_table(topics, show_author=True) -%}
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>Id</th>
|
||||||
|
<th></th>
|
||||||
|
<th>Title</th>
|
||||||
|
{% if show_author %}<th>Author</th>{% endif %}
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Link</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
{% for topic in topics %}
|
||||||
|
<tr{% if topic.wip %} class="wiptopic"{% endif %}>
|
||||||
|
<td>{{ topic.topic_id }}</td>
|
||||||
|
<td>
|
||||||
|
[{{ topic.type.value }}]
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="https://forum.minetest.net/viewtopic.php?t={{ topic.topic_id}}">{{ topic.title }}</a>
|
||||||
|
{% if topic.wip %}[WIP]{% endif %}
|
||||||
|
</td>
|
||||||
|
{% if show_author %}
|
||||||
|
<td><a href="{{ url_for('user_profile_page', username=topic.author.username) }}">{{ topic.author.display_name}}</a></td>
|
||||||
|
{% endif %}
|
||||||
|
<td>{{ topic.name or ""}}</td>
|
||||||
|
<td>{% if topic.link %}<a href="{{ topic.link }}">{{ topic.link | domain }}</a>{% endif %}</td>
|
||||||
|
<td>
|
||||||
|
<a href="{{ url_for('create_edit_package_page', author=topic.author.username, repo=topic.getRepoURL(), forums=topic.topic_id, title=topic.title, bname=topic.name) }}">Create</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
|
|
||||||
|
{% macro render_topics(topics, current_user, show_author=True) -%}
|
||||||
|
<ul>
|
||||||
|
{% for topic in topics %}
|
||||||
|
<li{% if topic.wip %} class="wiptopic"{% endif %}>
|
||||||
|
<a href="https://forum.minetest.net/viewtopic.php?t={{ topic.topic_id}}">{{ topic.title }}</a>
|
||||||
|
{% if topic.wip %}[WIP]{% endif %}
|
||||||
|
{% if topic.name %}[{{ topic.name }}]{% endif %}
|
||||||
|
{% if show_author %}
|
||||||
|
by <a href="{{ url_for('user_profile_page', username=topic.author.username) }}">{{ topic.author.display_name }}</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if topic.author == current_user or topic.author.checkPerm(current_user, "CHANGE_AUTHOR") %}
|
||||||
|
| <a href="{{ url_for('create_edit_package_page', author=topic.author.username, repo=topic.getRepoURL(), forums=topic.topic_id, title=topic.title, bname=topic.name) }}">Create</a>
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endmacro %}
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
|
|
||||||
<div class="pkg_wiz_1">
|
<div class="pkg_wiz_1">
|
||||||
<p>Enter the repo URL for the package.
|
<p>Enter the repo URL for the package.
|
||||||
If it's hosted on Github then metadata will automatically be imported.</p>
|
If the repo uses git then the metadata will be automatically imported.</p>
|
||||||
|
|
||||||
<p>Leave blank if you don't have a repo.</p>
|
<p>Leave blank if you don't have a repo.</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -61,7 +61,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pkg_wiz_2">
|
<div class="pkg_wiz_2">
|
||||||
Importing...
|
Importing... (This may take a while)
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{ render_field(form.website, class_="pkg_meta") }}
|
{{ render_field(form.website, class_="pkg_meta") }}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
{% if type %}<input type="hidden" name="type" value="{{ type }}" />{% endif %}
|
{% if type %}<input type="hidden" name="type" value="{{ type }}" />{% endif %}
|
||||||
<input type="text" name="q" value="{{ query or ''}}" />
|
<input type="text" name="q" value="{{ query or ''}}" />
|
||||||
<input type="submit" value="Search" />
|
<input type="submit" value="Search" />
|
||||||
|
<input type="submit" name="lucky" value="I'm feeling lucky" />
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Found {{ packages_count }} packages.
|
Found {{ packages_count }} packages.
|
||||||
@@ -37,4 +38,12 @@
|
|||||||
<li>{{ page }} / {{ page_max }}</li>
|
<li>{{ page }} / {{ page_max }}</li>
|
||||||
{% if next_url %}<li><a href="{{ next_url }}">Next</a></li> {% endif %}
|
{% if next_url %}<li><a href="{{ next_url }}">Next</a></li> {% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
{% if topics %}
|
||||||
|
<h2 style="margin-top:2em;">More content from the forums</h2>
|
||||||
|
|
||||||
|
{% from "macros/topics.html" import render_topics %}
|
||||||
|
{{ render_topics(topics, current_user) }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -21,8 +21,12 @@
|
|||||||
Url: <a href="{{ release.url }}">{{ release.url }}</a><br />
|
Url: <a href="{{ release.url }}">{{ release.url }}</a><br />
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if release.commit_hash %}
|
||||||
|
Commit Hash: {{ release.commit_hash }}<br />
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if release.task_id %}
|
{% if release.task_id %}
|
||||||
Importing... <a href="{ url_for('check_task', id=release.task_id, r=release.getEditURL()) }}">view task</a><br />
|
Importing... <a href="{{ url_for('check_task', id=release.task_id, r=release.getEditURL()) }}">view task</a><br />
|
||||||
{% if package.checkPerm(current_user, "CHANGE_RELEASE_URL") %}
|
{% if package.checkPerm(current_user, "CHANGE_RELEASE_URL") %}
|
||||||
{{ render_field(form.task_id) }}
|
{{ render_field(form.task_id) }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -38,23 +42,4 @@
|
|||||||
|
|
||||||
{{ render_submit_field(form.submit) }}
|
{{ render_submit_field(form.submit) }}
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{% if package.checkPerm(current_user, "APPROVE_RELEASE") %}
|
|
||||||
<div class="box box_grey">
|
|
||||||
<h2>Approval Checklist</h2>
|
|
||||||
<ul>
|
|
||||||
<li>Link leads to a valid download, ie: is a zip file which
|
|
||||||
has either init.lua or modpack.txt if a mod, mods/ if a game, or textures if a texture pack.
|
|
||||||
It's okay if they're inside an immediate folder, like so:
|
|
||||||
|
|
||||||
<pre>
|
|
||||||
example.zip/
|
|
||||||
└── example
|
|
||||||
└── init.lua
|
|
||||||
</pre>
|
|
||||||
</li>
|
|
||||||
<li>There's no obfuscated code.</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
{{ render_field(form.title, placeholder="Human readable. Eg: 1.0.0 or 2018-05-28") }}
|
{{ render_field(form.title, placeholder="Human readable. Eg: 1.0.0 or 2018-05-28") }}
|
||||||
{{ render_field(form.uploadOpt) }}
|
{{ render_field(form.uploadOpt) }}
|
||||||
{% if package.canMakeReleaseFromVCS() %}
|
{% if package.repo %}
|
||||||
{{ render_field(form.vcsLabel) }}
|
{{ render_field(form.vcsLabel) }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{{ render_field(form.fileUpload) }}
|
{{ render_field(form.fileUpload) }}
|
||||||
|
|||||||
@@ -5,7 +5,9 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<img src="{{ screenshot.getThumbnailURL() }}" alt="{{ screenshot.title }}" />
|
<a href="{{ screenshot.url }}">
|
||||||
|
<img src="{{ screenshot.getThumbnailURL() }}" alt="{{ screenshot.title }}" />
|
||||||
|
</a>
|
||||||
|
|
||||||
{% from "macros/forms.html" import render_field, render_submit_field %}
|
{% from "macros/forms.html" import render_field, render_submit_field %}
|
||||||
<form method="POST" action="" enctype="multipart/form-data">
|
<form method="POST" action="" enctype="multipart/form-data">
|
||||||
|
|||||||
@@ -24,6 +24,12 @@
|
|||||||
{% elif (package.type == package.type.GAME or package.type == package.type.TXP) and package.screenshots.count() == 0 %}
|
{% elif (package.type == package.type.GAME or package.type == package.type.TXP) and package.screenshots.count() == 0 %}
|
||||||
You need to add at least one screenshot.
|
You need to add at least one screenshot.
|
||||||
|
|
||||||
|
{% elif topic_error_lvl == "error" %}
|
||||||
|
Please fix the below topic issue(s).
|
||||||
|
|
||||||
|
{% elif "Other" in package.license.name or "Other" in package.media_license.name %}
|
||||||
|
Please wait for the license to be added to CDB.
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
{% if package.screenshots.count() == 0 %}
|
{% if package.screenshots.count() == 0 %}
|
||||||
<b>You should add at least one screenshot, but this isn't required.</b><br />
|
<b>You should add at least one screenshot, but this isn't required.</b><br />
|
||||||
@@ -43,6 +49,35 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<div style="clear: both;"></div>
|
<div style="clear: both;"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if topic_error %}
|
||||||
|
<div class="box box_grey alert alert-{{ topic_error_lvl }}">
|
||||||
|
<span class="icon_message"></span>
|
||||||
|
{{ topic_error | safe }}
|
||||||
|
<div style="clear: both;"></div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if package.author == current_user or package.checkPerm(current_user, "APPROVE_NEW") %}
|
||||||
|
{% if review_thread %}
|
||||||
|
<h2>{% if review_thread.private %}🔒{% endif %} {{ review_thread.title }}</h2>
|
||||||
|
{% if review_thread.private %}
|
||||||
|
<p><i>
|
||||||
|
This thread is only visible to the package owner and users of
|
||||||
|
Editor rank or above.
|
||||||
|
</i></p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% from "macros/threads.html" import render_thread %}
|
||||||
|
{{ render_thread(review_thread, current_user) }}
|
||||||
|
{% else %}
|
||||||
|
<div class="box box_grey alert alert-info">
|
||||||
|
Privately ask a question or give feedback
|
||||||
|
|
||||||
|
<a class="alert_right button" href="{{ url_for('new_thread_page', pid=package.id, title='Package approval comments') }}">Open Thread</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<h1>{{ package.title }} by {{ package.author.display_name }}</h1>
|
<h1>{{ package.title }} by {{ package.author.display_name }}</h1>
|
||||||
@@ -139,6 +174,9 @@
|
|||||||
{% if package.checkPerm(current_user, "MAKE_RELEASE") %}
|
{% if package.checkPerm(current_user, "MAKE_RELEASE") %}
|
||||||
<li><a href="{{ package.getCreateReleaseURL() }}">Create Release</a></li>
|
<li><a href="{{ package.getCreateReleaseURL() }}">Create Release</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if package.approved and package.checkPerm(current_user, "CREATE_THREAD") %}
|
||||||
|
<li><a href="{{ url_for('new_thread_page', pid=package.id) }}">Open Thread</a></li>
|
||||||
|
{% endif %}
|
||||||
{% if package.checkPerm(current_user, "DELETE_PACKAGE") %}
|
{% if package.checkPerm(current_user, "DELETE_PACKAGE") %}
|
||||||
<li><a href="{{ package.getDeleteURL() }}">Delete</a></li>
|
<li><a href="{{ package.getDeleteURL() }}">Delete</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -158,7 +196,8 @@
|
|||||||
<li>
|
<li>
|
||||||
{% if not rel.approved %}<i>{% endif %}
|
{% if not rel.approved %}<i>{% endif %}
|
||||||
|
|
||||||
<a href="{{ rel.url }}">{{ rel.title }}</a>,
|
<a href="{{ rel.getDownloadURL() }}">{{ rel.title }}</a>{% if rel.commit_hash %}
|
||||||
|
[{{ rel.commit_hash | truncate(5, end='') }}]{% endif %},
|
||||||
created {{ rel.releaseDate | datetime }}.
|
created {{ rel.releaseDate | datetime }}.
|
||||||
{% if rel.task_id %}
|
{% if rel.task_id %}
|
||||||
<a href="{{ url_for('check_task', id=rel.task_id, r=package.getDetailsURL()) }}">Importing...</a>
|
<a href="{{ url_for('check_task', id=rel.task_id, r=package.getDetailsURL()) }}">Importing...</a>
|
||||||
@@ -258,4 +297,39 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if similar_topics %}
|
||||||
|
<h3>Similar Forum Topics</h3>
|
||||||
|
{% if not package.approved and package.type == package.type.MOD %}
|
||||||
|
<div class="box box_grey alert alert-warning">
|
||||||
|
Please make sure that this package has the right to
|
||||||
|
the name '{{ package.name }}'.
|
||||||
|
See the
|
||||||
|
<a href="/policy_and_guidance/">Inclusion Policy</a>
|
||||||
|
for more info.
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<ul>
|
||||||
|
{% for t in similar_topics %}
|
||||||
|
<li>
|
||||||
|
[{{ t.type.value }}]
|
||||||
|
<a href="https://forum.minetest.net/viewtopic.php?t={{ t.topic_id }}">
|
||||||
|
{{ t.title }} by {{ t.author.display_name }}
|
||||||
|
</a>
|
||||||
|
{% if t.wip %}[WIP]{% endif %}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if threads %}
|
||||||
|
<h3>Threads</h3>
|
||||||
|
|
||||||
|
{% if package.approved and package.checkPerm(current_user, "CREATE_THREAD") %}
|
||||||
|
<p><a href="{{ url_for('new_thread_page', pid=package.id) }}">Open Thread</a></p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% from "macros/threads.html" import render_threadlist %}
|
||||||
|
{{ render_threadlist(threads) }}
|
||||||
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
12
app/templates/threads/list.html
Normal file
12
app/templates/threads/list.html
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
Threads
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>Threads</h1>
|
||||||
|
|
||||||
|
{% from "macros/threads.html" import render_threadlist %}
|
||||||
|
{{ render_threadlist(threads) }}
|
||||||
|
{% endblock %}
|
||||||
19
app/templates/threads/new.html
Normal file
19
app/templates/threads/new.html
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
New Thread
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% from "macros/forms.html" import render_field, render_submit_field %}
|
||||||
|
<form method="POST" action="" enctype="multipart/form-data">
|
||||||
|
{{ form.hidden_tag() }}
|
||||||
|
|
||||||
|
{{ render_field(form.title) }}
|
||||||
|
{{ render_field(form.comment) }}
|
||||||
|
{{ render_field(form.private) }}
|
||||||
|
{{ render_submit_field(form.submit) }}
|
||||||
|
|
||||||
|
<p>Only the you, the package author, and users of Editor rank and above can read private threads.</p>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
38
app/templates/threads/view.html
Normal file
38
app/templates/threads/view.html
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
Threads
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>{% if thread.private %}🔒 {% endif %}{{ thread.title }}</h1>
|
||||||
|
{% if thread.package or current_user.is_authenticated %}
|
||||||
|
{% if thread.package %}
|
||||||
|
<p>Package: <a href="{{ thread.package.getDetailsURL() }}">{{ thread.package.title }}</a></p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if current_user.is_authenticated %}
|
||||||
|
{% if current_user in thread.watchers %}
|
||||||
|
<form method="post" action="{{ thread.getUnsubscribeURL() }}">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||||
|
<input type="submit" value="Unsubscribe" />
|
||||||
|
</form>
|
||||||
|
{% else %}
|
||||||
|
<form method="post" action="{{ thread.getSubscribeURL() }}">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||||
|
<input type="submit" value="Subscribe" />
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if thread.private %}
|
||||||
|
<i>
|
||||||
|
This thread is only visible to its creator, the package owner, and users of
|
||||||
|
Editor rank or above.
|
||||||
|
</i>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% from "macros/threads.html" import render_thread %}
|
||||||
|
{{ render_thread(thread, current_user) }}
|
||||||
|
{% endblock %}
|
||||||
@@ -65,6 +65,6 @@
|
|||||||
<p>
|
<p>
|
||||||
There are
|
There are
|
||||||
<a href="{{ url_for('todo_topics_page') }}">{{ topics_to_add }} packages</a>
|
<a href="{{ url_for('todo_topics_page') }}">{{ topics_to_add }} packages</a>
|
||||||
to be added to cdb, based on forum topics picked up by Krock's mod search.
|
to be added to cdb, based on cdb's forum parser.
|
||||||
</p>
|
</p>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -12,23 +12,6 @@ Topics to be Added
|
|||||||
{{ topics | count }} remaining.
|
{{ topics | count }} remaining.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<table>
|
{% from "macros/topics.html" import render_topics_table %}
|
||||||
<tr>
|
{{ render_topics_table(topics) }}
|
||||||
<th>Id</th>
|
|
||||||
<th>Title</th>
|
|
||||||
<th>Author</th>
|
|
||||||
<th>Name</th>
|
|
||||||
<th>Link</th>
|
|
||||||
</tr>
|
|
||||||
{% for topic in topics %}
|
|
||||||
<tr>
|
|
||||||
<td>{{ topic.topic_id }}</td>
|
|
||||||
<td>[{{ topic.getType().value }}] <a href="https://forum.minetest.net/viewtopic.php?t={{ topic.topic_id}}">{{ topic.title }}</a></td>
|
|
||||||
<td><a href="{{ url_for('user_profile_page', username=topic.author.username) }}">{{ topic.author.display_name}}</a></td>
|
|
||||||
<td>{{ topic.name or ""}}</td>
|
|
||||||
<td><a href="{{ topic.link }}">{{ topic.link | domain }}</a></td>
|
|
||||||
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</table>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -5,24 +5,36 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
|
{% if optional %}
|
||||||
|
<div class="box box_grey alert alert-primary">
|
||||||
|
It is recommended that you set a password for your account.
|
||||||
|
|
||||||
|
<a class="alert_right button" href="{{ url_for('home_page') }}">Skip</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
<h1>Set Password</h1>
|
<h1>Set Password</h1>
|
||||||
|
|
||||||
{% from "macros/forms.html" import render_field, render_submit_field %}
|
{% from "macros/forms.html" import render_field, render_submit_field %}
|
||||||
<form action="" method="POST" class="form" role="form">
|
<form action="" method="POST" class="form" role="form">
|
||||||
<div class="row">
|
{{ form.hidden_tag() }}
|
||||||
<div class="col-sm-6 col-md-5 col-lg-4">
|
|
||||||
{{ form.hidden_tag() }}
|
|
||||||
|
|
||||||
{% if not current_user.email %}
|
{% if not current_user.email %}
|
||||||
{{ render_field(form.email, tabindex=230) }}
|
{{ render_field(form.email, tabindex=230) }}
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{{ render_field(form.password, tabindex=230) }}
|
<p>
|
||||||
{{ render_field(form.password2, tabindex=240) }}
|
Your email is needed to recover your account if you forget your
|
||||||
|
password, and to optionally send notifications.
|
||||||
|
Your email will never be shared to a third-party.
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{{ render_submit_field(form.submit, tabindex=280) }}
|
{{ render_field(form.password, tabindex=230) }}
|
||||||
</div>
|
{{ render_field(form.password2, tabindex=240) }}
|
||||||
</div>
|
|
||||||
|
{{ render_submit_field(form.submit, tabindex=280) }}
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -104,26 +104,11 @@
|
|||||||
|
|
||||||
<div class="box-body">
|
<div class="box-body">
|
||||||
<p>
|
<p>
|
||||||
List of your topics without a matching package.
|
List of your forum topics which do not have a matching package.
|
||||||
Powered by Krock's Mod Search.
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<table>
|
{% from "macros/topics.html" import render_topics_table %}
|
||||||
<tr>
|
{{ render_topics_table(topics_to_add, show_author=False) }}
|
||||||
<th>Id</th>
|
|
||||||
<th>Title</th>
|
|
||||||
<th>Name</th>
|
|
||||||
<th>Link</th>
|
|
||||||
</tr>
|
|
||||||
{% for topic in topics_to_add %}
|
|
||||||
<tr>
|
|
||||||
<td>{{ topic.topic_id }}</td>
|
|
||||||
<td>[{{ topic.getType().value }}] <a href="https://forum.minetest.net/viewtopic.php?t={{ topic.topic_id}}">{{ topic.title }}</a></td>
|
|
||||||
<td>{{ topic.name or ""}}</td>
|
|
||||||
<td><a href="{{ topic.link }}">{{ topic.link | domain }}</a></td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
21
app/utils.py
21
app/utils.py
@@ -50,6 +50,25 @@ def doFileUpload(file, allowedExtensions, fileTypeName):
|
|||||||
file.save(os.path.join("app/public/uploads", filename))
|
file.save(os.path.join("app/public/uploads", filename))
|
||||||
return "/uploads/" + filename
|
return "/uploads/" + filename
|
||||||
|
|
||||||
|
def make_flask_user_password(plaintext_str):
|
||||||
|
# http://passlib.readthedocs.io/en/stable/modular_crypt_format.html
|
||||||
|
# http://passlib.readthedocs.io/en/stable/lib/passlib.hash.bcrypt.html#format-algorithm
|
||||||
|
# Flask_User stores passwords in the Modular Crypt Format.
|
||||||
|
# https://github.com/lingthio/Flask-User/blob/master/flask_user/user_manager__settings.py#L166
|
||||||
|
# Note that Flask_User allows customizing password algorithms.
|
||||||
|
# USER_PASSLIB_CRYPTCONTEXT_SCHEMES defaults to bcrypt but if
|
||||||
|
# default changes or is customized, the code below needs adapting.
|
||||||
|
# Individual password values will look like:
|
||||||
|
# $2b$12$.az4S999Ztvy/wa3UdQvMOpcki1Qn6VYPXmEFMIdWQyYs7ULnH.JW
|
||||||
|
# $XX$RR$SSSSSSSSSSSSSSSSSSSSSSHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH
|
||||||
|
# $XX : Selects algorithm (2b is bcrypt).
|
||||||
|
# $RR : Selects bcrypt key expansion rounds (12 is 2**12 rounds).
|
||||||
|
# $SSS... : 22 chars of (random, per-password) salt
|
||||||
|
# HHH... : 31 remaining chars of password hash (note no dollar sign)
|
||||||
|
import bcrypt
|
||||||
|
plaintext = plaintext_str.encode("UTF-8")
|
||||||
|
password = bcrypt.hashpw(plaintext, bcrypt.gensalt())
|
||||||
|
return password.decode("UTF-8")
|
||||||
|
|
||||||
def _do_login_user(user, remember_me=False):
|
def _do_login_user(user, remember_me=False):
|
||||||
def _call_or_get(v):
|
def _call_or_get(v):
|
||||||
@@ -99,7 +118,7 @@ def loginUser(user):
|
|||||||
if user_manager.enable_username:
|
if user_manager.enable_username:
|
||||||
user_mixin = user_manager.find_user_by_username(user.username)
|
user_mixin = user_manager.find_user_by_username(user.username)
|
||||||
|
|
||||||
return _do_login_user(user_mixin, False)
|
return _do_login_user(user_mixin, True)
|
||||||
|
|
||||||
def rank_required(rank):
|
def rank_required(rank):
|
||||||
def decorator(f):
|
def decorator(f):
|
||||||
|
|||||||
@@ -46,12 +46,15 @@ def send_upload(path):
|
|||||||
@app.route("/")
|
@app.route("/")
|
||||||
@menu.register_menu(app, ".", "Home")
|
@menu.register_menu(app, ".", "Home")
|
||||||
def home_page():
|
def home_page():
|
||||||
query = Package.query.filter_by(approved=True, soft_deleted=False)
|
query = Package.query.filter_by(approved=True, soft_deleted=False)
|
||||||
count = query.count()
|
count = query.count()
|
||||||
packages = query.order_by(db.desc(Package.created_at)).limit(15).all()
|
new = query.order_by(db.desc(Package.created_at)).limit(15).all()
|
||||||
return render_template("index.html", packages=packages, count=count)
|
popular = query.order_by(db.desc(Package.score)).limit(6).all()
|
||||||
|
return render_template("index.html", new=new, popular=popular, count=count)
|
||||||
|
|
||||||
from . import users, githublogin, packages, sass, tasks, admin, notifications, tagseditor, meta, thumbnails
|
from . import users, githublogin, packages, meta, threads, api
|
||||||
|
from . import tasks, admin, notifications, tagseditor, licenseseditor
|
||||||
|
from . import sass, thumbnails
|
||||||
|
|
||||||
@menu.register_menu(app, ".help", "Help", order=19, endpoint_arguments_constructor=lambda: { 'path': 'help' })
|
@menu.register_menu(app, ".help", "Help", order=19, endpoint_arguments_constructor=lambda: { 'path': 'help' })
|
||||||
@app.route('/<path:path>/')
|
@app.route('/<path:path>/')
|
||||||
|
|||||||
@@ -20,22 +20,52 @@ from flask_user import *
|
|||||||
from flask.ext import menu
|
from flask.ext import menu
|
||||||
from app import app
|
from app import app
|
||||||
from app.models import *
|
from app.models import *
|
||||||
from app.tasks.importtasks import importRepoScreenshot, importAllDependencies
|
from celery import uuid
|
||||||
from app.tasks.forumtasks import importUsersFromModList, importKrocksModList
|
from app.tasks.importtasks import importRepoScreenshot, importAllDependencies, makeVCSRelease
|
||||||
|
from app.tasks.forumtasks import importTopicList
|
||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from wtforms import *
|
from wtforms import *
|
||||||
from app.utils import loginUser, rank_required
|
from app.utils import loginUser, rank_required, triggerNotif
|
||||||
|
import datetime
|
||||||
|
from flask_admin import Admin
|
||||||
|
from flask_admin.contrib.sqla import ModelView
|
||||||
|
|
||||||
|
class MyModelView(ModelView):
|
||||||
|
def is_accessible(self):
|
||||||
|
return current_user.is_authenticated and current_user.rank.atLeast(UserRank.ADMIN)
|
||||||
|
|
||||||
|
def inaccessible_callback(self, name, **kwargs):
|
||||||
|
# redirect to login page if user doesn't have access
|
||||||
|
if current_user.is_authenticated:
|
||||||
|
abort(403)
|
||||||
|
else:
|
||||||
|
return redirect(url_for('user.login', next=request.url))
|
||||||
|
|
||||||
|
admin = Admin(app, name='ContentDB', template_mode='bootstrap3', url="/admin/db")
|
||||||
|
admin.add_view(MyModelView(User, db.session))
|
||||||
|
admin.add_view(MyModelView(Package, db.session))
|
||||||
|
admin.add_view(MyModelView(Dependency, db.session))
|
||||||
|
admin.add_view(MyModelView(EditRequest, db.session))
|
||||||
|
admin.add_view(MyModelView(EditRequestChange, db.session))
|
||||||
|
admin.add_view(MyModelView(ForumTopic, db.session))
|
||||||
|
admin.add_view(MyModelView(License, db.session))
|
||||||
|
admin.add_view(MyModelView(MetaPackage, db.session))
|
||||||
|
admin.add_view(MyModelView(Notification, db.session))
|
||||||
|
admin.add_view(MyModelView(PackageRelease, db.session))
|
||||||
|
admin.add_view(MyModelView(PackageScreenshot, db.session))
|
||||||
|
admin.add_view(MyModelView(Tag, db.session))
|
||||||
|
admin.add_view(MyModelView(Thread, db.session))
|
||||||
|
admin.add_view(MyModelView(ThreadReply, db.session))
|
||||||
|
admin.add_view(MyModelView(UserEmailVerification, db.session))
|
||||||
|
|
||||||
|
|
||||||
@app.route("/admin/", methods=["GET", "POST"])
|
@app.route("/admin/", methods=["GET", "POST"])
|
||||||
@rank_required(UserRank.ADMIN)
|
@rank_required(UserRank.ADMIN)
|
||||||
def admin_page():
|
def admin_page():
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
action = request.form["action"]
|
action = request.form["action"]
|
||||||
if action == "importusers":
|
if action == "importmodlist":
|
||||||
task = importUsersFromModList.delay()
|
task = importTopicList.delay()
|
||||||
return redirect(url_for("check_task", id=task.id, r=url_for("user_list_page")))
|
|
||||||
elif action == "importmodlist":
|
|
||||||
task = importKrocksModList.delay()
|
|
||||||
return redirect(url_for("check_task", id=task.id, r=url_for("todo_topics_page")))
|
return redirect(url_for("check_task", id=task.id, r=url_for("todo_topics_page")))
|
||||||
elif action == "importscreenshots":
|
elif action == "importscreenshots":
|
||||||
packages = Package.query \
|
packages = Package.query \
|
||||||
@@ -67,6 +97,31 @@ def admin_page():
|
|||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return redirect(url_for("admin_page"))
|
return redirect(url_for("admin_page"))
|
||||||
|
elif action == "recalcscores":
|
||||||
|
for p in Package.query.all():
|
||||||
|
p.recalcScore()
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
return redirect(url_for("admin_page"))
|
||||||
|
elif action == "vcsrelease":
|
||||||
|
for package in Package.query.filter(Package.repo.isnot(None)).all():
|
||||||
|
if package.releases.count() != 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
rel = PackageRelease()
|
||||||
|
rel.package = package
|
||||||
|
rel.title = datetime.date.today().isoformat()
|
||||||
|
rel.url = ""
|
||||||
|
rel.task_id = uuid()
|
||||||
|
rel.approved = True
|
||||||
|
db.session.add(rel)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
makeVCSRelease.apply_async((rel.id, "master"), task_id=rel.task_id)
|
||||||
|
|
||||||
|
msg = "{}: Release {} created".format(package.title, rel.title)
|
||||||
|
triggerNotif(package.author, current_user, msg, rel.getEditURL())
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
else:
|
else:
|
||||||
flash("Unknown action: " + action, "error")
|
flash("Unknown action: " + action, "error")
|
||||||
@@ -74,6 +129,7 @@ def admin_page():
|
|||||||
deleted_packages = Package.query.filter_by(soft_deleted=True).all()
|
deleted_packages = Package.query.filter_by(soft_deleted=True).all()
|
||||||
return render_template("admin/list.html", deleted_packages=deleted_packages)
|
return render_template("admin/list.html", deleted_packages=deleted_packages)
|
||||||
|
|
||||||
|
|
||||||
class SwitchUserForm(FlaskForm):
|
class SwitchUserForm(FlaskForm):
|
||||||
username = StringField("Username")
|
username = StringField("Username")
|
||||||
submit = SubmitField("Switch")
|
submit = SubmitField("Switch")
|
||||||
|
|||||||
45
app/views/api.py
Normal file
45
app/views/api.py
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# Content DB
|
||||||
|
# Copyright (C) 2018 rubenwardy
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
|
from flask import *
|
||||||
|
from flask_user import *
|
||||||
|
from app import app
|
||||||
|
from app.models import *
|
||||||
|
from app.utils import is_package_page
|
||||||
|
from .packages import QueryBuilder
|
||||||
|
|
||||||
|
@app.route("/api/packages/")
|
||||||
|
def api_packages_page():
|
||||||
|
qb = QueryBuilder()
|
||||||
|
query = qb.buildPackageQuery()
|
||||||
|
|
||||||
|
pkgs = [package.getAsDictionaryShort(app.config["BASE_URL"]) \
|
||||||
|
for package in query.all() if package.getDownloadRelease() is not None]
|
||||||
|
return jsonify(pkgs)
|
||||||
|
|
||||||
|
@app.route("/api/packages/<author>/<name>/")
|
||||||
|
@is_package_page
|
||||||
|
def api_package_page(package):
|
||||||
|
return jsonify(package.getAsDictionary(app.config["BASE_URL"]))
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/topics/")
|
||||||
|
def api_topics_page():
|
||||||
|
query = ForumTopic.query \
|
||||||
|
.order_by(db.asc(ForumTopic.wip), db.asc(ForumTopic.name), db.asc(ForumTopic.title))
|
||||||
|
pkgs = [t.getAsDictionary() for t in query.all()]
|
||||||
|
return jsonify(pkgs)
|
||||||
@@ -51,9 +51,9 @@ def github_authorized(oauth_token):
|
|||||||
if current_user and current_user.is_authenticated:
|
if current_user and current_user.is_authenticated:
|
||||||
if userByGithub is None:
|
if userByGithub is None:
|
||||||
current_user.github_username = username
|
current_user.github_username = username
|
||||||
db.session.add(auth)
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return redirect(url_for("gitAccount", id=auth.id))
|
flash("Linked github to account", "success")
|
||||||
|
return redirect(url_for("home_page"))
|
||||||
else:
|
else:
|
||||||
flash("Github account is already associated with another user", "danger")
|
flash("Github account is already associated with another user", "danger")
|
||||||
return redirect(url_for("home_page"))
|
return redirect(url_for("home_page"))
|
||||||
@@ -64,7 +64,10 @@ def github_authorized(oauth_token):
|
|||||||
flash("Unable to find an account for that Github user", "error")
|
flash("Unable to find an account for that Github user", "error")
|
||||||
return redirect(url_for("user_claim_page"))
|
return redirect(url_for("user_claim_page"))
|
||||||
elif loginUser(userByGithub):
|
elif loginUser(userByGithub):
|
||||||
return redirect(next_url or url_for("home_page"))
|
if current_user.password is None:
|
||||||
|
return redirect(next_url or url_for("set_password_page", optional=True))
|
||||||
|
else:
|
||||||
|
return redirect(next_url or url_for("home_page"))
|
||||||
else:
|
else:
|
||||||
flash("Authorization failed [err=gh-login-failed]", "danger")
|
flash("Authorization failed [err=gh-login-failed]", "danger")
|
||||||
return redirect(url_for("user.login"))
|
return redirect(url_for("user.login"))
|
||||||
|
|||||||
62
app/views/licenseseditor.py
Normal file
62
app/views/licenseseditor.py
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
# Content DB
|
||||||
|
# Copyright (C) 2018 rubenwardy
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
|
from flask import *
|
||||||
|
from flask_user import *
|
||||||
|
from app import app
|
||||||
|
from app.models import *
|
||||||
|
from flask_wtf import FlaskForm
|
||||||
|
from wtforms import *
|
||||||
|
from wtforms.validators import *
|
||||||
|
from app.utils import rank_required
|
||||||
|
|
||||||
|
@app.route("/licenses/")
|
||||||
|
@rank_required(UserRank.MODERATOR)
|
||||||
|
def license_list_page():
|
||||||
|
return render_template("admin/licenses/list.html", licenses=License.query.order_by(db.asc(License.name)).all())
|
||||||
|
|
||||||
|
class LicenseForm(FlaskForm):
|
||||||
|
name = StringField("Name", [InputRequired(), Length(3,100)])
|
||||||
|
is_foss = BooleanField("Is FOSS")
|
||||||
|
submit = SubmitField("Save")
|
||||||
|
|
||||||
|
@app.route("/licenses/new/", methods=["GET", "POST"])
|
||||||
|
@app.route("/licenses/<name>/edit/", methods=["GET", "POST"])
|
||||||
|
@rank_required(UserRank.MODERATOR)
|
||||||
|
def createedit_license_page(name=None):
|
||||||
|
license = None
|
||||||
|
if name is not None:
|
||||||
|
license = License.query.filter_by(name=name).first()
|
||||||
|
if license is None:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
form = LicenseForm(formdata=request.form, obj=license)
|
||||||
|
if request.method == "GET" and license is None:
|
||||||
|
form.is_foss.data = True
|
||||||
|
elif request.method == "POST" and form.validate():
|
||||||
|
if license is None:
|
||||||
|
license = License(form.name.data)
|
||||||
|
db.session.add(license)
|
||||||
|
flash("Created license " + form.name.data, "success")
|
||||||
|
else:
|
||||||
|
flash("Updated license " + form.name.data, "success")
|
||||||
|
|
||||||
|
form.populate_obj(license)
|
||||||
|
db.session.commit()
|
||||||
|
return redirect(url_for("license_list_page"))
|
||||||
|
|
||||||
|
return render_template("admin/licenses/edit.html", license=license, form=form)
|
||||||
@@ -29,48 +29,102 @@ from flask_wtf import FlaskForm
|
|||||||
from wtforms import *
|
from wtforms import *
|
||||||
from wtforms.validators import *
|
from wtforms.validators import *
|
||||||
from wtforms.ext.sqlalchemy.fields import QuerySelectField, QuerySelectMultipleField
|
from wtforms.ext.sqlalchemy.fields import QuerySelectField, QuerySelectMultipleField
|
||||||
|
from sqlalchemy import or_, any_
|
||||||
|
|
||||||
# TODO: the following could be made into one route, except I"m not sure how
|
|
||||||
# to do the menu
|
class QueryBuilder:
|
||||||
|
title = None
|
||||||
|
types = None
|
||||||
|
search = None
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
title = "Packages"
|
||||||
|
|
||||||
|
# Get request types
|
||||||
|
types = request.args.getlist("type")
|
||||||
|
types = [PackageType.get(tname) for tname in types]
|
||||||
|
types = [type for type in types if type is not None]
|
||||||
|
if len(types) > 0:
|
||||||
|
title = ", ".join([type.value + "s" for type in types])
|
||||||
|
|
||||||
|
self.title = title
|
||||||
|
self.types = types
|
||||||
|
self.search = request.args.get("q")
|
||||||
|
self.lucky = "lucky" in request.args
|
||||||
|
self.limit = 1 if self.lucky else None
|
||||||
|
|
||||||
|
def buildPackageQuery(self):
|
||||||
|
query = Package.query.filter_by(soft_deleted=False, approved=True)
|
||||||
|
|
||||||
|
if len(self.types) > 0:
|
||||||
|
query = query.filter(Package.type.in_(self.types))
|
||||||
|
|
||||||
|
if self.search is not None and self.search.strip() != "":
|
||||||
|
query = query.filter(Package.title.ilike('%' + self.search + '%'))
|
||||||
|
|
||||||
|
query = query.order_by(db.desc(Package.score))
|
||||||
|
|
||||||
|
if self.limit:
|
||||||
|
query = query.limit(self.limit)
|
||||||
|
|
||||||
|
return query
|
||||||
|
|
||||||
|
def buildTopicQuery(self):
|
||||||
|
topics = ForumTopic.query \
|
||||||
|
.filter(~ db.exists().where(Package.forums==ForumTopic.topic_id)) \
|
||||||
|
.order_by(db.asc(ForumTopic.wip), db.asc(ForumTopic.name), db.asc(ForumTopic.title)) \
|
||||||
|
.filter(ForumTopic.title.ilike('%' + self.search + '%'))
|
||||||
|
|
||||||
|
if len(self.types) > 0:
|
||||||
|
topics = topics.filter(ForumTopic.type.in_(self.types))
|
||||||
|
|
||||||
|
if self.limit:
|
||||||
|
topics = topics.limit(self.limit)
|
||||||
|
|
||||||
|
return topics
|
||||||
|
|
||||||
@menu.register_menu(app, ".mods", "Mods", order=11, endpoint_arguments_constructor=lambda: { 'type': 'mod' })
|
@menu.register_menu(app, ".mods", "Mods", order=11, endpoint_arguments_constructor=lambda: { 'type': 'mod' })
|
||||||
@menu.register_menu(app, ".games", "Games", order=12, endpoint_arguments_constructor=lambda: { 'type': 'game' })
|
@menu.register_menu(app, ".games", "Games", order=12, endpoint_arguments_constructor=lambda: { 'type': 'game' })
|
||||||
@menu.register_menu(app, ".txp", "Texture Packs", order=13, endpoint_arguments_constructor=lambda: { 'type': 'txp' })
|
@menu.register_menu(app, ".txp", "Texture Packs", order=13, endpoint_arguments_constructor=lambda: { 'type': 'txp' })
|
||||||
@app.route("/packages/")
|
@app.route("/packages/")
|
||||||
def packages_page():
|
def packages_page():
|
||||||
type = request.args.get("type")
|
if shouldReturnJson():
|
||||||
if type is not None:
|
return redirect(url_for("api_packages_page"))
|
||||||
type = PackageType[type.upper()]
|
|
||||||
|
|
||||||
title = "Packages"
|
qb = QueryBuilder()
|
||||||
query = Package.query.filter_by(soft_deleted=False)
|
query = qb.buildPackageQuery()
|
||||||
|
title = qb.title
|
||||||
|
|
||||||
if type is not None:
|
if qb.lucky:
|
||||||
title = type.value + "s"
|
package = query.first()
|
||||||
query = query.filter_by(type=type, approved=True)
|
if package:
|
||||||
|
return redirect(package.getDetailsURL())
|
||||||
|
|
||||||
|
topic = qb.buildTopicQuery().first()
|
||||||
|
if topic:
|
||||||
|
return redirect("https://forum.minetest.net/viewtopic.php?t=" + str(topic.topic_id))
|
||||||
|
|
||||||
|
page = int(request.args.get("page") or 1)
|
||||||
|
num = min(42, int(request.args.get("n") or 100))
|
||||||
|
query = query.paginate(page, num, True)
|
||||||
|
|
||||||
search = request.args.get("q")
|
search = request.args.get("q")
|
||||||
if search is not None:
|
type_name = request.args.get("type")
|
||||||
query = query.filter(Package.title.ilike('%' + search + '%'))
|
|
||||||
|
|
||||||
if shouldReturnJson():
|
next_url = url_for("packages_page", type=type_name, q=search, page=query.next_num) \
|
||||||
pkgs = [package.getAsDictionary(app.config["BASE_URL"]) \
|
if query.has_next else None
|
||||||
for package in query.all() if package.getDownloadRelease() is not None]
|
prev_url = url_for("packages_page", type=type_name, q=search, page=query.prev_num) \
|
||||||
return jsonify(pkgs)
|
if query.has_prev else None
|
||||||
else:
|
|
||||||
page = int(request.args.get("page") or 1)
|
|
||||||
num = min(42, int(request.args.get("n") or 100))
|
|
||||||
query = query.paginate(page, num, True)
|
|
||||||
|
|
||||||
next_url = url_for("packages_page", type=type.toName(), q=search, page=query.next_num) \
|
topics = None
|
||||||
if query.has_next else None
|
if qb.search and not query.has_next:
|
||||||
prev_url = url_for("packages_page", type=type.toName(), q=search, page=query.prev_num) \
|
topics = qb.buildTopicQuery().all()
|
||||||
if query.has_prev else None
|
|
||||||
|
|
||||||
tags = Tag.query.all()
|
tags = Tag.query.all()
|
||||||
return render_template("packages/list.html", title=title, packages=query.items, \
|
return render_template("packages/list.html", \
|
||||||
query=search, tags=tags, type=None if type is None else type.toName(), \
|
title=title, packages=query.items, topics=topics, \
|
||||||
next_url=next_url, prev_url=prev_url, page=page, page_max=query.pages, packages_count=query.total)
|
query=search, tags=tags, type=type_name, \
|
||||||
|
next_url=next_url, prev_url=prev_url, page=page, page_max=query.pages, packages_count=query.total)
|
||||||
|
|
||||||
|
|
||||||
def getReleases(package):
|
def getReleases(package):
|
||||||
@@ -83,24 +137,68 @@ def getReleases(package):
|
|||||||
@app.route("/packages/<author>/<name>/")
|
@app.route("/packages/<author>/<name>/")
|
||||||
@is_package_page
|
@is_package_page
|
||||||
def package_page(package):
|
def package_page(package):
|
||||||
if shouldReturnJson():
|
clearNotifications(package.getDetailsURL())
|
||||||
return jsonify(package.getAsDictionary(app.config["BASE_URL"]))
|
|
||||||
else:
|
|
||||||
clearNotifications(package.getDetailsURL())
|
|
||||||
|
|
||||||
alternatives = None
|
alternatives = None
|
||||||
if package.type == PackageType.MOD:
|
if package.type == PackageType.MOD:
|
||||||
alternatives = Package.query \
|
alternatives = Package.query \
|
||||||
.filter_by(name=package.name, type=PackageType.MOD, soft_deleted=False) \
|
.filter_by(name=package.name, type=PackageType.MOD, soft_deleted=False) \
|
||||||
.filter(Package.id != package.id) \
|
.filter(Package.id != package.id) \
|
||||||
.order_by(db.asc(Package.title)) \
|
.order_by(db.asc(Package.title)) \
|
||||||
.all()
|
.all()
|
||||||
|
|
||||||
releases = getReleases(package)
|
show_similar_topics = current_user == package.author or \
|
||||||
requests = [r for r in package.requests if r.status == 0]
|
package.checkPerm(current_user, Permission.APPROVE_NEW)
|
||||||
return render_template("packages/view.html", \
|
|
||||||
package=package, releases=releases, requests=requests, \
|
similar_topics = None if not show_similar_topics else \
|
||||||
alternatives=alternatives)
|
ForumTopic.query \
|
||||||
|
.filter_by(name=package.name) \
|
||||||
|
.filter(ForumTopic.topic_id != package.forums) \
|
||||||
|
.filter(~ db.exists().where(Package.forums==ForumTopic.topic_id)) \
|
||||||
|
.order_by(db.asc(ForumTopic.name), db.asc(ForumTopic.title)) \
|
||||||
|
.all()
|
||||||
|
|
||||||
|
releases = getReleases(package)
|
||||||
|
requests = [r for r in package.requests if r.status == 0]
|
||||||
|
|
||||||
|
review_thread = package.review_thread
|
||||||
|
if review_thread is not None and not review_thread.checkPerm(current_user, Permission.SEE_THREAD):
|
||||||
|
review_thread = None
|
||||||
|
|
||||||
|
topic_error = None
|
||||||
|
topic_error_lvl = "warning"
|
||||||
|
if not package.approved and package.forums is not None:
|
||||||
|
errors = []
|
||||||
|
if Package.query.filter_by(forums=package.forums, soft_deleted=False).count() > 1:
|
||||||
|
errors.append("<b>Error: Another package already uses this forum topic!</b>")
|
||||||
|
topic_error_lvl = "error"
|
||||||
|
|
||||||
|
topic = ForumTopic.query.get(package.forums)
|
||||||
|
if topic is not None:
|
||||||
|
if topic.author != package.author:
|
||||||
|
errors.append("<b>Error: Forum topic author doesn't match package author.</b>")
|
||||||
|
topic_error_lvl = "error"
|
||||||
|
|
||||||
|
if topic.wip:
|
||||||
|
errors.append("Warning: Forum topic is in WIP section, make sure package meets playability standards.")
|
||||||
|
elif package.type != PackageType.TXP:
|
||||||
|
errors.append("Warning: Forum topic not found. This may happen if the topic has only just been created.")
|
||||||
|
|
||||||
|
topic_error = "<br />".join(errors)
|
||||||
|
|
||||||
|
|
||||||
|
threads = Thread.query.filter_by(package_id=package.id)
|
||||||
|
if not current_user.is_authenticated:
|
||||||
|
threads = threads.filter_by(private=False)
|
||||||
|
elif not current_user.rank.atLeast(UserRank.EDITOR) and not current_user == package.author:
|
||||||
|
threads = threads.filter(or_(Thread.private == False, Thread.author == current_user))
|
||||||
|
|
||||||
|
|
||||||
|
return render_template("packages/view.html", \
|
||||||
|
package=package, releases=releases, requests=requests, \
|
||||||
|
alternatives=alternatives, similar_topics=similar_topics, \
|
||||||
|
review_thread=review_thread, topic_error=topic_error, topic_error_lvl=topic_error_lvl, \
|
||||||
|
threads=threads.all())
|
||||||
|
|
||||||
|
|
||||||
@app.route("/packages/<author>/<name>/download/")
|
@app.route("/packages/<author>/<name>/download/")
|
||||||
@@ -125,13 +223,13 @@ class PackageForm(FlaskForm):
|
|||||||
shortDesc = StringField("Short Description (Plaintext)", [InputRequired(), Length(1,200)])
|
shortDesc = StringField("Short Description (Plaintext)", [InputRequired(), Length(1,200)])
|
||||||
desc = TextAreaField("Long Description (Markdown)", [Optional(), Length(0,10000)])
|
desc = TextAreaField("Long Description (Markdown)", [Optional(), Length(0,10000)])
|
||||||
type = SelectField("Type", [InputRequired()], choices=PackageType.choices(), coerce=PackageType.coerce, default=PackageType.MOD)
|
type = SelectField("Type", [InputRequired()], choices=PackageType.choices(), coerce=PackageType.coerce, default=PackageType.MOD)
|
||||||
license = QuerySelectField("License", [InputRequired()], query_factory=lambda: License.query, get_pk=lambda a: a.id, get_label=lambda a: a.name)
|
license = QuerySelectField("License", [InputRequired()], query_factory=lambda: License.query.order_by(db.asc(License.name)), get_pk=lambda a: a.id, get_label=lambda a: a.name)
|
||||||
media_license = QuerySelectField("Media License", [InputRequired()], query_factory=lambda: License.query, get_pk=lambda a: a.id, get_label=lambda a: a.name)
|
media_license = QuerySelectField("Media License", [InputRequired()], query_factory=lambda: License.query.order_by(db.asc(License.name)), get_pk=lambda a: a.id, get_label=lambda a: a.name)
|
||||||
provides_str = StringField("Provides (mods included in package)", [Optional(), Length(0,1000)])
|
provides_str = StringField("Provides (mods included in package)", [Optional(), Length(0,1000)])
|
||||||
tags = QuerySelectMultipleField('Tags', query_factory=lambda: Tag.query.order_by(db.asc(Tag.name)), get_pk=lambda a: a.id, get_label=lambda a: a.title)
|
tags = QuerySelectMultipleField('Tags', query_factory=lambda: Tag.query.order_by(db.asc(Tag.name)), get_pk=lambda a: a.id, get_label=lambda a: a.title)
|
||||||
harddep_str = StringField("Hard Dependencies", [Optional(), Length(0,1000)])
|
harddep_str = StringField("Hard Dependencies", [Optional(), Length(0,1000)])
|
||||||
softdep_str = StringField("Soft Dependencies", [Optional(), Length(0,1000)])
|
softdep_str = StringField("Soft Dependencies", [Optional(), Length(0,1000)])
|
||||||
repo = StringField("Repo URL", [Optional(), URL()])
|
repo = StringField("VCS Repository URL", [Optional(), URL()])
|
||||||
website = StringField("Website URL", [Optional(), URL()])
|
website = StringField("Website URL", [Optional(), URL()])
|
||||||
issueTracker = StringField("Issue Tracker URL", [Optional(), URL()])
|
issueTracker = StringField("Issue Tracker URL", [Optional(), URL()])
|
||||||
forums = IntegerField("Forum Topic ID", [Optional(), NumberRange(0,999999)])
|
forums = IntegerField("Forum Topic ID", [Optional(), NumberRange(0,999999)])
|
||||||
@@ -168,11 +266,17 @@ def create_edit_package_page(author=None, name=None):
|
|||||||
form = PackageForm(formdata=request.form, obj=package)
|
form = PackageForm(formdata=request.form, obj=package)
|
||||||
|
|
||||||
# Initial form class from post data and default data
|
# Initial form class from post data and default data
|
||||||
if request.method == "GET" and package is not None:
|
if request.method == "GET":
|
||||||
deps = package.dependencies
|
if package is None:
|
||||||
form.harddep_str.data = ",".join([str(x) for x in deps if not x.optional])
|
form.name.data = request.args.get("bname")
|
||||||
form.softdep_str.data = ",".join([str(x) for x in deps if x.optional])
|
form.title.data = request.args.get("title")
|
||||||
form.provides_str.data = MetaPackage.ListToSpec(package.provides)
|
form.repo.data = request.args.get("repo")
|
||||||
|
form.forums.data = request.args.get("forums")
|
||||||
|
else:
|
||||||
|
deps = package.dependencies
|
||||||
|
form.harddep_str.data = ",".join([str(x) for x in deps if not x.optional])
|
||||||
|
form.softdep_str.data = ",".join([str(x) for x in deps if x.optional])
|
||||||
|
form.provides_str.data = MetaPackage.ListToSpec(package.provides)
|
||||||
|
|
||||||
if request.method == "POST" and form.validate():
|
if request.method == "POST" and form.validate():
|
||||||
wasNew = False
|
wasNew = False
|
||||||
@@ -221,11 +325,15 @@ def create_edit_package_page(author=None, name=None):
|
|||||||
|
|
||||||
db.session.commit() # save
|
db.session.commit() # save
|
||||||
|
|
||||||
if wasNew and package.canImportScreenshot():
|
next_url = package.getDetailsURL()
|
||||||
|
if wasNew and package.repo is not None:
|
||||||
task = importRepoScreenshot.delay(package.id)
|
task = importRepoScreenshot.delay(package.id)
|
||||||
return redirect(url_for("check_task", id=task.id, r=package.getDetailsURL()))
|
next_url = url_for("check_task", id=task.id, r=next_url)
|
||||||
|
|
||||||
return redirect(package.getDetailsURL())
|
if wasNew and ("WTFPL" in package.license.name or "WTFPL" in package.media_license.name):
|
||||||
|
next_url = url_for("flatpage", path="help/wtfpl", r=next_url)
|
||||||
|
|
||||||
|
return redirect(next_url)
|
||||||
|
|
||||||
package_query = Package.query.filter_by(approved=True, soft_deleted=False)
|
package_query = Package.query.filter_by(approved=True, soft_deleted=False)
|
||||||
if package is not None:
|
if package is not None:
|
||||||
|
|||||||
@@ -52,8 +52,8 @@ def create_release_page(package):
|
|||||||
|
|
||||||
# Initial form class from post data and default data
|
# Initial form class from post data and default data
|
||||||
form = CreatePackageReleaseForm()
|
form = CreatePackageReleaseForm()
|
||||||
if package.canMakeReleaseFromVCS():
|
if package.repo is not None:
|
||||||
form["uploadOpt"].choices = [("vcs", "From VCS Commit or Branch"), ("upload", "File Upload")]
|
form["uploadOpt"].choices = [("vcs", "From Git Commit or Branch"), ("upload", "File Upload")]
|
||||||
if request.method != "POST":
|
if request.method != "POST":
|
||||||
form["uploadOpt"].data = "vcs"
|
form["uploadOpt"].data = "vcs"
|
||||||
|
|
||||||
@@ -91,6 +91,23 @@ def create_release_page(package):
|
|||||||
|
|
||||||
return render_template("packages/release_new.html", package=package, form=form)
|
return render_template("packages/release_new.html", package=package, form=form)
|
||||||
|
|
||||||
|
@app.route("/packages/<author>/<name>/releases/<id>/download/")
|
||||||
|
@is_package_page
|
||||||
|
def download_release_page(package, id):
|
||||||
|
release = PackageRelease.query.get(id)
|
||||||
|
if release is None or release.package != package:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
if release is None:
|
||||||
|
if "application/zip" in request.accept_mimetypes and \
|
||||||
|
not "text/html" in request.accept_mimetypes:
|
||||||
|
return "", 204
|
||||||
|
else:
|
||||||
|
flash("No download available.", "error")
|
||||||
|
return redirect(package.getDetailsURL())
|
||||||
|
else:
|
||||||
|
return redirect(release.url, code=300)
|
||||||
|
|
||||||
@app.route("/packages/<author>/<name>/releases/<id>/", methods=["GET", "POST"])
|
@app.route("/packages/<author>/<name>/releases/<id>/", methods=["GET", "POST"])
|
||||||
@login_required
|
@login_required
|
||||||
@is_package_page
|
@is_package_page
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ def create_screenshot_page(package, id=None):
|
|||||||
if uploadedPath is not None:
|
if uploadedPath is not None:
|
||||||
ss = PackageScreenshot()
|
ss = PackageScreenshot()
|
||||||
ss.package = package
|
ss.package = package
|
||||||
ss.title = form["title"].data
|
ss.title = form["title"].data or "Untitled"
|
||||||
ss.url = uploadedPath
|
ss.url = uploadedPath
|
||||||
db.session.add(ss)
|
db.session.add(ss)
|
||||||
|
|
||||||
@@ -91,7 +91,7 @@ def edit_screenshot_page(package, id):
|
|||||||
wasApproved = screenshot.approved
|
wasApproved = screenshot.approved
|
||||||
|
|
||||||
if canEdit:
|
if canEdit:
|
||||||
screenshot.title = form["title"].data
|
screenshot.title = form["title"].data or "Untitled"
|
||||||
|
|
||||||
if canApprove:
|
if canApprove:
|
||||||
screenshot.approved = form["approved"].data
|
screenshot.approved = form["approved"].data
|
||||||
|
|||||||
@@ -41,8 +41,8 @@ def todo_page():
|
|||||||
screenshots = PackageScreenshot.query.filter_by(approved=False).all()
|
screenshots = PackageScreenshot.query.filter_by(approved=False).all()
|
||||||
|
|
||||||
|
|
||||||
topics_to_add = KrockForumTopic.query \
|
topics_to_add = ForumTopic.query \
|
||||||
.filter(~ db.exists().where(Package.forums==KrockForumTopic.topic_id)) \
|
.filter(~ db.exists().where(Package.forums==ForumTopic.topic_id)) \
|
||||||
.count()
|
.count()
|
||||||
|
|
||||||
return render_template("todo/list.html", title="Reports and Work Queue",
|
return render_template("todo/list.html", title="Reports and Work Queue",
|
||||||
@@ -54,11 +54,11 @@ def todo_page():
|
|||||||
@app.route("/todo/topics/")
|
@app.route("/todo/topics/")
|
||||||
@login_required
|
@login_required
|
||||||
def todo_topics_page():
|
def todo_topics_page():
|
||||||
total = KrockForumTopic.query.count()
|
total = ForumTopic.query.count()
|
||||||
|
|
||||||
topics = KrockForumTopic.query \
|
topics = ForumTopic.query \
|
||||||
.filter(~ db.exists().where(Package.forums==KrockForumTopic.topic_id)) \
|
.filter(~ db.exists().where(Package.forums==ForumTopic.topic_id)) \
|
||||||
.order_by(db.asc(KrockForumTopic.name), db.asc(KrockForumTopic.title)) \
|
.order_by(db.asc(ForumTopic.wip), db.asc(ForumTopic.name), db.asc(ForumTopic.title)) \
|
||||||
.all()
|
.all()
|
||||||
|
|
||||||
return render_template("todo/topics.html", topics=topics, total=total)
|
return render_template("todo/topics.html", topics=topics, total=total)
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ from app.utils import rank_required
|
|||||||
@app.route("/tags/")
|
@app.route("/tags/")
|
||||||
@rank_required(UserRank.MODERATOR)
|
@rank_required(UserRank.MODERATOR)
|
||||||
def tag_list_page():
|
def tag_list_page():
|
||||||
return render_template("tags/list.html", tags=Tag.query.order_by(db.asc(Tag.title)).all())
|
return render_template("admin/tags/list.html", tags=Tag.query.order_by(db.asc(Tag.title)).all())
|
||||||
|
|
||||||
class TagForm(FlaskForm):
|
class TagForm(FlaskForm):
|
||||||
title = StringField("Title", [InputRequired(), Length(3,100)])
|
title = StringField("Title", [InputRequired(), Length(3,100)])
|
||||||
@@ -54,4 +54,4 @@ def createedit_tag_page(name=None):
|
|||||||
db.session.commit()
|
db.session.commit()
|
||||||
return redirect(url_for("createedit_tag_page", name=tag.name))
|
return redirect(url_for("createedit_tag_page", name=tag.name))
|
||||||
|
|
||||||
return render_template("tags/edit.html", tag=tag, form=form)
|
return render_template("admin/tags/edit.html", tag=tag, form=form)
|
||||||
|
|||||||
190
app/views/threads.py
Normal file
190
app/views/threads.py
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
# Content DB
|
||||||
|
# Copyright (C) 2018 rubenwardy
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
|
from flask import *
|
||||||
|
from flask_user import *
|
||||||
|
from app import app
|
||||||
|
from app.models import *
|
||||||
|
from app.utils import triggerNotif, clearNotifications
|
||||||
|
|
||||||
|
from flask_wtf import FlaskForm
|
||||||
|
from wtforms import *
|
||||||
|
from wtforms.validators import *
|
||||||
|
|
||||||
|
@app.route("/threads/")
|
||||||
|
def threads_page():
|
||||||
|
query = Thread.query
|
||||||
|
if not Permission.SEE_THREAD.check(current_user):
|
||||||
|
query = query.filter_by(private=False)
|
||||||
|
return render_template("threads/list.html", threads=query.all())
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/threads/<int:id>/subscribe/", methods=["POST"])
|
||||||
|
@login_required
|
||||||
|
def thread_subscribe_page(id):
|
||||||
|
thread = Thread.query.get(id)
|
||||||
|
if thread is None or not thread.checkPerm(current_user, Permission.SEE_THREAD):
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
if current_user in thread.watchers:
|
||||||
|
flash("Already subscribed!", "success")
|
||||||
|
else:
|
||||||
|
flash("Subscribed to thread", "success")
|
||||||
|
thread.watchers.append(current_user)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return redirect(url_for("thread_page", id=id))
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/threads/<int:id>/unsubscribe/", methods=["POST"])
|
||||||
|
@login_required
|
||||||
|
def thread_unsubscribe_page(id):
|
||||||
|
thread = Thread.query.get(id)
|
||||||
|
if thread is None or not thread.checkPerm(current_user, Permission.SEE_THREAD):
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
if current_user in thread.watchers:
|
||||||
|
flash("Unsubscribed!", "success")
|
||||||
|
thread.watchers.remove(current_user)
|
||||||
|
db.session.commit()
|
||||||
|
else:
|
||||||
|
flash("Not subscribed to thread", "success")
|
||||||
|
|
||||||
|
return redirect(url_for("thread_page", id=id))
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/threads/<int:id>/", methods=["GET", "POST"])
|
||||||
|
def thread_page(id):
|
||||||
|
clearNotifications(url_for("thread_page", id=id))
|
||||||
|
|
||||||
|
thread = Thread.query.get(id)
|
||||||
|
if thread is None or not thread.checkPerm(current_user, Permission.SEE_THREAD):
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
if current_user.is_authenticated and request.method == "POST":
|
||||||
|
comment = request.form["comment"]
|
||||||
|
|
||||||
|
if len(comment) <= 500 and len(comment) > 3:
|
||||||
|
reply = ThreadReply()
|
||||||
|
reply.author = current_user
|
||||||
|
reply.comment = comment
|
||||||
|
db.session.add(reply)
|
||||||
|
|
||||||
|
thread.replies.append(reply)
|
||||||
|
if not current_user in thread.watchers:
|
||||||
|
thread.watchers.append(current_user)
|
||||||
|
|
||||||
|
msg = None
|
||||||
|
if thread.package is None:
|
||||||
|
msg = "New comment on '{}'".format(thread.title)
|
||||||
|
else:
|
||||||
|
msg = "New comment on '{}' on package {}".format(thread.title, thread.package.title)
|
||||||
|
|
||||||
|
|
||||||
|
for user in thread.watchers:
|
||||||
|
if user != current_user:
|
||||||
|
triggerNotif(user, current_user, msg, url_for("thread_page", id=thread.id))
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return redirect(url_for("thread_page", id=id))
|
||||||
|
|
||||||
|
else:
|
||||||
|
flash("Comment needs to be between 3 and 500 characters.")
|
||||||
|
|
||||||
|
return render_template("threads/view.html", thread=thread)
|
||||||
|
|
||||||
|
|
||||||
|
class ThreadForm(FlaskForm):
|
||||||
|
title = StringField("Title", [InputRequired(), Length(3,100)])
|
||||||
|
comment = TextAreaField("Comment", [InputRequired(), Length(10, 500)])
|
||||||
|
private = BooleanField("Private")
|
||||||
|
submit = SubmitField("Open Thread")
|
||||||
|
|
||||||
|
@app.route("/threads/new/", methods=["GET", "POST"])
|
||||||
|
@login_required
|
||||||
|
def new_thread_page():
|
||||||
|
form = ThreadForm(formdata=request.form)
|
||||||
|
|
||||||
|
package = None
|
||||||
|
if "pid" in request.args:
|
||||||
|
package = Package.query.get(int(request.args.get("pid")))
|
||||||
|
if package is None:
|
||||||
|
flash("Unable to find that package!", "error")
|
||||||
|
|
||||||
|
# Don't allow making threads on approved packages for now
|
||||||
|
if package is None:
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
def_is_private = request.args.get("private") or False
|
||||||
|
if not package.approved:
|
||||||
|
def_is_private = True
|
||||||
|
allow_change = package.approved
|
||||||
|
is_review_thread = package is not None and not package.approved
|
||||||
|
|
||||||
|
# Check that user can make the thread
|
||||||
|
if not package.checkPerm(current_user, Permission.CREATE_THREAD):
|
||||||
|
flash("Unable to create thread!", "error")
|
||||||
|
return redirect(url_for("home_page"))
|
||||||
|
|
||||||
|
# Only allow creating one thread when not approved
|
||||||
|
elif is_review_thread and package.review_thread is not None:
|
||||||
|
flash("A review thread already exists!", "error")
|
||||||
|
if request.method == "GET":
|
||||||
|
return redirect(url_for("thread_page", id=package.review_thread.id))
|
||||||
|
|
||||||
|
# Set default values
|
||||||
|
elif request.method == "GET":
|
||||||
|
form.private.data = def_is_private
|
||||||
|
form.title.data = request.args.get("title") or ""
|
||||||
|
|
||||||
|
# Validate and submit
|
||||||
|
elif request.method == "POST" and form.validate():
|
||||||
|
thread = Thread()
|
||||||
|
thread.author = current_user
|
||||||
|
thread.title = form.title.data
|
||||||
|
thread.private = form.private.data if allow_change else def_is_private
|
||||||
|
thread.package = package
|
||||||
|
db.session.add(thread)
|
||||||
|
|
||||||
|
thread.watchers.append(current_user)
|
||||||
|
if package is not None and package.author != current_user:
|
||||||
|
thread.watchers.append(package.author)
|
||||||
|
|
||||||
|
reply = ThreadReply()
|
||||||
|
reply.thread = thread
|
||||||
|
reply.author = current_user
|
||||||
|
reply.comment = form.comment.data
|
||||||
|
db.session.add(reply)
|
||||||
|
|
||||||
|
thread.replies.append(reply)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
if is_review_thread:
|
||||||
|
package.review_thread = thread
|
||||||
|
|
||||||
|
if package is not None:
|
||||||
|
triggerNotif(package.author, current_user,
|
||||||
|
"New thread '{}' on package {}".format(thread.title, package.title), url_for("thread_page", id=thread.id))
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return redirect(url_for("thread_page", id=thread.id))
|
||||||
|
|
||||||
|
|
||||||
|
return render_template("threads/new.html", form=form, allow_private_change=allow_change)
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
from flask import *
|
from flask import *
|
||||||
from app import app
|
from app import app
|
||||||
|
|
||||||
import glob, os
|
import os
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
ALLOWED_RESOLUTIONS=[(350,233)]
|
ALLOWED_RESOLUTIONS=[(350,233)]
|
||||||
@@ -29,6 +29,33 @@ def mkdir(path):
|
|||||||
|
|
||||||
mkdir("app/public/thumbnails/")
|
mkdir("app/public/thumbnails/")
|
||||||
|
|
||||||
|
def resize_and_crop(img_path, modified_path, size):
|
||||||
|
img = Image.open(img_path)
|
||||||
|
|
||||||
|
# Get current and desired ratio for the images
|
||||||
|
img_ratio = img.size[0] / float(img.size[1])
|
||||||
|
ratio = size[0] / float(size[1])
|
||||||
|
|
||||||
|
# Is more portrait than target, scale and crop
|
||||||
|
if ratio > img_ratio:
|
||||||
|
img = img.resize((int(size[0]), int(size[0] * img.size[1] / img.size[0])),
|
||||||
|
Image.BICUBIC)
|
||||||
|
box = (0, (img.size[1] - size[1]) / 2, img.size[0], (img.size[1] + size[1]) / 2)
|
||||||
|
img = img.crop(box)
|
||||||
|
|
||||||
|
# Is more landscape than target, scale and crop
|
||||||
|
elif ratio < img_ratio:
|
||||||
|
img = img.resize((int(size[1] * img.size[0] / img.size[1]), int(size[1])),
|
||||||
|
Image.BICUBIC)
|
||||||
|
box = ((img.size[0] - size[0]) / 2, 0, (img.size[0] + size[0]) / 2, img.size[1])
|
||||||
|
img = img.crop(box)
|
||||||
|
|
||||||
|
# Is exactly the same ratio as target
|
||||||
|
else:
|
||||||
|
img = img.resize(size, Image.BICUBIC)
|
||||||
|
|
||||||
|
img.save(modified_path)
|
||||||
|
|
||||||
@app.route("/thumbnails/<img>")
|
@app.route("/thumbnails/<img>")
|
||||||
@app.route("/thumbnails/<int:w>x<int:h>/<img>")
|
@app.route("/thumbnails/<int:w>x<int:h>/<img>")
|
||||||
def make_thumbnail(img, w=350, h=233):
|
def make_thumbnail(img, w=350, h=233):
|
||||||
@@ -40,7 +67,5 @@ def make_thumbnail(img, w=350, h=233):
|
|||||||
cache_filepath = "public/thumbnails/{}x{}/{}".format(w, h, img)
|
cache_filepath = "public/thumbnails/{}x{}/{}".format(w, h, img)
|
||||||
source_filepath = "public/uploads/" + img
|
source_filepath = "public/uploads/" + img
|
||||||
|
|
||||||
im = Image.open("app/" + source_filepath)
|
resize_and_crop("app/" + source_filepath, "app/" + cache_filepath, (w, h))
|
||||||
im.thumbnail((w, h), Image.ANTIALIAS)
|
|
||||||
im.save("app/" + cache_filepath, optimize=True)
|
|
||||||
return send_file(cache_filepath)
|
return send_file(cache_filepath)
|
||||||
|
|||||||
@@ -98,10 +98,10 @@ def user_profile_page(username):
|
|||||||
|
|
||||||
topics_to_add = None
|
topics_to_add = None
|
||||||
if current_user == user or user.checkPerm(current_user, Permission.CHANGE_AUTHOR):
|
if current_user == user or user.checkPerm(current_user, Permission.CHANGE_AUTHOR):
|
||||||
topics_to_add = KrockForumTopic.query \
|
topics_to_add = ForumTopic.query \
|
||||||
.filter_by(author_id=user.id) \
|
.filter_by(author_id=user.id) \
|
||||||
.filter(~ db.exists().where(Package.forums==KrockForumTopic.topic_id)) \
|
.filter(~ db.exists().where(Package.forums==ForumTopic.topic_id)) \
|
||||||
.order_by(db.asc(KrockForumTopic.name), db.asc(KrockForumTopic.title)) \
|
.order_by(db.asc(ForumTopic.name), db.asc(ForumTopic.title)) \
|
||||||
.all()
|
.all()
|
||||||
|
|
||||||
# Process GET or invalid POST
|
# Process GET or invalid POST
|
||||||
@@ -109,7 +109,7 @@ def user_profile_page(username):
|
|||||||
user=user, form=form, packages=packages, topics_to_add=topics_to_add)
|
user=user, form=form, packages=packages, topics_to_add=topics_to_add)
|
||||||
|
|
||||||
class SetPasswordForm(FlaskForm):
|
class SetPasswordForm(FlaskForm):
|
||||||
email = StringField("Email (Optional)", [Optional(), Email()])
|
email = StringField("Email", [Optional(), Email()])
|
||||||
password = PasswordField("New password", [InputRequired(), Length(2, 20)])
|
password = PasswordField("New password", [InputRequired(), Length(2, 20)])
|
||||||
password2 = PasswordField("Verify password", [InputRequired(), Length(2, 20)])
|
password2 = PasswordField("Verify password", [InputRequired(), Length(2, 20)])
|
||||||
submit = SubmitField("Save")
|
submit = SubmitField("Save")
|
||||||
@@ -121,6 +121,9 @@ def set_password_page():
|
|||||||
return redirect(url_for("user.change_password"))
|
return redirect(url_for("user.change_password"))
|
||||||
|
|
||||||
form = SetPasswordForm(request.form)
|
form = SetPasswordForm(request.form)
|
||||||
|
if current_user.email == None:
|
||||||
|
form.email.validators = [InputRequired(), Email()]
|
||||||
|
|
||||||
if request.method == "POST" and form.validate():
|
if request.method == "POST" and form.validate():
|
||||||
one = form.password.data
|
one = form.password.data
|
||||||
two = form.password2.data
|
two = form.password2.data
|
||||||
@@ -159,7 +162,7 @@ def set_password_page():
|
|||||||
else:
|
else:
|
||||||
flash("Passwords do not match", "error")
|
flash("Passwords do not match", "error")
|
||||||
|
|
||||||
return render_template("users/set_password.html", form=form)
|
return render_template("users/set_password.html", form=form, optional=request.args.get("optional"))
|
||||||
|
|
||||||
|
|
||||||
@app.route("/user/claim/", methods=["GET", "POST"])
|
@app.route("/user/claim/", methods=["GET", "POST"])
|
||||||
|
|||||||
@@ -22,3 +22,4 @@ MAIL_DEFAULT_SENDER=""
|
|||||||
MAIL_SERVER=""
|
MAIL_SERVER=""
|
||||||
MAIL_PORT=587
|
MAIL_PORT=587
|
||||||
MAIL_USE_TLS=True
|
MAIL_USE_TLS=True
|
||||||
|
MAIL_UTILS_ERROR_SEND_TO=[""]
|
||||||
|
|||||||
28
migrations/versions/11b6ef362f98_.py
Normal file
28
migrations/versions/11b6ef362f98_.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: 11b6ef362f98
|
||||||
|
Revises: 9fc23495713b
|
||||||
|
Create Date: 2018-07-04 01:01:45.440662
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '11b6ef362f98'
|
||||||
|
down_revision = '9fc23495713b'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('package', sa.Column('score', sa.Float(), nullable=False, server_default="0.0"))
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_column('package', 'score')
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -19,17 +19,11 @@ depends_on = None
|
|||||||
|
|
||||||
def upgrade():
|
def upgrade():
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
op.alter_column('user','username', type_=ty.VARCHAR(50, collation='NOCASE'))
|
pass
|
||||||
op.alter_column('user','github_username', type_=ty.VARCHAR(50, collation='NOCASE'))
|
|
||||||
op.alter_column('user','forums_username', type_=ty.VARCHAR(50, collation='NOCASE'))
|
|
||||||
op.create_index(op.f('ix_user_username'), 'user', ['username'], unique=True)
|
|
||||||
# ### end Alembic commands ###
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
def downgrade():
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
op.alter_column('user','username', type_=ty.VARCHAR(50))
|
pass
|
||||||
op.alter_column('user','github_username', type_=ty.VARCHAR(50))
|
|
||||||
op.alter_column('user','forums_username', type_=ty.VARCHAR(50))
|
|
||||||
op.drop_index(op.f('ix_user_username'), table_name='user')
|
|
||||||
# ### end Alembic commands ###
|
# ### end Alembic commands ###
|
||||||
|
|||||||
28
migrations/versions/44e138485931_.py
Normal file
28
migrations/versions/44e138485931_.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: 44e138485931
|
||||||
|
Revises: 9e2ac631efb0
|
||||||
|
Create Date: 2018-07-28 14:45:28.879331
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '44e138485931'
|
||||||
|
down_revision = '9e2ac631efb0'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('package_release', sa.Column('commit_hash', sa.String(length=41), nullable=True, server_default=None))
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_column('package_release', 'commit_hash')
|
||||||
|
# ### end Alembic commands ###
|
||||||
55
migrations/versions/605b3d74ada1_.py
Normal file
55
migrations/versions/605b3d74ada1_.py
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: 605b3d74ada1
|
||||||
|
Revises: 28a427cbd4cf
|
||||||
|
Create Date: 2018-06-11 22:50:36.828818
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '605b3d74ada1'
|
||||||
|
down_revision = '28a427cbd4cf'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('thread',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('package_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('author_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('title', sa.String(length=100), nullable=False),
|
||||||
|
sa.Column('private', sa.Boolean(), server_default='0', nullable=True),
|
||||||
|
sa.Column('created_at', sa.DateTime(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['author_id'], ['user.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['package_id'], ['package.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_table('thread_reply',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('thread_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('comment', sa.String(length=500), nullable=False),
|
||||||
|
sa.Column('author_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['author_id'], ['user.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['thread_id'], ['thread.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
|
||||||
|
op.add_column('package', sa.Column('review_thread_id', sa.Integer(), nullable=True))
|
||||||
|
op.create_foreign_key(None, 'package', 'thread', ['review_thread_id'], ['id'])
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_constraint(None, 'package', type_='foreignkey')
|
||||||
|
op.drop_constraint(None, 'package', type_='foreignkey')
|
||||||
|
op.drop_column('package', 'review_thread_id')
|
||||||
|
op.drop_table('thread_reply')
|
||||||
|
op.drop_table('thread')
|
||||||
|
# ### end Alembic commands ###
|
||||||
28
migrations/versions/9e2ac631efb0_.py
Normal file
28
migrations/versions/9e2ac631efb0_.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: 9e2ac631efb0
|
||||||
|
Revises: 11b6ef362f98
|
||||||
|
Create Date: 2018-07-06 23:16:50.507010
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '9e2ac631efb0'
|
||||||
|
down_revision = '11b6ef362f98'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('forum_topic', sa.Column('wip', sa.Boolean(), nullable=False, server_default="0"))
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_column('forum_topic', 'wip')
|
||||||
|
# ### end Alembic commands ###
|
||||||
55
migrations/versions/9fc23495713b_.py
Normal file
55
migrations/versions/9fc23495713b_.py
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: 9fc23495713b
|
||||||
|
Revises: de004661c5e1
|
||||||
|
Create Date: 2018-07-04 00:03:20.123285
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '9fc23495713b'
|
||||||
|
down_revision = 'de004661c5e1'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
from sqlalchemy.dialects.postgresql import ENUM
|
||||||
|
|
||||||
|
type_enum = ENUM('MOD', 'GAME', 'TXP', name='packagetype', create_type=False)
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
type_enum.create(op.get_bind(), checkfirst=True)
|
||||||
|
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_table('krock_forum_topic')
|
||||||
|
op.create_table('forum_topic',
|
||||||
|
sa.Column('topic_id', sa.Integer(), autoincrement=False, nullable=False),
|
||||||
|
sa.Column('author_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('type', type_enum, nullable=True),
|
||||||
|
sa.Column('title', sa.String(length=200), nullable=False),
|
||||||
|
sa.Column('name', sa.String(length=30), nullable=True),
|
||||||
|
sa.Column('link', sa.String(length=200), nullable=True),
|
||||||
|
sa.Column('posts', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('views', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['author_id'], ['user.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('topic_id')
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_table('forum_topic')
|
||||||
|
op.create_table('krock_forum_topic',
|
||||||
|
sa.Column('topic_id', sa.Integer(), autoincrement=False, nullable=False),
|
||||||
|
sa.Column('author_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('ttype', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('title', sa.String(length=200), nullable=False),
|
||||||
|
sa.Column('name', sa.String(length=30), nullable=True),
|
||||||
|
sa.Column('link', sa.String(length=50), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['author_id'], ['user.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('topic_id')
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
34
migrations/versions/de004661c5e1_.py
Normal file
34
migrations/versions/de004661c5e1_.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: de004661c5e1
|
||||||
|
Revises: 605b3d74ada1
|
||||||
|
Create Date: 2018-06-11 23:38:38.611039
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'de004661c5e1'
|
||||||
|
down_revision = '605b3d74ada1'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('watchers',
|
||||||
|
sa.Column('user_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('thread_id', sa.Integer(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['thread_id'], ['thread.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('user_id', 'thread_id')
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_table('watchers')
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
Flask>=0.12.2
|
Flask>=0.12.2,<1.0
|
||||||
Flask-SQLAlchemy>=2.3
|
Flask-SQLAlchemy>=2.3
|
||||||
Flask-Login>=0.4.1
|
Flask-Login>=0.4.1
|
||||||
Flask-User==0.6.19
|
Flask-User==0.6.19
|
||||||
@@ -13,3 +13,5 @@ lxml==4.2.1
|
|||||||
Flask-FlatPages==0.6
|
Flask-FlatPages==0.6
|
||||||
Flask-Migrate==2.1.1
|
Flask-Migrate==2.1.1
|
||||||
pillow==5.1.0
|
pillow==5.1.0
|
||||||
|
GitPython==2.1.10
|
||||||
|
flask-admin==1.5.1
|
||||||
|
|||||||
7
setup.py
7
setup.py
@@ -23,6 +23,7 @@ if not "FLASK_CONFIG" in os.environ:
|
|||||||
test_data = len(sys.argv) >= 2 and sys.argv[1].strip() == "-t"
|
test_data = len(sys.argv) >= 2 and sys.argv[1].strip() == "-t"
|
||||||
|
|
||||||
from app.models import *
|
from app.models import *
|
||||||
|
from app.utils import make_flask_user_password
|
||||||
|
|
||||||
def defineDummyData(licenses, tags, ruben):
|
def defineDummyData(licenses, tags, ruben):
|
||||||
ez = User("Shara")
|
ez = User("Shara")
|
||||||
@@ -342,6 +343,8 @@ db.create_all()
|
|||||||
print("Filling database...")
|
print("Filling database...")
|
||||||
|
|
||||||
ruben = User("rubenwardy")
|
ruben = User("rubenwardy")
|
||||||
|
ruben.active = True
|
||||||
|
ruben.password = make_flask_user_password("tuckfrump")
|
||||||
ruben.github_username = "rubenwardy"
|
ruben.github_username = "rubenwardy"
|
||||||
ruben.forums_username = "rubenwardy"
|
ruben.forums_username = "rubenwardy"
|
||||||
ruben.rank = UserRank.ADMIN
|
ruben.rank = UserRank.ADMIN
|
||||||
@@ -359,12 +362,12 @@ for tag in ["Inventory", "Mapgen", "Building", \
|
|||||||
licenses = {}
|
licenses = {}
|
||||||
for license in ["GPLv2.1", "GPLv3", "LGPLv2.1", "LGPLv3", "AGPLv2.1", "AGPLv3",
|
for license in ["GPLv2.1", "GPLv3", "LGPLv2.1", "LGPLv3", "AGPLv2.1", "AGPLv3",
|
||||||
"Apache", "BSD 3-Clause", "BSD 2-Clause", "CC0", "CC-BY-SA",
|
"Apache", "BSD 3-Clause", "BSD 2-Clause", "CC0", "CC-BY-SA",
|
||||||
"CC-BY", "MIT", "ZLib"]:
|
"CC-BY", "MIT", "ZLib", "Other (Free)"]:
|
||||||
row = License(license)
|
row = License(license)
|
||||||
licenses[row.name] = row
|
licenses[row.name] = row
|
||||||
db.session.add(row)
|
db.session.add(row)
|
||||||
|
|
||||||
for license in ["CC-BY-NC-SA"]:
|
for license in ["CC-BY-NC-SA", "Other (Non-free)"]:
|
||||||
row = License(license, False)
|
row = License(license, False)
|
||||||
licenses[row.name] = row
|
licenses[row.name] = row
|
||||||
db.session.add(row)
|
db.session.add(row)
|
||||||
|
|||||||
Reference in New Issue
Block a user