Migrate to Bulma

Bulma is simple and free full-CSS framework. Unlike Bootstrap, it fits
within a single small CSS file. It is also well integrated in Django.
https://bulma.io/
This commit is contained in:
Rodolphe Breard 2018-06-03 22:16:40 +02:00
parent 2e7c42f61f
commit b0b8f30580
50 changed files with 439 additions and 21641 deletions

View file

@ -1,18 +1,14 @@
[[source]]
url = "https://pypi.python.org/simple"
verify_ssl = true
name = "pypi"
[packages]
django = "*"
python-decouple = "*"
markdown = "*"
django-npb = "*"
django-bulma = "*"
[dev-packages]
gunicorn = "*"

View file

@ -49,6 +49,7 @@ You can set the following variables in the `.env` file:
* `KHAGANAT_EMAIL_PORT`: Port to use for the SMTP server, default is `25`.
* `KHAGANAT_EMAIL_HOST_USER`: Username to use for the SMTP server, default is empty (no authentication).
* `KHAGANAT_EMAIL_HOST_PASSWORD`: Password to use for the SMTP server, default is empty.
* `KHAGANAT_EMAIL_USE_STARTTLS`: Whether to use STARTTLS to connect to the SMTP server, default is `False`.
* `KHAGANAT_EMAIL_USE_TLS`: Whether to use a TLS connection to the SMTP server, default is `False`.
* `KHAGANAT_EMAIL_SUBJECT_PREFIX`: Subject-line prefix for email, default is empty.
* `KHAGANAT_DEFAULT_FROM_EMAIL`: Default email address to use, default is `no-reply@localhost`.

View file

