corrige conflitos
This commit is contained in:
5
diários_oficiais_alems/__init__.py
Normal file
5
diários_oficiais_alems/__init__.py
Normal file
@ -0,0 +1,5 @@
|
||||
__version__ = "0.1.0"
|
||||
__version_info__ = tuple(
|
||||
int(num) if num.isdigit() else num
|
||||
for num in __version__.replace("-", ".", 1).split(".")
|
||||
)
|
||||
14
diários_oficiais_alems/conftest.py
Normal file
14
diários_oficiais_alems/conftest.py
Normal file
@ -0,0 +1,14 @@
|
||||
import pytest
|
||||
|
||||
from diários_oficiais_alems.users.models import User
|
||||
from diários_oficiais_alems.users.tests.factories import UserFactory
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _media_storage(settings, tmpdir) -> None:
|
||||
settings.MEDIA_ROOT = tmpdir.strpath
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def user(db) -> User:
|
||||
return UserFactory()
|
||||
5
diários_oficiais_alems/contrib/__init__.py
Normal file
5
diários_oficiais_alems/contrib/__init__.py
Normal file
@ -0,0 +1,5 @@
|
||||
"""
|
||||
To understand why this file is here, please read:
|
||||
|
||||
https://cookiecutter-django.readthedocs.io/en/latest/5-help/faq.html#why-is-there-a-django-contrib-sites-directory-in-cookiecutter-django
|
||||
"""
|
||||
5
diários_oficiais_alems/contrib/sites/__init__.py
Normal file
5
diários_oficiais_alems/contrib/sites/__init__.py
Normal file
@ -0,0 +1,5 @@
|
||||
"""
|
||||
To understand why this file is here, please read:
|
||||
|
||||
https://cookiecutter-django.readthedocs.io/en/latest/5-help/faq.html#why-is-there-a-django-contrib-sites-directory-in-cookiecutter-django
|
||||
"""
|
||||
@ -0,0 +1,43 @@
|
||||
import django.contrib.sites.models
|
||||
from django.contrib.sites.models import _simple_domain_name_validator
|
||||
from django.db import migrations
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = []
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="Site",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
verbose_name="ID",
|
||||
serialize=False,
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"domain",
|
||||
models.CharField(
|
||||
max_length=100,
|
||||
verbose_name="domain name",
|
||||
validators=[_simple_domain_name_validator],
|
||||
),
|
||||
),
|
||||
("name", models.CharField(max_length=50, verbose_name="display name")),
|
||||
],
|
||||
options={
|
||||
"ordering": ("domain",),
|
||||
"db_table": "django_site",
|
||||
"verbose_name": "site",
|
||||
"verbose_name_plural": "sites",
|
||||
},
|
||||
bases=(models.Model,),
|
||||
managers=[("objects", django.contrib.sites.models.SiteManager())],
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,21 @@
|
||||
import django.contrib.sites.models
|
||||
from django.db import migrations
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [("sites", "0001_initial")]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="site",
|
||||
name="domain",
|
||||
field=models.CharField(
|
||||
max_length=100,
|
||||
unique=True,
|
||||
validators=[django.contrib.sites.models._simple_domain_name_validator],
|
||||
verbose_name="domain name",
|
||||
),
|
||||
)
|
||||
]
|
||||
@ -0,0 +1,63 @@
|
||||
"""
|
||||
To understand why this file is here, please read:
|
||||
|
||||
https://cookiecutter-django.readthedocs.io/en/latest/5-help/faq.html#why-is-there-a-django-contrib-sites-directory-in-cookiecutter-django
|
||||
"""
|
||||
from django.conf import settings
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def _update_or_create_site_with_sequence(site_model, connection, domain, name):
|
||||
"""Update or create the site with default ID and keep the DB sequence in sync."""
|
||||
site, created = site_model.objects.update_or_create(
|
||||
id=settings.SITE_ID,
|
||||
defaults={
|
||||
"domain": domain,
|
||||
"name": name,
|
||||
},
|
||||
)
|
||||
if created:
|
||||
# We provided the ID explicitly when creating the Site entry, therefore the DB
|
||||
# sequence to auto-generate them wasn't used and is now out of sync. If we
|
||||
# don't do anything, we'll get a unique constraint violation the next time a
|
||||
# site is created.
|
||||
# To avoid this, we need to manually update DB sequence and make sure it's
|
||||
# greater than the maximum value.
|
||||
max_id = site_model.objects.order_by("-id").first().id
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute("SELECT last_value from django_site_id_seq")
|
||||
(current_id,) = cursor.fetchone()
|
||||
if current_id <= max_id:
|
||||
cursor.execute(
|
||||
"alter sequence django_site_id_seq restart with %s",
|
||||
[max_id + 1],
|
||||
)
|
||||
|
||||
|
||||
def update_site_forward(apps, schema_editor):
|
||||
"""Set site domain and name."""
|
||||
Site = apps.get_model("sites", "Site")
|
||||
_update_or_create_site_with_sequence(
|
||||
Site,
|
||||
schema_editor.connection,
|
||||
"example.com",
|
||||
"Diários Oficiais ALEMS",
|
||||
)
|
||||
|
||||
|
||||
def update_site_backward(apps, schema_editor):
|
||||
"""Revert site domain and name to default."""
|
||||
Site = apps.get_model("sites", "Site")
|
||||
_update_or_create_site_with_sequence(
|
||||
Site,
|
||||
schema_editor.connection,
|
||||
"example.com",
|
||||
"example.com",
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [("sites", "0002_alter_domain_unique")]
|
||||
|
||||
operations = [migrations.RunPython(update_site_forward, update_site_backward)]
|
||||
@ -0,0 +1,21 @@
|
||||
# Generated by Django 3.1.7 on 2021-02-04 14:49
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("sites", "0003_set_site_domain_and_name"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name="site",
|
||||
options={
|
||||
"ordering": ["domain"],
|
||||
"verbose_name": "site",
|
||||
"verbose_name_plural": "sites",
|
||||
},
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,5 @@
|
||||
"""
|
||||
To understand why this file is here, please read:
|
||||
|
||||
https://cookiecutter-django.readthedocs.io/en/latest/5-help/faq.html#why-is-there-a-django-contrib-sites-directory-in-cookiecutter-django
|
||||
"""
|
||||
13
diários_oficiais_alems/static/css/project.css
Normal file
13
diários_oficiais_alems/static/css/project.css
Normal file
@ -0,0 +1,13 @@
|
||||
/* These styles are generated from project.scss. */
|
||||
|
||||
.alert-debug {
|
||||
color: black;
|
||||
background-color: white;
|
||||
border-color: #d6e9c6;
|
||||
}
|
||||
|
||||
.alert-error {
|
||||
color: #b94a48;
|
||||
background-color: #f2dede;
|
||||
border-color: #eed3d7;
|
||||
}
|
||||
0
diários_oficiais_alems/static/fonts/.gitkeep
Normal file
0
diários_oficiais_alems/static/fonts/.gitkeep
Normal file
BIN
diários_oficiais_alems/static/images/favicons/favicon.ico
Normal file
BIN
diários_oficiais_alems/static/images/favicons/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.2 KiB |
1
diários_oficiais_alems/static/js/project.js
Normal file
1
diários_oficiais_alems/static/js/project.js
Normal file
@ -0,0 +1 @@
|
||||
/* Project specific Javascript goes here. */
|
||||
13
diários_oficiais_alems/templates/403.html
Normal file
13
diários_oficiais_alems/templates/403.html
Normal file
@ -0,0 +1,13 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Forbidden (403){% endblock title %}
|
||||
{% block content %}
|
||||
<h1>Forbidden (403)</h1>
|
||||
<p>
|
||||
{% if exception %}
|
||||
{{ exception }}
|
||||
{% else %}
|
||||
You're not allowed to access this page.
|
||||
{% endif %}
|
||||
</p>
|
||||
{% endblock content %}
|
||||
13
diários_oficiais_alems/templates/403_csrf.html
Normal file
13
diários_oficiais_alems/templates/403_csrf.html
Normal file
@ -0,0 +1,13 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Forbidden (403){% endblock title %}
|
||||
{% block content %}
|
||||
<h1>Forbidden (403)</h1>
|
||||
<p>
|
||||
{% if exception %}
|
||||
{{ exception }}
|
||||
{% else %}
|
||||
You're not allowed to access this page.
|
||||
{% endif %}
|
||||
</p>
|
||||
{% endblock content %}
|
||||
13
diários_oficiais_alems/templates/404.html
Normal file
13
diários_oficiais_alems/templates/404.html
Normal file
@ -0,0 +1,13 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Page not found{% endblock title %}
|
||||
{% block content %}
|
||||
<h1>Page not found</h1>
|
||||
<p>
|
||||
{% if exception %}
|
||||
{{ exception }}
|
||||
{% else %}
|
||||
This is not the page you were looking for.
|
||||
{% endif %}
|
||||
</p>
|
||||
{% endblock content %}
|
||||
10
diários_oficiais_alems/templates/500.html
Normal file
10
diários_oficiais_alems/templates/500.html
Normal file
@ -0,0 +1,10 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Server Error{% endblock title %}
|
||||
{% block content %}
|
||||
<h1>Ooops!!! 500</h1>
|
||||
<h3>Looks like something went wrong!</h3>
|
||||
<p>
|
||||
We track these errors automatically, but if the problem persists feel free to contact us. In the meantime, try refreshing.
|
||||
</p>
|
||||
{% endblock content %}
|
||||
@ -0,0 +1,11 @@
|
||||
{% extends "account/base_manage.html" %}
|
||||
|
||||
{% block main %}
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
{% block content %}
|
||||
{% endblock content %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock main %}
|
||||
|
||||
@ -0,0 +1,7 @@
|
||||
{% load i18n %}
|
||||
{% load allauth %}
|
||||
|
||||
<div class="alert alert-error">
|
||||
{% slot message %}
|
||||
{% endslot %}
|
||||
</div>
|
||||
@ -0,0 +1,6 @@
|
||||
{% load allauth %}
|
||||
|
||||
<span class="badge {% if 'success' in attrs.tags %}bg-success{% endif %} {% if 'warning' in attrs.tags %}bg-warning{% endif %} {% if 'secondary' in attrs.tags %}bg-secondary{% endif %} {% if 'danger' in attrs.tags %}bg-danger{% endif %} {% if 'primary' in attrs.tags %}bg-primary{% endif %}">
|
||||
{% slot %}
|
||||
{% endslot %}
|
||||
</span>
|
||||
@ -0,0 +1,20 @@
|
||||
{% load allauth %}
|
||||
|
||||
{% comment %} djlint:off {% endcomment %}
|
||||
<{% if attrs.href %}a href="{{ attrs.href }}"{% else %}button{% endif %}
|
||||
{% if attrs.form %}form="{{ attrs.form }}"{% endif %}
|
||||
{% if attrs.id %}id="{{ attrs.id }}"{% endif %}
|
||||
{% if attrs.name %}name="{{ attrs.name }}"{% endif %}
|
||||
{% if attrs.type %}type="{{ attrs.type }}"{% endif %}
|
||||
class="btn
|
||||
{% if 'success' in attrs.tags %}btn-success
|
||||
{% elif 'warning' in attrs.tags %}btn-warning
|
||||
{% elif 'secondary' in attrs.tags %}btn-secondary
|
||||
{% elif 'danger' in attrs.tags %}btn-danger
|
||||
{% elif 'primary' in attrs.tags %}btn-primary
|
||||
{% else %}btn-primary
|
||||
{% endif %}"
|
||||
>
|
||||
{% slot %}
|
||||
{% endslot %}
|
||||
</{% if attrs.href %}a{% else %}button{% endif %}>
|
||||
66
diários_oficiais_alems/templates/allauth/elements/field.html
Normal file
66
diários_oficiais_alems/templates/allauth/elements/field.html
Normal file
@ -0,0 +1,66 @@
|
||||
{% load allauth %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
{% if attrs.type == "textarea" %}
|
||||
<div class="row mb-3">
|
||||
<div class="col-sm-10">
|
||||
<label for="{{ attrs.id }}">
|
||||
{% slot label %}
|
||||
{% endslot %}
|
||||
</label>
|
||||
</div>
|
||||
<textarea {% if attrs.required %}required{% endif %}
|
||||
{% if attrs.rows %}rows="{{ attrs.rows }}"{% endif %}
|
||||
{% if attrs.disabled %}disabled{% endif %}
|
||||
{% if attrs.readonly %}readonly{% endif %}
|
||||
{% if attrs.checked %}checked{% endif %}
|
||||
{% if attrs.name %}name="{{ attrs.name }}"{% endif %}
|
||||
{% if attrs.id %}id="{{ attrs.id }}"{% endif %}
|
||||
{% if attrs.placeholder %}placeholder="{{ attrs.placeholder }}"{% endif %}
|
||||
class="form-control">{% slot value %}{% endslot %}</textarea>
|
||||
</div>
|
||||
{% elif attrs.type == "radio" %}
|
||||
<div class="row mb-3">
|
||||
<div class="col-sm-10">
|
||||
<div class="form-check">
|
||||
<input {% if attrs.required %}required{% endif %}
|
||||
{% if attrs.disabled %}disabled{% endif %}
|
||||
{% if attrs.readonly %}readonly{% endif %}
|
||||
{% if attrs.checked %}checked{% endif %}
|
||||
{% if attrs.name %}name="{{ attrs.name }}"{% endif %}
|
||||
{% if attrs.id %}id="{{ attrs.id }}"{% endif %}
|
||||
{% if attrs.placeholder %}placeholder="{{ attrs.placeholder }}"{% endif %}
|
||||
{% if attrs.autocomplete %}autocomplete="{{ attrs.autocomplete }}"{% endif %}
|
||||
value="{{ attrs.value|default_if_none:"" }}"
|
||||
type="{{ attrs.type }}" />
|
||||
<label class="form-check-label" for="{{ attrs.id }}">
|
||||
{% slot label %}
|
||||
{% endslot %}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="col-sm-10">
|
||||
<label for="{{ attrs.id }}">
|
||||
{% slot label %}
|
||||
{% endslot %}
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-sm-10">
|
||||
<input {% if attrs.required %}required{% endif %}
|
||||
{% if attrs.disabled %}disabled{% endif %}
|
||||
{% if attrs.readonly %}readonly{% endif %}
|
||||
{% if attrs.checked %}checked{% endif %}
|
||||
{% if attrs.name %}name="{{ attrs.name }}"{% endif %}
|
||||
{% if attrs.id %}id="{{ attrs.id }}"{% endif %}
|
||||
{% if attrs.placeholder %}placeholder="{{ attrs.placeholder }}"{% endif %}
|
||||
{% if attrs.autocomplete %}autocomplete="{{ attrs.autocomplete }}"{% endif %}
|
||||
value="{{ attrs.value|default_if_none:"" }}"
|
||||
type="{{ attrs.type }}"
|
||||
class="form-control" />
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if slots.help_text %}
|
||||
<div class="form-text">{% slot help_text %}{% endslot %}</div>
|
||||
{% endif %}
|
||||
@ -0,0 +1,3 @@
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
{{ attrs.form|crispy }}
|
||||
19
diários_oficiais_alems/templates/allauth/elements/panel.html
Normal file
19
diários_oficiais_alems/templates/allauth/elements/panel.html
Normal file
@ -0,0 +1,19 @@
|
||||
{% load allauth %}
|
||||
|
||||
<section>
|
||||
<div class="card mb-4">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">
|
||||
{% slot title %}
|
||||
{% endslot %}
|
||||
</h2>
|
||||
{% slot body %}
|
||||
{% endslot %}
|
||||
{% if slots.actions %}
|
||||
<ul>
|
||||
{% for action in slots.actions %}<li>{{ action }}</li>{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@ -0,0 +1,6 @@
|
||||
{% load allauth %}
|
||||
|
||||
<table class="table">
|
||||
{% slot %}
|
||||
{% endslot %}
|
||||
</table>
|
||||
@ -0,0 +1,31 @@
|
||||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
{% block bodyclass %}bg-light{% endblock bodyclass %}
|
||||
|
||||
{% block css %}{{ block.super }}{% endblock css %}
|
||||
{% block title %}
|
||||
{% block head_title %}
|
||||
{% trans "Sign In" %}
|
||||
{% endblock head_title %}
|
||||
{% endblock title %}
|
||||
{% block body %}
|
||||
<div class="d-flex justify-content-center h-100 py-4">
|
||||
<div class="col-md-4 py-4 my-4 px-4">
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-dismissible {% if message.tags %}alert-{{ message.tags }}{% endif %}">
|
||||
{{ message }}
|
||||
<button type="button"
|
||||
class="btn-close"
|
||||
data-bs-dismiss="alert"
|
||||
aria-label="Close"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% block content %}
|
||||
{% endblock content %}
|
||||
{% block extra_body %}
|
||||
{% endblock extra_body %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock body %}
|
||||
@ -0,0 +1,6 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block main %}
|
||||
{% block content %}
|
||||
{% endblock content %}
|
||||
{% endblock main %}
|
||||
145
diários_oficiais_alems/templates/base.html
Normal file
145
diários_oficiais_alems/templates/base.html
Normal file
@ -0,0 +1,145 @@
|
||||
|
||||
{% load static i18n compress%}<!DOCTYPE html>
|
||||
{% get_current_language as LANGUAGE_CODE %}
|
||||
<html lang="{{ LANGUAGE_CODE }}">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
||||
<title>
|
||||
{% block title %}
|
||||
Diários Oficiais ALEMS
|
||||
{% endblock title %}
|
||||
</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="description"
|
||||
content="Indexação dos Diários Oficiais da ALEMS" />
|
||||
<meta name="author"
|
||||
content="Antonio Roberto" />
|
||||
<link rel="icon" href="{% static 'images/favicons/favicon.ico' %}" />
|
||||
{% block css %}
|
||||
<!-- Latest compiled and minified Bootstrap CSS -->
|
||||
<link rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.2.3/css/bootstrap.min.css"
|
||||
integrity="sha512-SbiR/eusphKoMVVXysTKG/7VseWii+Y3FdHrt0EpKgpToZeemhqHeZeLWLhJutz/2ut2Vw1uQEj2MbRF+TVBUA=="
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer" />
|
||||
<!-- Your stuff: Third-party CSS libraries go here -->
|
||||
<!-- This file stores project-specific CSS -->
|
||||
|
||||
|
||||
{% compress css %}
|
||||
<link href="{% static 'css/project.css' %}" rel="stylesheet" />
|
||||
{% endcompress %}
|
||||
|
||||
|
||||
{% endblock css %}
|
||||
<!-- Le javascript
|
||||
================================================== -->
|
||||
{# Placed at the top of the document so pages load faster with defer #}
|
||||
{% block javascript %}
|
||||
|
||||
|
||||
<!-- Bootstrap JS -->
|
||||
<script defer
|
||||
src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.2.3/js/bootstrap.min.js"
|
||||
integrity="sha512-1/RvZTcCDEUjY/CypiMz+iqqtaoQfAITmNSJY17Myp4Ms5mdxPS5UV7iOfdZoxcGhzFbOm6sntTKJppjvuhg4g=="
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"></script>
|
||||
<!-- Your stuff: Third-party javascript libraries go here -->
|
||||
|
||||
|
||||
<!-- place project specific Javascript in this file -->
|
||||
|
||||
|
||||
{% compress js %}
|
||||
<script defer src="{% static 'js/project.js' %}"></script>
|
||||
{% endcompress %}
|
||||
|
||||
|
||||
{% endblock javascript %}
|
||||
</head>
|
||||
<body class="{% block bodyclass %}{% endblock bodyclass %}">
|
||||
{% block body %}
|
||||
<div class="mb-1">
|
||||
<nav class="navbar navbar-expand-md navbar-light bg-light">
|
||||
<div class="container-fluid">
|
||||
<button class="navbar-toggler navbar-toggler-right"
|
||||
type="button"
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target="#navbarSupportedContent"
|
||||
aria-controls="navbarSupportedContent"
|
||||
aria-expanded="false"
|
||||
aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<a class="navbar-brand" href="{% url 'home' %}">Diários Oficiais ALEMS</a>
|
||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
<ul class="navbar-nav mr-auto">
|
||||
<li class="nav-item active">
|
||||
<a class="nav-link" href="{% url 'home' %}">Home <span class="visually-hidden">(current)</span></a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'about' %}">About</a>
|
||||
</li>
|
||||
{% if request.user.is_authenticated %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link"
|
||||
href="{% url 'users:detail' request.user.username %}">{% translate "My Profile" %}</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
{# URL provided by django-allauth/account/urls.py #}
|
||||
<a class="nav-link" href="{% url 'account_logout' %}">{% translate "Sign Out" %}</a>
|
||||
</li>
|
||||
{% else %}
|
||||
{% if ACCOUNT_ALLOW_REGISTRATION %}
|
||||
<li class="nav-item">
|
||||
{# URL provided by django-allauth/account/urls.py #}
|
||||
<a id="sign-up-link" class="nav-link" href="{% url 'account_signup' %}">{% translate "Sign Up" %}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li class="nav-item">
|
||||
{# URL provided by django-allauth/account/urls.py #}
|
||||
<a id="log-in-link" class="nav-link" href="{% url 'account_login' %}">{% translate "Sign In" %}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="container">
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-dismissible {% if message.tags %}alert-{{ message.tags }}{% endif %}">
|
||||
{{ message }}
|
||||
<button type="button"
|
||||
class="btn-close"
|
||||
data-bs-dismiss="alert"
|
||||
aria-label="Close"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% block main %}
|
||||
{% block content %}
|
||||
<p>Use this document as a way to quick start any new project.</p>
|
||||
{% endblock content %}
|
||||
{% endblock main %}
|
||||
|
||||
</div>
|
||||
{% endblock body %}
|
||||
<!-- /container -->
|
||||
{% block modal %}
|
||||
{% endblock modal %}
|
||||
{% block inline_javascript %}
|
||||
{% comment %}
|
||||
Script tags with only code, no src (defer by default). To run
|
||||
with a "defer" so that you run inline code:
|
||||
<script>
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
/* Run whatever you want */
|
||||
});
|
||||
</script>
|
||||
{% endcomment %}
|
||||
{% endblock inline_javascript %}
|
||||
</body>
|
||||
</html>
|
||||
3
diários_oficiais_alems/templates/pages/about.html
Normal file
3
diários_oficiais_alems/templates/pages/about.html
Normal file
@ -0,0 +1,3 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
|
||||
3
diários_oficiais_alems/templates/pages/home.html
Normal file
3
diários_oficiais_alems/templates/pages/home.html
Normal file
@ -0,0 +1,3 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
|
||||
44
diários_oficiais_alems/templates/users/user_detail.html
Normal file
44
diários_oficiais_alems/templates/users/user_detail.html
Normal file
@ -0,0 +1,44 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% load static %}
|
||||
|
||||
{% block title %}
|
||||
User:
|
||||
|
||||
{{ object.username }}
|
||||
|
||||
|
||||
{% endblock title %}
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<h2>
|
||||
|
||||
|
||||
{{ object.username }}
|
||||
|
||||
</h2>
|
||||
{% if object.name %}
|
||||
<p>{{ object.name }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% if object == request.user %}
|
||||
<!-- Action buttons -->
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<a class="btn btn-primary" href="{% url 'users:update' %}" role="button">My Info</a>
|
||||
<a class="btn btn-primary"
|
||||
href="{% url 'account_email' %}"
|
||||
role="button">E-Mail</a>
|
||||
<a class="btn btn-primary"
|
||||
href="{% url 'mfa_index' %}"
|
||||
role="button">MFA</a>
|
||||
<!-- Your Stuff: Custom user template urls -->
|
||||
</div>
|
||||
</div>
|
||||
<!-- End Action buttons -->
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock content %}
|
||||
31
diários_oficiais_alems/templates/users/user_form.html
Normal file
31
diários_oficiais_alems/templates/users/user_form.html
Normal file
@ -0,0 +1,31 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
{% block title %}
|
||||
|
||||
|
||||
{{ user.username }}
|
||||
|
||||
|
||||
{% endblock title %}
|
||||
{% block content %}
|
||||
<h1>
|
||||
|
||||
|
||||
{{ user.username }}
|
||||
|
||||
|
||||
</h1>
|
||||
<form class="form-horizontal"
|
||||
method="post"
|
||||
action="{% url 'users:update' %}">
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
<div class="control-group">
|
||||
<div class="controls">
|
||||
<button type="submit" class="btn btn-primary">Update</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock content %}
|
||||
0
diários_oficiais_alems/users/__init__.py
Normal file
0
diários_oficiais_alems/users/__init__.py
Normal file
48
diários_oficiais_alems/users/adapters.py
Normal file
48
diários_oficiais_alems/users/adapters.py
Normal file
@ -0,0 +1,48 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import typing
|
||||
|
||||
from allauth.account.adapter import DefaultAccountAdapter
|
||||
from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
|
||||
from django.conf import settings
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from allauth.socialaccount.models import SocialLogin
|
||||
from django.http import HttpRequest
|
||||
|
||||
from diários_oficiais_alems.users.models import User
|
||||
|
||||
|
||||
class AccountAdapter(DefaultAccountAdapter):
|
||||
def is_open_for_signup(self, request: HttpRequest) -> bool:
|
||||
return getattr(settings, "ACCOUNT_ALLOW_REGISTRATION", True)
|
||||
|
||||
|
||||
class SocialAccountAdapter(DefaultSocialAccountAdapter):
|
||||
def is_open_for_signup(
|
||||
self,
|
||||
request: HttpRequest,
|
||||
sociallogin: SocialLogin,
|
||||
) -> bool:
|
||||
return getattr(settings, "ACCOUNT_ALLOW_REGISTRATION", True)
|
||||
|
||||
def populate_user(
|
||||
self,
|
||||
request: HttpRequest,
|
||||
sociallogin: SocialLogin,
|
||||
data: dict[str, typing.Any],
|
||||
) -> User:
|
||||
"""
|
||||
Populates user information from social provider info.
|
||||
|
||||
See: https://docs.allauth.org/en/latest/socialaccount/advanced.html#creating-and-populating-user-instances
|
||||
"""
|
||||
user = super().populate_user(request, sociallogin, data)
|
||||
if not user.name:
|
||||
if name := data.get("name"):
|
||||
user.name = name
|
||||
elif first_name := data.get("first_name"):
|
||||
user.name = first_name
|
||||
if last_name := data.get("last_name"):
|
||||
user.name += f" {last_name}"
|
||||
return user
|
||||
40
diários_oficiais_alems/users/admin.py
Normal file
40
diários_oficiais_alems/users/admin.py
Normal file
@ -0,0 +1,40 @@
|
||||
from allauth.account.decorators import secure_admin_login
|
||||
from django.conf import settings
|
||||
from django.contrib import admin
|
||||
from django.contrib.auth import admin as auth_admin
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from .forms import UserAdminChangeForm
|
||||
from .forms import UserAdminCreationForm
|
||||
from .models import User
|
||||
|
||||
if settings.DJANGO_ADMIN_FORCE_ALLAUTH:
|
||||
# Force the `admin` sign in process to go through the `django-allauth` workflow:
|
||||
# https://docs.allauth.org/en/latest/common/admin.html#admin
|
||||
admin.autodiscover()
|
||||
admin.site.login = secure_admin_login(admin.site.login) # type: ignore[method-assign]
|
||||
|
||||
|
||||
@admin.register(User)
|
||||
class UserAdmin(auth_admin.UserAdmin):
|
||||
form = UserAdminChangeForm
|
||||
add_form = UserAdminCreationForm
|
||||
fieldsets = (
|
||||
(None, {"fields": ("username", "password")}),
|
||||
(_("Personal info"), {"fields": ("name", "email")}),
|
||||
(
|
||||
_("Permissions"),
|
||||
{
|
||||
"fields": (
|
||||
"is_active",
|
||||
"is_staff",
|
||||
"is_superuser",
|
||||
"groups",
|
||||
"user_permissions",
|
||||
),
|
||||
},
|
||||
),
|
||||
(_("Important dates"), {"fields": ("last_login", "date_joined")}),
|
||||
)
|
||||
list_display = ["username", "name", "is_superuser"]
|
||||
search_fields = ["name"]
|
||||
13
diários_oficiais_alems/users/apps.py
Normal file
13
diários_oficiais_alems/users/apps.py
Normal file
@ -0,0 +1,13 @@
|
||||
import contextlib
|
||||
|
||||
from django.apps import AppConfig
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class UsersConfig(AppConfig):
|
||||
name = "diários_oficiais_alems.users"
|
||||
verbose_name = _("Users")
|
||||
|
||||
def ready(self):
|
||||
with contextlib.suppress(ImportError):
|
||||
import diários_oficiais_alems.users.signals # noqa: F401
|
||||
8
diários_oficiais_alems/users/context_processors.py
Normal file
8
diários_oficiais_alems/users/context_processors.py
Normal file
@ -0,0 +1,8 @@
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
def allauth_settings(request):
|
||||
"""Expose some settings from django-allauth in templates."""
|
||||
return {
|
||||
"ACCOUNT_ALLOW_REGISTRATION": settings.ACCOUNT_ALLOW_REGISTRATION,
|
||||
}
|
||||
40
diários_oficiais_alems/users/forms.py
Normal file
40
diários_oficiais_alems/users/forms.py
Normal file
@ -0,0 +1,40 @@
|
||||
from allauth.account.forms import SignupForm
|
||||
from allauth.socialaccount.forms import SignupForm as SocialSignupForm
|
||||
from django.contrib.auth import forms as admin_forms
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from .models import User
|
||||
|
||||
|
||||
class UserAdminChangeForm(admin_forms.UserChangeForm):
|
||||
class Meta(admin_forms.UserChangeForm.Meta): # type: ignore[name-defined]
|
||||
model = User
|
||||
|
||||
|
||||
class UserAdminCreationForm(admin_forms.UserCreationForm):
|
||||
"""
|
||||
Form for User Creation in the Admin Area.
|
||||
To change user signup, see UserSignupForm and UserSocialSignupForm.
|
||||
"""
|
||||
|
||||
class Meta(admin_forms.UserCreationForm.Meta): # type: ignore[name-defined]
|
||||
model = User
|
||||
error_messages = {
|
||||
"username": {"unique": _("This username has already been taken.")},
|
||||
}
|
||||
|
||||
|
||||
class UserSignupForm(SignupForm):
|
||||
"""
|
||||
Form that will be rendered on a user sign up section/screen.
|
||||
Default fields will be added automatically.
|
||||
Check UserSocialSignupForm for accounts created from social.
|
||||
"""
|
||||
|
||||
|
||||
class UserSocialSignupForm(SocialSignupForm):
|
||||
"""
|
||||
Renders the form when user has signed up using social accounts.
|
||||
Default fields will be added automatically.
|
||||
See UserSignupForm otherwise.
|
||||
"""
|
||||
126
diários_oficiais_alems/users/migrations/0001_initial.py
Normal file
126
diários_oficiais_alems/users/migrations/0001_initial.py
Normal file
@ -0,0 +1,126 @@
|
||||
import django.contrib.auth.models
|
||||
import django.contrib.auth.validators
|
||||
import django.utils.timezone
|
||||
from django.db import migrations
|
||||
from django.db import models
|
||||
|
||||
import diários_oficiais_alems.users.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
("auth", "0012_alter_user_first_name_max_length"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="User",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("password", models.CharField(max_length=128, verbose_name="password")),
|
||||
(
|
||||
"last_login",
|
||||
models.DateTimeField(
|
||||
blank=True, null=True, verbose_name="last login",
|
||||
),
|
||||
),
|
||||
(
|
||||
"is_superuser",
|
||||
models.BooleanField(
|
||||
default=False,
|
||||
help_text="Designates that this user has all permissions without explicitly assigning them.",
|
||||
verbose_name="superuser status",
|
||||
),
|
||||
),(
|
||||
"username",
|
||||
models.CharField(
|
||||
error_messages={
|
||||
"unique": "A user with that username already exists."
|
||||
},
|
||||
help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.",
|
||||
max_length=150,
|
||||
unique=True,
|
||||
validators=[
|
||||
django.contrib.auth.validators.UnicodeUsernameValidator()
|
||||
],
|
||||
verbose_name="username",
|
||||
),
|
||||
),
|
||||
(
|
||||
"email",
|
||||
models.EmailField(
|
||||
blank=True, max_length=254, verbose_name="email address",
|
||||
),
|
||||
),
|
||||
(
|
||||
"is_staff",
|
||||
models.BooleanField(
|
||||
default=False,
|
||||
help_text="Designates whether the user can log into this admin site.",
|
||||
verbose_name="staff status",
|
||||
),
|
||||
),
|
||||
(
|
||||
"is_active",
|
||||
models.BooleanField(
|
||||
default=True,
|
||||
help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.",
|
||||
verbose_name="active",
|
||||
),
|
||||
),
|
||||
(
|
||||
"date_joined",
|
||||
models.DateTimeField(
|
||||
default=django.utils.timezone.now, verbose_name="date joined",
|
||||
),
|
||||
),
|
||||
(
|
||||
"name",
|
||||
models.CharField(
|
||||
blank=True, max_length=255, verbose_name="Name of User",
|
||||
),
|
||||
),
|
||||
(
|
||||
"groups",
|
||||
models.ManyToManyField(
|
||||
blank=True,
|
||||
help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.",
|
||||
related_name="user_set",
|
||||
related_query_name="user",
|
||||
to="auth.Group",
|
||||
verbose_name="groups",
|
||||
),
|
||||
),
|
||||
(
|
||||
"user_permissions",
|
||||
models.ManyToManyField(
|
||||
blank=True,
|
||||
help_text="Specific permissions for this user.",
|
||||
related_name="user_set",
|
||||
related_query_name="user",
|
||||
to="auth.Permission",
|
||||
verbose_name="user permissions",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "user",
|
||||
"verbose_name_plural": "users",
|
||||
"abstract": False,
|
||||
},
|
||||
managers=[
|
||||
("objects", django.contrib.auth.models.UserManager()),
|
||||
],
|
||||
),
|
||||
]
|
||||
0
diários_oficiais_alems/users/migrations/__init__.py
Normal file
0
diários_oficiais_alems/users/migrations/__init__.py
Normal file
26
diários_oficiais_alems/users/models.py
Normal file
26
diários_oficiais_alems/users/models.py
Normal file
@ -0,0 +1,26 @@
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.db.models import CharField
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class User(AbstractUser):
|
||||
"""
|
||||
Default custom user model for Diários Oficiais ALEMS.
|
||||
If adding fields that need to be filled at user signup,
|
||||
check forms.SignupForm and forms.SocialSignupForms accordingly.
|
||||
"""
|
||||
|
||||
# First and last name do not cover name patterns around the globe
|
||||
name = CharField(_("Name of User"), blank=True, max_length=255)
|
||||
first_name = None # type: ignore[assignment]
|
||||
last_name = None # type: ignore[assignment]
|
||||
|
||||
def get_absolute_url(self) -> str:
|
||||
"""Get URL for user's detail view.
|
||||
|
||||
Returns:
|
||||
str: URL for user detail.
|
||||
|
||||
"""
|
||||
return reverse("users:detail", kwargs={"username": self.username})
|
||||
0
diários_oficiais_alems/users/tests/__init__.py
Normal file
0
diários_oficiais_alems/users/tests/__init__.py
Normal file
41
diários_oficiais_alems/users/tests/factories.py
Normal file
41
diários_oficiais_alems/users/tests/factories.py
Normal file
@ -0,0 +1,41 @@
|
||||
from collections.abc import Sequence
|
||||
from typing import Any
|
||||
|
||||
from factory import Faker
|
||||
from factory import post_generation
|
||||
from factory.django import DjangoModelFactory
|
||||
|
||||
from diários_oficiais_alems.users.models import User
|
||||
|
||||
|
||||
class UserFactory(DjangoModelFactory[User]):
|
||||
username = Faker("user_name")
|
||||
email = Faker("email")
|
||||
name = Faker("name")
|
||||
|
||||
@post_generation
|
||||
def password(self, create: bool, extracted: Sequence[Any], **kwargs): # noqa: FBT001
|
||||
password = (
|
||||
extracted
|
||||
if extracted
|
||||
else Faker(
|
||||
"password",
|
||||
length=42,
|
||||
special_chars=True,
|
||||
digits=True,
|
||||
upper_case=True,
|
||||
lower_case=True,
|
||||
).evaluate(None, None, extra={"locale": None})
|
||||
)
|
||||
self.set_password(password)
|
||||
|
||||
@classmethod
|
||||
def _after_postgeneration(cls, instance, create, results=None):
|
||||
"""Save again the instance if creating and at least one hook ran."""
|
||||
if create and results and not cls._meta.skip_postgeneration_save:
|
||||
# Some post-generation hooks ran, and may have modified us.
|
||||
instance.save()
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
django_get_or_create = ["username"]
|
||||
65
diários_oficiais_alems/users/tests/test_admin.py
Normal file
65
diários_oficiais_alems/users/tests/test_admin.py
Normal file
@ -0,0 +1,65 @@
|
||||
import contextlib
|
||||
from http import HTTPStatus
|
||||
from importlib import reload
|
||||
|
||||
import pytest
|
||||
from django.contrib import admin
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.urls import reverse
|
||||
from pytest_django.asserts import assertRedirects
|
||||
|
||||
from diários_oficiais_alems.users.models import User
|
||||
|
||||
|
||||
class TestUserAdmin:
|
||||
def test_changelist(self, admin_client):
|
||||
url = reverse("admin:users_user_changelist")
|
||||
response = admin_client.get(url)
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
|
||||
def test_search(self, admin_client):
|
||||
url = reverse("admin:users_user_changelist")
|
||||
response = admin_client.get(url, data={"q": "test"})
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
|
||||
def test_add(self, admin_client):
|
||||
url = reverse("admin:users_user_add")
|
||||
response = admin_client.get(url)
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
|
||||
response = admin_client.post(
|
||||
url,
|
||||
data={
|
||||
"username": "test",
|
||||
"password1": "My_R@ndom-P@ssw0rd",
|
||||
"password2": "My_R@ndom-P@ssw0rd",
|
||||
},
|
||||
)
|
||||
assert response.status_code == HTTPStatus.FOUND
|
||||
assert User.objects.filter(username="test").exists()
|
||||
|
||||
def test_view_user(self, admin_client):
|
||||
user = User.objects.get(username="admin")
|
||||
url = reverse("admin:users_user_change", kwargs={"object_id": user.pk})
|
||||
response = admin_client.get(url)
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
|
||||
@pytest.fixture
|
||||
def _force_allauth(self, settings):
|
||||
settings.DJANGO_ADMIN_FORCE_ALLAUTH = True
|
||||
# Reload the admin module to apply the setting change
|
||||
import diários_oficiais_alems.users.admin as users_admin
|
||||
|
||||
with contextlib.suppress(admin.sites.AlreadyRegistered): # type: ignore[attr-defined]
|
||||
reload(users_admin)
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.usefixtures("_force_allauth")
|
||||
def test_allauth_login(self, rf, settings):
|
||||
request = rf.get("/fake-url")
|
||||
request.user = AnonymousUser()
|
||||
response = admin.site.login(request)
|
||||
|
||||
# The `admin` login view should redirect to the `allauth` login view
|
||||
target_url = reverse(settings.LOGIN_URL) + "?next=" + request.path
|
||||
assertRedirects(response, target_url, fetch_redirect_response=False)
|
||||
35
diários_oficiais_alems/users/tests/test_forms.py
Normal file
35
diários_oficiais_alems/users/tests/test_forms.py
Normal file
@ -0,0 +1,35 @@
|
||||
"""Module for all Form Tests."""
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from diários_oficiais_alems.users.forms import UserAdminCreationForm
|
||||
from diários_oficiais_alems.users.models import User
|
||||
|
||||
|
||||
class TestUserAdminCreationForm:
|
||||
"""
|
||||
Test class for all tests related to the UserAdminCreationForm
|
||||
"""
|
||||
|
||||
def test_username_validation_error_msg(self, user: User):
|
||||
"""
|
||||
Tests UserAdminCreation Form's unique validator functions correctly by testing:
|
||||
1) A new user with an existing username cannot be added.
|
||||
2) Only 1 error is raised by the UserCreation Form
|
||||
3) The desired error message is raised
|
||||
"""
|
||||
|
||||
# The user already exists,
|
||||
# hence cannot be created.
|
||||
form = UserAdminCreationForm(
|
||||
{
|
||||
"username": user.username,
|
||||
"password1": user.password,
|
||||
"password2": user.password,
|
||||
},
|
||||
)
|
||||
|
||||
assert not form.is_valid()
|
||||
assert len(form.errors) == 1
|
||||
assert "username" in form.errors
|
||||
assert form.errors["username"][0] == _("This username has already been taken.")
|
||||
5
diários_oficiais_alems/users/tests/test_models.py
Normal file
5
diários_oficiais_alems/users/tests/test_models.py
Normal file
@ -0,0 +1,5 @@
|
||||
from diários_oficiais_alems.users.models import User
|
||||
|
||||
|
||||
def test_user_get_absolute_url(user: User):
|
||||
assert user.get_absolute_url() == f"/users/{user.username}/"
|
||||
22
diários_oficiais_alems/users/tests/test_urls.py
Normal file
22
diários_oficiais_alems/users/tests/test_urls.py
Normal file
@ -0,0 +1,22 @@
|
||||
from django.urls import resolve
|
||||
from django.urls import reverse
|
||||
|
||||
from diários_oficiais_alems.users.models import User
|
||||
|
||||
|
||||
def test_detail(user: User):
|
||||
assert (
|
||||
reverse("users:detail", kwargs={"username": user.username})
|
||||
== f"/users/{user.username}/"
|
||||
)
|
||||
assert resolve(f"/users/{user.username}/").view_name == "users:detail"
|
||||
|
||||
|
||||
def test_update():
|
||||
assert reverse("users:update") == "/users/~update/"
|
||||
assert resolve("/users/~update/").view_name == "users:update"
|
||||
|
||||
|
||||
def test_redirect():
|
||||
assert reverse("users:redirect") == "/users/~redirect/"
|
||||
assert resolve("/users/~redirect/").view_name == "users:redirect"
|
||||
101
diários_oficiais_alems/users/tests/test_views.py
Normal file
101
diários_oficiais_alems/users/tests/test_views.py
Normal file
@ -0,0 +1,101 @@
|
||||
from http import HTTPStatus
|
||||
|
||||
import pytest
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.contrib.messages.middleware import MessageMiddleware
|
||||
from django.contrib.sessions.middleware import SessionMiddleware
|
||||
from django.http import HttpRequest
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.test import RequestFactory
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from diários_oficiais_alems.users.forms import UserAdminChangeForm
|
||||
from diários_oficiais_alems.users.models import User
|
||||
from diários_oficiais_alems.users.tests.factories import UserFactory
|
||||
from diários_oficiais_alems.users.views import UserRedirectView
|
||||
from diários_oficiais_alems.users.views import UserUpdateView
|
||||
from diários_oficiais_alems.users.views import user_detail_view
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
class TestUserUpdateView:
|
||||
"""
|
||||
TODO:
|
||||
extracting view initialization code as class-scoped fixture
|
||||
would be great if only pytest-django supported non-function-scoped
|
||||
fixture db access -- this is a work-in-progress for now:
|
||||
https://github.com/pytest-dev/pytest-django/pull/258
|
||||
"""
|
||||
|
||||
def dummy_get_response(self, request: HttpRequest):
|
||||
return None
|
||||
|
||||
def test_get_success_url(self, user: User, rf: RequestFactory):
|
||||
view = UserUpdateView()
|
||||
request = rf.get("/fake-url/")
|
||||
request.user = user
|
||||
|
||||
view.request = request
|
||||
assert view.get_success_url() == f"/users/{user.username}/"
|
||||
|
||||
def test_get_object(self, user: User, rf: RequestFactory):
|
||||
view = UserUpdateView()
|
||||
request = rf.get("/fake-url/")
|
||||
request.user = user
|
||||
|
||||
view.request = request
|
||||
|
||||
assert view.get_object() == user
|
||||
|
||||
def test_form_valid(self, user: User, rf: RequestFactory):
|
||||
view = UserUpdateView()
|
||||
request = rf.get("/fake-url/")
|
||||
|
||||
# Add the session/message middleware to the request
|
||||
SessionMiddleware(self.dummy_get_response).process_request(request)
|
||||
MessageMiddleware(self.dummy_get_response).process_request(request)
|
||||
request.user = user
|
||||
|
||||
view.request = request
|
||||
|
||||
# Initialize the form
|
||||
form = UserAdminChangeForm()
|
||||
form.cleaned_data = {}
|
||||
form.instance = user
|
||||
view.form_valid(form)
|
||||
|
||||
messages_sent = [m.message for m in messages.get_messages(request)]
|
||||
assert messages_sent == [_("Information successfully updated")]
|
||||
|
||||
|
||||
class TestUserRedirectView:
|
||||
def test_get_redirect_url(self, user: User, rf: RequestFactory):
|
||||
view = UserRedirectView()
|
||||
request = rf.get("/fake-url")
|
||||
request.user = user
|
||||
|
||||
view.request = request
|
||||
assert view.get_redirect_url() == f"/users/{user.username}/"
|
||||
|
||||
|
||||
class TestUserDetailView:
|
||||
def test_authenticated(self, user: User, rf: RequestFactory):
|
||||
request = rf.get("/fake-url/")
|
||||
request.user = UserFactory()
|
||||
response = user_detail_view(request, username=user.username)
|
||||
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
|
||||
def test_not_authenticated(self, user: User, rf: RequestFactory):
|
||||
request = rf.get("/fake-url/")
|
||||
request.user = AnonymousUser()
|
||||
response = user_detail_view(request, username=user.username)
|
||||
login_url = reverse(settings.LOGIN_URL)
|
||||
|
||||
assert isinstance(response, HttpResponseRedirect)
|
||||
assert response.status_code == HTTPStatus.FOUND
|
||||
assert response.url == f"{login_url}?next=/fake-url/"
|
||||
12
diários_oficiais_alems/users/urls.py
Normal file
12
diários_oficiais_alems/users/urls.py
Normal file
@ -0,0 +1,12 @@
|
||||
from django.urls import path
|
||||
|
||||
from .views import user_detail_view
|
||||
from .views import user_redirect_view
|
||||
from .views import user_update_view
|
||||
|
||||
app_name = "users"
|
||||
urlpatterns = [
|
||||
path("~redirect/", view=user_redirect_view, name="redirect"),
|
||||
path("~update/", view=user_update_view, name="update"),
|
||||
path("<str:username>/", view=user_detail_view, name="detail"),
|
||||
]
|
||||
46
diários_oficiais_alems/users/views.py
Normal file
46
diários_oficiais_alems/users/views.py
Normal file
@ -0,0 +1,46 @@
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.db.models import QuerySet
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic import DetailView
|
||||
from django.views.generic import RedirectView
|
||||
from django.views.generic import UpdateView
|
||||
|
||||
from diários_oficiais_alems.users.models import User
|
||||
|
||||
|
||||
class UserDetailView(LoginRequiredMixin, DetailView):
|
||||
model = User
|
||||
slug_field = "username"
|
||||
slug_url_kwarg = "username"
|
||||
|
||||
|
||||
user_detail_view = UserDetailView.as_view()
|
||||
|
||||
|
||||
class UserUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
|
||||
model = User
|
||||
fields = ["name"]
|
||||
success_message = _("Information successfully updated")
|
||||
|
||||
def get_success_url(self) -> str:
|
||||
assert self.request.user.is_authenticated # type guard
|
||||
return self.request.user.get_absolute_url()
|
||||
|
||||
def get_object(self, queryset: QuerySet | None=None) -> User:
|
||||
assert self.request.user.is_authenticated # type guard
|
||||
return self.request.user
|
||||
|
||||
|
||||
user_update_view = UserUpdateView.as_view()
|
||||
|
||||
|
||||
class UserRedirectView(LoginRequiredMixin, RedirectView):
|
||||
permanent = False
|
||||
|
||||
def get_redirect_url(self) -> str:
|
||||
return reverse("users:detail", kwargs={"username": self.request.user.username})
|
||||
|
||||
|
||||
user_redirect_view = UserRedirectView.as_view()
|
||||
Reference in New Issue
Block a user