@ -47,6 +47,7 @@ INSTALLED_APPS = [
'navbar.apps.NavbarConfig',
'logs.apps.LogsConfig',
'npb.apps.NpbConfig',
'bulma',
]
MIDDLEWARE = [
@ -99,7 +100,8 @@ EMAIL_HOST = config('KHAGANAT_EMAIL_HOST', default='localhost')
EMAIL_PORT = config('KHAGANAT_EMAIL_PORT', default=25, cast=int)
EMAIL_HOST_USER = config('KHAGANAT_EMAIL_HOST_USER', default='')
EMAIL_HOST_PASSWORD = config('KHAGANAT_EMAIL_HOST_PASSWORD', default='')
EMAIL_USE_TLS = config('KHAGANAT_EMAIL_USE_TLS', default=False, cast=bool)
EMAIL_USE_TLS = config('KHAGANAT_EMAIL_USE_STARTTLS', default=False, cast=bool)
EMAIL_USE_SSL = config('KHAGANAT_EMAIL_USE_TLS', default=False, cast=bool)
EMAIL_SUBJECT_PREFIX = config('KHAGANAT_EMAIL_SUBJECT_PREFIX', default='')
DEFAULT_FROM_EMAIL = config(

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,343 +0,0 @@
/*!
* Bootstrap Reboot v4.0.0-beta.3 (https://getbootstrap.com)
* Copyright 2011-2017 The Bootstrap Authors
* Copyright 2011-2017 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
*/
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
font-family: sans-serif;
line-height: 1.15;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
-ms-overflow-style: scrollbar;
-webkit-tap-highlight-color: transparent;
}
@-ms-viewport {
width: device-width;
}
article, aside, dialog, figcaption, figure, footer, header, hgroup, main, nav, section {
display: block;
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: #212529;
text-align: left;
background-color: #fff;
}
[tabindex="-1"]:focus {
outline: 0 !important;
}
hr {
box-sizing: content-box;
height: 0;
overflow: visible;
}
h1, h2, h3, h4, h5, h6 {
margin-top: 0;
margin-bottom: 0.5rem;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
abbr[title],
abbr[data-original-title] {
text-decoration: underline;
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
border-bottom: 0;
}
address {
margin-bottom: 1rem;
font-style: normal;
line-height: inherit;
}
ol,
ul,
dl {
margin-top: 0;
margin-bottom: 1rem;
}
ol ol,
ul ul,
ol ul,
ul ol {
margin-bottom: 0;
}
dt {
font-weight: 700;
}
dd {
margin-bottom: .5rem;
margin-left: 0;
}
blockquote {
margin: 0 0 1rem;
}
dfn {
font-style: italic;
}
b,
strong {
font-weight: bolder;
}
small {
font-size: 80%;
}
sub,
sup {
position: relative;
font-size: 75%;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -.25em;
}
sup {
top: -.5em;
}
a {
color: #007bff;
text-decoration: none;
background-color: transparent;
-webkit-text-decoration-skip: objects;
}
a:hover {
color: #0056b3;
text-decoration: underline;
}
a:not([href]):not([tabindex]) {
color: inherit;
text-decoration: none;
}
a:not([href]):not([tabindex]):focus, a:not([href]):not([tabindex]):hover {
color: inherit;
text-decoration: none;
}
a:not([href]):not([tabindex]):focus {
outline: 0;
}
pre,
code,
kbd,
samp {
font-family: monospace, monospace;
font-size: 1em;
}
pre {
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
-ms-overflow-style: scrollbar;
}
figure {
margin: 0 0 1rem;
}
img {
vertical-align: middle;
border-style: none;
}
svg:not(:root) {
overflow: hidden;
}
a,
area,
button,
[role="button"],
input:not([type="range"]),
label,
select,
summary,
textarea {
-ms-touch-action: manipulation;
touch-action: manipulation;
}
table {
border-collapse: collapse;
}
caption {
padding-top: 0.75rem;
padding-bottom: 0.75rem;
color: #868e96;
text-align: left;
caption-side: bottom;
}
th {
text-align: inherit;
}
label {
display: inline-block;
margin-bottom: .5rem;
}
button {
border-radius: 0;
}
button:focus {
outline: 1px dotted;
outline: 5px auto -webkit-focus-ring-color;
}
input,
button,
select,
optgroup,
textarea {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
button,
input {
overflow: visible;
}
button,
select {
text-transform: none;
}
button,
html [type="button"],
[type="reset"],
[type="submit"] {
-webkit-appearance: button;
}
button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner {
padding: 0;
border-style: none;
}
input[type="radio"],
input[type="checkbox"] {
box-sizing: border-box;
padding: 0;
}
input[type="date"],
input[type="time"],
input[type="datetime-local"],
input[type="month"] {
-webkit-appearance: listbox;
}
textarea {
overflow: auto;
resize: vertical;
}
fieldset {
min-width: 0;
padding: 0;
margin: 0;
border: 0;
}
legend {
display: block;
width: 100%;
max-width: 100%;
padding: 0;
margin-bottom: .5rem;
font-size: 1.5rem;
line-height: inherit;
color: inherit;
white-space: normal;
}
progress {
vertical-align: baseline;
}
[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
height: auto;
}
[type="search"] {
outline-offset: -2px;
-webkit-appearance: none;
}
[type="search"]::-webkit-search-cancel-button,
[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-file-upload-button {
font: inherit;
-webkit-appearance: button;
}
output {
display: inline-block;
}
summary {
display: list-item;
cursor: pointer;
}
template {
display: none;
}
[hidden] {
display: none !important;
}
/*# sourceMappingURL=bootstrap-reboot.css.map */

File diff suppressed because one or more lines are too long

View file

@ -1,8 +0,0 @@
/*!
* Bootstrap Reboot v4.0.0-beta.3 (https://getbootstrap.com)
* Copyright 2011-2017 The Bootstrap Authors
* Copyright 2011-2017 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
*/*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;-ms-overflow-style:scrollbar;-webkit-tap-highlight-color:transparent}@-ms-viewport{width:device-width}article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus{outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}dfn{font-style:italic}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent;-webkit-text-decoration-skip:objects}a:hover{color:#0056b3;text-decoration:underline}a:not([href]):not([tabindex]){color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus,a:not([href]):not([tabindex]):hover{color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus{outline:0}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto;-ms-overflow-style:scrollbar}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg:not(:root){overflow:hidden}[role=button],a,area,button,input:not([type=range]),label,select,summary,textarea{-ms-touch-action:manipulation;touch-action:manipulation}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#868e96;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important}
/*# sourceMappingURL=bootstrap-reboot.min.css.map */

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 162 KiB

File diff suppressed because one or more lines are too long

View file

@ -1,3 +0,0 @@
$(function () {
$('[data-toggle="tooltip"]').tooltip()
})

View file

@ -1,49 +1,46 @@
{% load static %}{% load i18n %}{% load navbar %}{% load get_page %}{% get_current_language as LANGUAGE_CODE %}<!doctype html>
<html lang="{{ LANGUAGE_CODE }}">
<head>
<!-- Begin bootstrap headers -->
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<link rel="stylesheet" href="{% static "khaganat/bootstrap/4.0.0-beta.3/css/bootstrap.min.css" %}" integrity="sha384-Zug+QiDoJOrZ5t4lssLdxGhVrurbmBWopoEl+M6BdEfwnCJZtKxi1KgxUyJq13dy" />
<!-- End bootstrap headers -->
<!-- Begin global headers -->
<link rel="shortcut icon" href="{% static "khaganat/images/favicon.ico" %}" />
<link rel="stylesheet" href="{% static "khaganat/css/khaganat.css" %}" />
<!-- End global headers -->
<!-- Begin custom headers -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="{% static 'bulma/css/font-awesome.min.css' %}">
<link rel="stylesheet" href="{% static 'bulma/css/style.css' %}">
<link rel="shortcut icon" href="{% static "khaganat/images/favicon.ico" %}">
<!-- <link rel="stylesheet" href="{% static "khaganat/css/khaganat.css" %}" /> -->
<script defer src="{% static "khaganat/js/khaganat.js" %}"></script>
{% block headers %}{% endblock %}
<!-- End custom headers -->
<title>Khaganat - {% block title %}{% endblock %}</title>
</head>
<body>
{% navbar %}
<div class="container" id="main-content">
<section class="section">
{% if messages %}
<div id="messages">
<div class="container">
{% for message in messages %}
<div class="alert alert-{% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %}danger{% elif message.level == DEFAULT_MESSAGE_LEVELS.WARNING %}warning{% elif message.level == DEFAULT_MESSAGE_LEVELS.SUCCESS %}success{% elif message.level == DEFAULT_MESSAGE_LEVELS.INFO %}info{% else %}secondary{% endif %}" role="alert">
{% if 'safe' in message.tags %}
{{ message|safe }}
{% else %}
{{ message }}
{% endif %}
</div>
<article class="message{% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %} is-danger{% elif message.level == DEFAULT_MESSAGE_LEVELS.WARNING %} is-warning{% elif message.level == DEFAULT_MESSAGE_LEVELS.SUCCESS %} is-success{% elif message.level == DEFAULT_MESSAGE_LEVELS.INFO %} is-info{% endif %}">
<div class="message-body">
{% if 'safe' in message.tags %}
{{ message|safe }}
{% else %}
{{ message }}
{% endif %}
</div>
</article>
{% endfor %}
</div>
{% endif %}
{% block content %}{% endblock %}
<footer id="page-footer" class="content-bloc">
<div class="container">
{% block content %}{% endblock %}
</div>
</section>
<footer class="footer">
<div class="container">
{% get_page 'legal' as legal_page %}
{% if legal_page %}
<a href="{% url 'page' 'legal' %}">{{ legal_page.title }}</a>
{% endif %}
</footer>
</div>
<script src="{% static "khaganat/jquery-3.2.1.min.js" %}"></script>
<script src="{% static "khaganat/bootstrap/4.0.0-beta.3/js/bootstrap.bundle.min.js" %}"></script>
<script src="{% static "khaganat/js/khaganat.js" %}"></script>
<ul>
<li><a href="https://khaganat.net/wikhan/fr:licence">Licences</a></li>
{% if legal_page %}<li><a href="{% url 'page' 'legal' %}">{{ legal_page.title }}</a></li>{% endif %}
</ul>
</div>
</footer>
</body>
</html>

View file

@ -0,0 +1,20 @@
{% extends "khaganat/base.html" %}
{% load bulma_tags %}
{% load i18n %}
{% block content %}
<div class="columns">
<div class="column"></div>
<div class="column {% block dialog_size %}{% endblock %}">
<article class="message {% block dialog_class %}{% endblock %}">
<div class="message-header">
<p>{% block title %}{% endblock %}</p>
</div>
<div class="message-body">
{% block dialog %}{% endblock %}
</div>
</article>
</div>
<div class="column"></div>
</div>
{% endblock %}

View file

@ -1 +0,0 @@
{% extends "khaganat/base.html" %}

View file

@ -0,0 +1,35 @@
{% extends "khaganat/base.html" %}
{% load bulma_tags %}
{% load static %}
{% load i18n %}
{% block headers %}
<link rel="stylesheet" href="{% static css_file_name %}">
{% endblock %}
{% block title %}{% trans "Untitled" as untitled %}{{ paste.title|default:untitled }}{% endblock %}
{% block content %}
{% if paste.is_suspended %}
{% else %}
{% trans "Untitled" as untitled %}
<a class="button is-danger is-pulled-right" href="{% url 'npb:report_paste' paste.uuid %}">{% trans 'report this paste'|capfirst %}</a>
<h1 class="title is-3">{{ paste.title|default:untitled }}</h1>
<p class="subtitle is-5">
{% trans "anonymous" as anonymous %}
{% blocktrans with author=paste.author|default:anonymous date=paste.created_on %}By {{ author }} on {{ date }}{% endblocktrans %}
{% if paste.edited %}
<br />
{% blocktrans with date=paste.edited_on %}Edited on {{ date }}{% endblocktrans %}
{% endif %}
{% if paste.expire_on %}
<br />
{% blocktrans with date=paste.expire_on %}Expires on {{ date }}{% endblocktrans %}
{% endif %}
</p>
{{ paste.formated_content|safe }}
{% endif %}
{% endblock %}

View file

@ -0,0 +1,15 @@
{% extends "khaganat/centered_dialog.html" %}
{% load bulma_tags %}
{% load i18n %}
{% block title %}{% trans 'New paste' %}{% endblock %}
{% block dialog_class %}is-link{% endblock %}
{% block dialog_size %}is-two-thirds{% endblock %}
{% block dialog %}
<form action="" method="post">
{% csrf_token %}
{{ form|bulma }}
<button type="submit" class="button is-link">{% trans "Submit" %}</button>
</form>
{% endblock %}

View file

@ -0,0 +1,15 @@
{% extends "khaganat/centered_dialog.html" %}
{% load bulma_tags %}
{% load i18n %}
{% block title %}{% trans 'Report paste' %}{% endblock %}
{% block dialog_class %}is-warning{% endblock %}
{% block dialog_size %}is-two-thirds{% endblock %}
{% block dialog %}
<form action="" method="post">
{% csrf_token %}
{{ form|bulma }}
<button type="submit" class="button is-warning">{% trans "Submit" %}</button>
</form>
{% endblock %}

View file

@ -9,6 +9,6 @@ class SearchForm(forms.Form):
date = forms.DateField(
label=_('date'),
initial=datetime.date.today(),
widget=DateInput(format='%d-%m-%Y'),
input_formats=['%d-%m-%Y'],
widget=DateInput(format='%Y-%m-%d'),
input_formats=['%Y-%m-%d'],
)

View file

@ -1,24 +1,77 @@
{% extends "khaganat/base.html" %}
{% load i18n %}
{% block title %}{{ source_channel.name }}{% endblock %}
{% block title %}{% trans "logs_title" %}{% endblock %}
{% block content %}
<div class="content-bloc">
<h2>{{ source_channel.name }} ({{ date }})</h2>
<div>
<a class="btn btn-primary" href="{% url 'log_index' %}" role="button">{% trans "logs_back" %}</a>
</div>
<div>
{% for entry in entries%}
{% if request.user.is_staff %}
<a href="{% url 'log_switch_entry' entry.pk %}?next={{ request.path_info }}">{% if entry.hidden %}<span class="text-warning"></span>{% else %}<span class="text-success"></span>{% endif %}</a>
{% endif %}
{{ entry.created|date:"H:i:s" }} {% if entry.nick %}<span class="log-nick">&lt;{{ entry.nick }}&gt;</span> <span class="log-content">{{ entry.content }}</span>{% else %}<span class="log-action">* {{ entry.content }}</span>{% endif %}<br />
{% endfor %}
</div>
<div>
<a class="btn btn-primary" href="{% url 'log_index' %}" role="button">{% trans "logs_back" %}</a>
<div class="columns">
<div class="column is-one-quarter">
<nav class="panel">
<p class="panel-heading">{% trans "logs_title" %}</p>
{% for source in sources %}
{% with date=source.first_date %}
<a class="panel-block{% if source.source__id == current_source.id %} is-active has-text-weight-semibold{% endif %}" href="{% url 'log_day' source=source.source__slug year=date.year month=date.month day=date.day %}">{{ source.source__name }}</a>
{% endwith %}
{% empty %}
<a class="panel-block">{% trans "logs_no_logs_available" %}</a>
{% endfor %}
{% if current_source and request.user.is_staff %}
<div class="panel-block">
<form method="post" action="{% url 'log_search' %}">
{% csrf_token %}
<input type="hidden" name="{{ form.source.name }}" value="{{ current_source.slug }}" />
<div class="field has-addons">
<p class="control">
<input class="input" type="date" name="{{ form.date.name }}" value="{{ form.date.value|date:"Y-m-d" }}" id="{{ form.date.id }}" required pattern="[0-9]{4}-[0-9]{2}-[0-9]{2}">
</p>
<p class="control">
<button type="submit" class="button is-link">{% trans 'go'|capfirst %}</button>
</p>
</div>
</form>
</div>
<div class="panel-block">
{% if current_source.hidden %}
{% blocktrans with name=current_source.name %}{{ name }} is hidden.{% endblocktrans %}&nbsp;
<a href="{% url 'log_switch_source' current_source.id %}?next={% url 'log_day' source=current_source.slug year=current_date.year month=current_date.month day=current_date.day %}" class="button is-warning">{% trans 'show'|capfirst %}</a>
{% else %}
{% blocktrans with name=current_source.name %}{{ name }} is published.{% endblocktrans %}&nbsp;
<a href="{% url 'log_switch_source' current_source.id %}?next={% url 'log_day' source=current_source.slug year=current_date.year month=current_date.month day=current_date.day %}" class="button is-warning">{% trans 'hide'|capfirst %}</a>
{% endif %}
</div>
{% endif %}
</nav>
</div>
<div class="column">
{% if dates %}
<div class="tabs">
<ul>
{% for date in dates %}
<li class="{% if date == current_date %}is-active{% endif %}"><a href="{% url 'log_day' source=current_source.slug year=date.year month=date.month day=date.day %}">{{ date }}</a></li>
{% endfor %}
</ul>
</div>
{% endif %}
{% if entries %}
<table class="table is-narrow is-striped">
<tbody>
{% for entry in entries %}
<tr>
{% if request.user.is_staff %}
<td>
<a href="{% url 'log_switch_entry' entry.pk %}?next={{ request.path_info }}">{% if entry.hidden %}<span class="has-text-danger"></span>{% else %}<span class="has-text-success"></span>{% endif %}</a>
</td>
{% endif %}
<td>{{ entry.created|date:"H:i:s" }}</td>
<td class="has-text-right">{% if entry.nick %}{{ entry.nick }}{% else %}*{% endif %}</td>
<td>{{ entry.content }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
</div>
</div>
</div>
{% endblock %}

View file

@ -1,54 +0,0 @@
{% extends "khaganat/base.html" %}
{% load i18n %}
{% block title %}{% trans "logs_title" %}{% endblock %}
{% block content %}
<div class="content-bloc">
<h2>{% trans "logs_title" %}</h2>
{% if sources %}
<div class="row">
<div class="col-4">
<div class="list-group" id="list-tab" role="tablist">
{% for source in sources %}
<a class="list-group-item list-group-item-action" id="list-{{ source.source }}-list" data-toggle="list" href="#list-{{ source.source }}" role="tab" aria-controls="home">{{ source.source__name }}</a>
{% endfor %}
</div>
</div>
<div class="col-8">
<div class="tab-content" id="nav-tabContent">
{% for source in sources %}
<div class="tab-pane fade show" id="list-{{ source.source }}" role="tabpanel" aria-labelledby="list-{{ source.source }}-list">
<div class="list-group">
{% for date in source.stats %}
<a href="{% url 'log_day' source=source.source__slug year=date.date.year month=date.date.month day=date.date.day %}" class="list-group-item list-group-item-action">{{ date.date }}</a>
{% endfor %}
{% if request.user.is_staff %}
<div class="list-group-item list-group-item-action">
<form method="post" action="{% url 'log_search' %}">
{% csrf_token %}
{{ form.date }}
<input type="hidden" name="{{ form.source.name }}" value="{{ source.source__slug }}" />
<input type="submit" value="{% trans 'go'|capfirst %}" class="btn btn-secondary" />
</form>
{% if source.source__hidden %}
{% blocktrans with name=source.source__name %}{{ name }} is hidden.{% endblocktrans %}
<a href="{% url 'log_switch_source' source.source__id %}" class="btn btn-warning">{% trans 'show'|capfirst %}</a>
{% else %}
{% blocktrans with name=source.source__name %}{{ name }} is published.{% endblocktrans %}
<a href="{% url 'log_switch_source' source.source__id %}" class="btn btn-warning">{% trans 'hide'|capfirst %}</a>
{% endif %}
</div>
{% endif %}
</div>
</div>
{% endfor %}
</div>
</div>
</div>
{% else %}
<p>{% trans "logs_no_logs_available" %}</p>
{% endif %}
</div>
{% endblock %}

View file

@ -3,7 +3,7 @@ from . import views
urlpatterns = [
path('', views.IndexView.as_view(), name='log_index'),
path('', views.EntriesView.as_view(), name='log_index'),
path(
'<slug:source>/<int:year>/<int:month>/<int:day>/',
views.EntriesView.as_view(),

View file

@ -9,6 +9,7 @@ from django.http import Http404
from neluser import nsfw
from .models import Source, Entry
from .forms import SearchForm
from utils import is_link_legit
import datetime
@ -28,8 +29,8 @@ def _switch_hidden(request, obj, pk):
e.hidden = not e.hidden
e.save()
next_page = request.GET.get('next', '')
if next_page == '':
next_page = request.GET.get('next', '/')
if not is_link_legit(next_page):
next_page = reverse('log_index')
return redirect(next_page)
@ -45,9 +46,14 @@ def switch_entry(request, pk):
def search_view(request):
print('Debug: 1')
if request.method == 'POST':
print('Debug: 2')
form = SearchForm(request.POST)
print('Debug: form is_valid: %s' % form.is_valid())
print('Debug: form: %s' % form)
if form.is_valid():
print('Debug: 3')
date = form.cleaned_data['date']
return redirect(reverse('log_day', kwargs={
'source': form.cleaned_data['source'],
@ -58,50 +64,9 @@ def search_view(request):
raise Http404('No search parameters.')
class IndexView(generic.ListView):
template_name = 'logs/index.html'
context_object_name = 'sources'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['form'] = SearchForm()
return context
def get_queryset(self):
now, start_date, end_date = _get_dates()
out = []
qs = Entry.objects.all()
if not self.request.user.is_staff:
qs = qs.filter(
hidden=False,
source__hidden=False,
created__range=(start_date, end_date)
)
qs = qs.values(
'source', 'source__name', 'source__slug',
'source__id', 'source__hidden'
).annotate(
nb=Count('id')
)
for src in qs:
src['stats'] = Entry.objects.filter(
hidden=False,
source=src['source'],
created__range=(start_date, end_date)
).annotate(
date=TruncDate('created')
).values('date').annotate(nb=Count('date')).order_by('-date')
out.append(src)
return out
class EntriesView(generic.ListView):
context_object_name = 'entries'
template_name = 'logs/entries.html'
allow_empty = False
context_object_name = 'entries'
def is_nsfw(self):
for e in self.get_queryset():
@ -118,27 +83,87 @@ class EntriesView(generic.ListView):
nsfw.alert(request)
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['date'] = datetime.date(
def get_date(self):
"""Return the date object corresponding to the URL parameters
or None if missing.
"""
has_date = all([
self.kwargs.get('year') is not None,
self.kwargs.get('month') is not None,
self.kwargs.get('day') is not None,
])
if not has_date:
return None
return datetime.date(
self.kwargs['year'],
self.kwargs['month'],
self.kwargs['day']
)
context['source_channel'] = Source.objects.get(
slug=self.kwargs['source']
def get_dates(self, source):
"""Return a list if available dates for the current source."""
_, start_date, end_date = _get_dates()
if source is None:
return []
lst = Entry.objects.filter(
hidden=False,
source=source,
created__range=(start_date, end_date)
).annotate(
date=TruncDate('created')
).values('date').annotate(
nb=Count('date')
).order_by('-date')
return [o['date'] for o in lst]
def get_source(self):
"""Return the current source."""
if self.kwargs.get('source') is None:
return None
return Source.objects.get(slug=self.kwargs['source'])
def get_sources(self):
"""Return available sources."""
now, start_date, end_date = _get_dates()
qs = Entry.objects.all()
if not self.request.user.is_staff:
qs = qs.filter(
hidden=False,
source__hidden=False,
created__range=(start_date, end_date)
)
qs = qs.values(
'source', 'source__name', 'source__slug',
'source__id', 'source__hidden'
).annotate(
nb=Count('id')
)
if isinstance(context['source_channel'], tuple):
context['source_channel'] = context['source_channel'][0]
out = []
for src in qs:
dts = self.get_dates(src['source'])
src['first_date'] = dts[0] if dts else None
out.append(src)
return out
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['form'] = SearchForm()
context['sources'] = self.get_sources()
context['current_source'] = self.get_source()
context['dates'] = self.get_dates(context['current_source'])
context['current_date'] = self.get_date()
return context
def get_queryset(self):
"""Return the entries to be displayed."""
dt = self.get_date()
if dt is None:
return []
is_staff = self.request.user.is_staff
dt = datetime.date(
self.kwargs['year'],
self.kwargs['month'],
self.kwargs['day']
)
now = datetime.date.today()
out_of_bounds = any((
(now - dt).days > settings.KHAGANAT_LOGS_MAX_DAYS,
@ -146,7 +171,7 @@ class EntriesView(generic.ListView):
))
if out_of_bounds and not is_staff:
return Entry.objects.none()
src = Source.objects.get(slug=self.kwargs['source'])
src = self.get_source()
if src is None:
return Entry.objects.none()
if src.hidden and not is_staff:

View file

@ -1,71 +1,64 @@
{% load static %}
{% load i18n %}
<nav class="navbar navbar-expand-lg navbar-light" id="mainNav">
<a class="navbar-brand" href="{% url 'index' %}">
<img src="{% static "khaganat/images/icon_khaganat.png" %}" alt="Khaganat">
</a>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav">
{% for e in elems %}
{% if e.localized_link %}
<li class="nav-item">
<a class="nav-link" href="{{ e.localized_link }}"{% if e.new_window %} target="_blank"{% endif %}>{{ e }}</a>
</li>
{% elif e.children %}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{{ e.description.short_name }}
</a>
<div class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink">
{% for c in e.children %}
{% if c.is_separator %}
<hr />
{% else %}
<a class="dropdown-item" href="{% if c.add_locale %}/{{ current_lang_code }}{% endif %}{{ c.localized_link }}"{% if c.new_window %} target="_blank"{% endif %}{% if c.description.full_name or c.description.description %} data-toggle="tooltip" data-placement="right" data-html="true" title="{% if c.description.full_name %}<b>{{ c.description.full_name }}</b>{% endif %}{% if c.description.full_name and c.description.description %}<hr />{% endif %}{{ c.description.description }}"{% endif %}>
<img src="{% static c.icon_path %}" alt="" /> {{ c }}
</a>
{% endif %}
{% endfor %}
</div>
</li>
{% else %}
<li class="nav-item">
<a class="nav-link disabled" href="#">{{ e }}</a>
</li>
{% endif %}
{% endfor %}
</ul>
</div>
<ul class="navbar-nav flex-row ml-md-auto d-none d-md-flex">
{% if user.is_authenticated %}
<li class="nav-item dropdown">
<a class="nav-item nav-link dropdown-toggle mr-md-2" href="#" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">{% trans "my_account"|capfirst %}</a>
<div class="dropdown-menu dropdown-menu-right">
<!--
<a class="dropdown-item" href="{% url "settings" %}">{% trans "settings"|capfirst %}</a>
-->
{% if user.is_superuser %}
<a class="dropdown-item" href="{% url "admin:index" %}">{% trans "administration"|capfirst %}</a>
{% endif %}
<a class="dropdown-item" href="{% url "logout" %}">{% trans "logout"|capfirst %}</a>
</div>
</li>
{% else %}
<li>
<a class="btn btn-primary" href="{% url "login" %}?next={{ current_url }}" role="button">{% trans "login"|capfirst %}</a>
</li>
<li>
<a class="btn btn-secondary" href="{% url "register" %}" role="button">{% trans "register"|capfirst %}</a>
</li>
{% endif %}
<li class="nav-item dropdown">
<a class="nav-item nav-link dropdown-toggle mr-md-2" href="#" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">{{ current_lang_name|capfirst }}</a>
<div class="dropdown-menu dropdown-menu-right">
{% for lang_code, lang_name, lang_url in all_langs %}
<a class="dropdown-item{% if lang_code == current_lang_code %} active{% endif %}" href="{{ lang_url }}">{{ lang_name|capfirst }}</a>
{% endfor %}
</div>
</li>
</ul>
<nav class="navbar is-light">
<div class="navbar-brand">
<a class="navbar-item" href="{% url 'index' %}"><img src="{% static "khaganat/images/icon_khaganat.png" %}" alt="Khaganat"></a>
</div>
<div class="navbar-menu">
<div class="navbar-start">
{% for e in elems %}
{% if e.localized_link %}
<a class="navbar-item" href="{{ e.localized_link }}"{% if e.new_window %} target="_blank"{% endif %}>{{ e }}</a>
{% elif e.children %}
<div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link">{{ e.description.short_name }}</a>
<div class="navbar-dropdown">
{% for c in e.children %}
{% if c.is_separator %}<hr class="navbar-divider">{% else %}
<a class="navbar-item" href="{% if c.add_locale %}/{{ current_lang_code }}{% endif %}{{ c.localized_link }}"{% if c.new_window %} target="_blank"{% endif %}{% if c.description.description %} title="{{ c.description.description }}"{% endif %}>
<span>
<span><img src="{% static c.icon_path %}" alt="" /></span>
<span>{{ c }}</span>
</span>
</a>
{% endif %}
{% endfor %}
</div>
</div>
{% else %}
<span class="navbar-item">{{ e }}</span>
{% endif %}
{% endfor %}
</div>
<div class="navbar-end">
{% if user.is_authenticated %}
<div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link">{% trans "my_account"|capfirst %}</a>
<div class="navbar-dropdown">
{% if user.is_superuser %}
<a class="navbar-item" href="{% url "admin:index" %}">{% trans "administration"|capfirst %}</a>
{% endif %}
<a class="navbar-item" href="{% url "logout" %}">{% trans "logout"|capfirst %}</a>
</div>
</div>
{% else %}
<div class="navbar-item">
<div>
<a class="button is-link" href="{% url "login" %}?next={{ current_url }}">{% trans "login"|capfirst %}</a>
<a class="button is-white" href="{% url "register" %}">{% trans "register"|capfirst %}</a>
</div>
</div>
{% endif %}
<div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link">{{ current_lang_name|capfirst }}</a>
<div class="navbar-dropdown">
{% for lang_code, lang_name, lang_url in all_langs %}
<a class="navbar-item{% if lang_code == current_lang_code %} is-active{% endif %}" href="{{ lang_url }}">{{ lang_name|capfirst }}</a>
{% endfor %}
</div>
</div>
</div>
</div>
</nav>

View file

@ -1,20 +1,12 @@
from django.utils.translation import ugettext_lazy as _
from django.http import HttpResponseRedirect, QueryDict
from django.template.loader import render_to_string
from django.urls.exceptions import Resolver404
from django.shortcuts import render
from django.contrib import messages
from neluser.models import NelUser
from django.urls import reverse, resolve
from django.conf import settings
def is_link_legit(url):
try:
resolve(url)
except Resolver404:
return False
return True
from django.urls import reverse
from utils import is_link_legit
def is_nsfw_allowed(request):

View file

@ -1,13 +1,10 @@
{% extends "khaganat/base.html" %}
{% extends "khaganat/centered_dialog.html" %}
{% load i18n %}
{% block title %}{% trans "register" %}{% endblock %}
{% block title %}{% trans "register"|capfirst %}{% endblock %}
{% block dialog_class %}is-success{% endblock %}
{% block content %}
<div class="content-bloc">
<div class="alert alert-success" role="alert">
<p>{% trans 'account_activated' %}</p>
<a class="btn btn-secondary" href="{% url "login" %}" role="button">{% trans "login"|capfirst %}</a>
</div>
</div>
{% block dialog %}
<p>{% trans 'account_activated'|capfirst %}</p>
<a class="button is-success" href="{% url "login" %}" role="button">{% trans "login"|capfirst %}</a>
{% endblock %}

View file

@ -1,17 +1,17 @@
{% extends "khaganat/base.html" %}
{% extends "khaganat/centered_dialog.html" %}
{% load bulma_tags %}
{% load i18n %}
{% block title %}{% trans "login" %}{% endblock %}
{% block title %}{% trans "login"|capfirst %}{% endblock %}
{% block dialog_class %}is-link{% endblock %}
{% block content %}
<div class="content-bloc">
<form method="post" action="{% url 'login' %}">
{% csrf_token %}
{{ form.as_p }}
<input type="hidden" name="next" value="{{ request.GET.next }}" />
<button type="submit" class="btn btn-success">{% trans "login"|capfirst %}</button>
<a class="btn btn-primary" href="{% url "register" %}" role="button">{% trans "register"|capfirst %}</a>
<a class="btn btn-secondary" href="{% url "password_reset" %}" role="button">{% trans "forgotten_password"|capfirst %}</a>
</form>
</div>
{% block dialog %}
<form method="post" action="{% url 'login' %}">
{% csrf_token %}
{{ form|bulma }}
<input type="hidden" name="next" value="{{ request.GET.next }}" />
<button type="submit" class="button is-link">{% trans "login"|capfirst %}</button>
<a class="button is-light" href="{% url "password_reset" %}" role="button">{% trans "forgotten_password"|capfirst %}</a>
<a class="button is-light" href="{% url "register" %}" role="button">{% trans "register"|capfirst %}</a>
</form>
{% endblock %}

View file

@ -4,17 +4,24 @@
{% block title %}{% trans "NSFW content" %}{% endblock %}
{% block content %}
<div class="content-bloc">
<h2>{% trans "NSFW content" %}</h2>
<p>{% trans "The content you were about to see is flagged as sensitive and therefore cannot be seen while the safe mode is activated." %}</p>
<div>
<a class="btn btn-primary" href="{% url "index" %}" role="button">{% trans "Go back home" %}</a>
<a class="btn btn-danger" href="{% url "enable_nsfw" "0" %}?next={{ next_url }}" role="button">{% trans "Permanently disable safe mode" %}</a>
<br>
{% trans "Or disable safe mode for:" %}
<a class="badge badge-warning" href="{% url "enable_nsfw" "300" %}?next={{ next_url }}">{% trans "5 minutes" %}</a>
<a class="badge badge-warning" href="{% url "enable_nsfw" "3600" %}?next={{ next_url }}">{% trans "1 hour" %}</a>
<a class="badge badge-warning" href="{% url "enable_nsfw" "86400" %}?next={{ next_url }}">{% trans "1 day" %}</a>
<article class="message is-warning">
<div class="message-header">
<p>{% trans "NSFW content" %}</p>
</div>
</div>
<div class="message-body">
<p>{% trans "The content you were about to see is flagged as sensitive and therefore cannot be seen while the safe mode is activated." %}</p>
<div>
<p>
<a class="button is-link" href="{% url "index" %}" role="button">{% trans "Go back home" %}</a>
<a class="button is-danger" href="{% url "enable_nsfw" "0" %}?next={{ next_url }}" role="button">{% trans "Permanently disable safe mode" %}</a>
</p>
<p>
{% trans "Or disable safe mode for:" %}<br>
<a class="button is-warning" href="{% url "enable_nsfw" "300" %}?next={{ next_url }}">{% trans "5 minutes" %}</a>
<a class="button is-warning" href="{% url "enable_nsfw" "3600" %}?next={{ next_url }}">{% trans "1 hour" %}</a>
<a class="button is-warning" href="{% url "enable_nsfw" "86400" %}?next={{ next_url }}">{% trans "1 day" %}</a>
</p>
</div>
</div>
</article>
{% endblock %}

View file

@ -1,14 +1,14 @@
{% extends "khaganat/base.html" %}
{% extends "khaganat/centered_dialog.html" %}
{% load bulma_tags %}
{% load i18n %}
{% block title %}{% trans "password_reset" %}{% endblock %}
{% block title %}{% trans "password_reset"|capfirst %}{% endblock %}
{% block dialog_class %}is-link{% endblock %}
{% block content %}
<div class="content-bloc">
<form method="post" action="{% url 'password_reset' %}">
{% csrf_token %}
{{ form.as_p }}
<button type="submit" class="btn btn-success">{% trans "reset_my_password" %}</button>
</form>
</div>
{% block dialog %}
<form method="post" action="{% url 'password_reset' %}">
{% csrf_token %}
{{ form|bulma }}
<button type="submit" class="button is-link">{% trans "reset_my_password"|capfirst %}</button>
</form>
{% endblock %}

View file

@ -1,19 +1,18 @@
{% extends "khaganat/base.html" %}
{% extends "khaganat/centered_dialog.html" %}
{% load bulma_tags %}
{% load i18n %}
{% block title %}{% trans "password_reset" %}{% endblock %}
{% block title %}{% if validlink %}{% trans "set_new_password"|capfirst %}{% else %}{% trans "password_reset"|capfirst %}{% endif %}{% endblock %}
{% block dialog_class %}{% if validlink %}is-link{% else %}is-danger{% endif %}{% endblock %}
{% block content %}
<div class="content-bloc">
{% if validlink %}
<h2>{% trans "set_new_password" %}</h2>
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit" class="btn btn-success">{% trans "change_my_password"|capfirst %}</button>
</form>
{% else %}
<div class="alert alert-danger" role="alert">{% trans "reset_password_invalid_link" %}</div>
{% endif %}
</div>
{% block dialog %}
{% if validlink %}
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit" class="button is-link">{% trans "change_my_password"|capfirst %}</button>
</form>
{% else %}
{% trans "reset_password_invalid_link" %}
{% endif %}
{% endblock %}

View file

@ -1,13 +1,10 @@
{% extends "khaganat/base.html" %}
{% extends "khaganat/centered_dialog.html" %}
{% load i18n %}
{% block title %}{% trans "password_reset" %}{% endblock %}
{% block title %}{% trans "password_reset"|capfirst %}{% endblock %}
{% block dialog_class %}is-success{% endblock %}
{% block content %}
<div class="content-bloc">
<div class="alert alert-success" role="alert">
<p>{% trans "password_reset_success" %}</p>
<a class="btn btn-secondary" href="{% url "login" %}" role="button">{% trans "login"|capfirst %}</a>
</div>
</div>
{% block dialog %}
<p>{% trans "password_reset_success" %}</p>
<a class="button is-success" href="{% url "login" %}" role="button">{% trans "login"|capfirst %}</a>
{% endblock %}

View file

@ -1,10 +1,10 @@
{% extends "khaganat/base.html" %}
{% extends "khaganat/centered_dialog.html" %}
{% load i18n %}
{% block title %}{% trans "password_reset" %}{% endblock %}
{% block title %}{% trans "password_reset"|capfirst %}{% endblock %}
{% block dialog_class %}is-link{% endblock %}
{% block content %}
<div class="content-bloc">
<div class="alert alert-success" role="alert">{% trans "password_reset_success_email" %}</div>
</div>
{% block dialog %}
{% trans "password_reset_success_email" %}
<a class="button is-link" href="{% url "index" %}" role="button">{% trans "Go back home" %}</a>
{% endblock %}

View file

@ -1,14 +1,15 @@
{% extends "khaganat/base.html" %}
{% extends "khaganat/centered_dialog.html" %}
{% load bulma_tags %}
{% load i18n %}
{% block title %}{% trans "register" %}{% endblock %}
{% block title %}{% trans "register"|capfirst %}{% endblock %}
{% block dialog_class %}is-link{% endblock %}
{% block dialog_size %}is-two-thirds{% endblock %}
{% block content %}
<div class="content-bloc">
<form method="post" action="{% url 'register' %}">
{% csrf_token %}
{{ form.as_p }}
<button type="submit" class="btn btn-success">{% trans "register"|capfirst %}</button>
</form>
</div>
{% block dialog %}
<form method="post" action="{% url 'register' %}">
{% csrf_token %}
{{ form|bulma }}
<button type="submit" class="button is-link">{% trans "register"|capfirst %}</button>
</form>
{% endblock %}

View file

@ -1,13 +1,10 @@
{% extends "khaganat/base.html" %}
{% extends "khaganat/centered_dialog.html" %}
{% load i18n %}
{% block title %}{% trans "register" %}{% endblock %}
{% block title %}{% trans "almost_there"|capfirst %}{% endblock %}
{% block dialog_class %}is-success{% endblock %}
{% block content %}
<div class="alert alert-success" role="alert">
<h4 class="alert-heading">{% trans 'almost_there' %}</h4>
<p>{% trans 'activate_your_account' %}</p>
<a class="btn btn-secondary" href="{% url "index" %}" role="button">{% trans "take_me_home"|capfirst %}</a>
</div>
{% block dialog %}
<p>{% trans 'activate_your_account' %}</p>
<a class="button is-success" href="{% url "index" %}" role="button">{% trans "take_me_home"|capfirst %}</a>
{% endblock %}

View file

@ -3,5 +3,7 @@
{% block title %}{{ page.title }}{% endblock %}
{% block content %}
{{ page.formated_content|safe }}
<div class="content">
{{ page.formated_content|safe }}
</div>
{% endblock %}

10
utils.py Normal file
View file

@ -0,0 +1,10 @@
from django.urls.exceptions import Resolver404
from django.urls import resolve
def is_link_legit(url):
try:
resolve(url)
except Resolver404:
return False
return True