Skip to content
Snippets Groups Projects
Commit 1494a197 authored by Tomáš Valenta's avatar Tomáš Valenta
Browse files

implement AO requests - new fields and name changes, categorization, select2...

implement AO requests - new fields and name changes, categorization, select2 searching, state and other minor changes
parent 7ebf881c
No related branches found
No related tags found
No related merge requests found
......@@ -59,36 +59,77 @@ class ContractAdmin(MarkdownxGuardedModelAdmin):
ContractIntentInline,
)
def get_fields(self, request, obj=None):
fields = [
"identifier",
def get_fieldsets(self, request, obj=None):
fieldsets = [
(
"Základní informace",
{
"fields": [
"name",
"id_number",
"types",
"summary",
"public_state",
"publishing_rejection_comment",
"primary_contract",
]
}
),
(
"Data",
{
"fields": [
"valid_start_date",
"valid_end_date",
]
}
),
(
"Administrativa",
{
"fields": [
"legal_state",
"public_state",
"publishing_rejection_comment",
"paper_form_state",
"filing_area",
]
}
),
(
"Odkazy",
{
"fields": [
"tender_url",
"agreement_url",
]
}
),
(
"Náklady",
{
"fields": [
"cost_amount",
"cost_unit",
]
}
),
(
"Doplňující informace",
{
"fields": [
"issues",
"expected_cost_year",
"expected_cost_month",
"expected_cost_hour",
"filing_area",
"primary_contract",
"notes",
"created_by",
]
}
)
]
if request.user.is_superuser or request.user.has_perm("approve", self):
fields.insert(
fields.index("summary"),
"is_approved",
fieldsets[0][1]["fields"].insert(
fieldsets[0][1]["fields"].index("publishing_rejection_comment"),
"approval_state",
)
return fields
return fieldsets
def save_model(self, request, obj, form, change) -> None:
if obj.created_by is None:
......
import djhacker
import dal.autocomplete
from django import forms
from webpack_loader.loader import WebpackLoader
from .models import Contractee, Signee
from .models import Contract, ContracteeSignature, SigneeSignature
class ContractAdminForm(forms.ModelForm):
......@@ -20,3 +23,53 @@ class SigneeAdminForm(forms.ModelForm):
"shared/shared.js",
"shared/admin_signee_form.js",
)
# BEGIN Autocompleted Contract fields
for foreign_key_field in (
Contract.filing_area,
Contract.primary_contract,
):
djhacker.formfield(
foreign_key_field,
forms.ModelChoiceField,
widget=dal.autocomplete.ModelSelect2(
url="contracts:select2_djhacker_contract_autocomplete"
)
)
for many_to_many_field in (
Contract.types,
Contract.issues,
):
djhacker.formfield(
many_to_many_field,
forms.ModelChoiceField,
widget=dal.autocomplete.ModelSelect2Multiple(
url="contracts:select2_djhacker_contract_autocomplete"
)
)
# END Autocompleted Contract fields
# BEGIN Autocompleted ContracteeSignature / SigneeSignature fields
djhacker.formfield(
ContracteeSignature.contractee,
forms.ModelChoiceField,
widget=dal.autocomplete.ModelSelect2(
url="contracts:select2_djhacker_contractee_signature_autocomplete"
)
)
djhacker.formfield(
SigneeSignature.signee,
forms.ModelChoiceField,
widget=dal.autocomplete.ModelSelect2(
url="contracts:select2_djhacker_signee_signature_autocomplete"
)
)
# END Autocompleted ContracteeSignature / SigneeSignature fields
# Generated by Django 4.1.4 on 2023-02-23 19:44
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('contracts', '0001_initial'),
]
operations = [
migrations.RemoveField(
model_name='contract',
name='expected_cost_hour',
),
migrations.RemoveField(
model_name='contract',
name='expected_cost_month',
),
migrations.RemoveField(
model_name='contract',
name='expected_cost_year',
),
migrations.RemoveField(
model_name='contract',
name='identifier',
),
migrations.RemoveField(
model_name='contract',
name='is_approved',
),
migrations.RemoveField(
model_name='contractee',
name='color',
),
migrations.RemoveField(
model_name='contracteerepresentative',
name='role',
),
migrations.RemoveField(
model_name='signee',
name='color',
),
migrations.RemoveField(
model_name='signee',
name='is_legal_entity',
),
migrations.RemoveField(
model_name='signeerepresentative',
name='role',
),
migrations.AddField(
model_name='contract',
name='approval_state',
field=models.CharField(choices=[('unknown', 'Nová'), ('no', 'Odmítnutá'), ('yes', 'Přijatá')], default='unknown', help_text='Může měnit jen schvalovatel. Pokud je smlouva veřejná, se stavem "Přijatá" se vypustí ven.', max_length=7, verbose_name='Stav schválení'),
),
migrations.AddField(
model_name='contract',
name='cost_amount',
field=models.IntegerField(blank=True, null=True, verbose_name='Náklady (Kč)'),
),
migrations.AddField(
model_name='contract',
name='cost_type',
field=models.CharField(blank=True, choices=[('hour', 'Hodina'), ('month', 'Měsíc'), ('year', 'Rok'), ('total', 'Celkem')], max_length=5, null=True, verbose_name='Jednotka nákladů'),
),
migrations.AddField(
model_name='contract',
name='id_number',
field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Identifikační číslo'),
),
migrations.AddField(
model_name='contract',
name='name',
field=models.CharField(default='', max_length=128, verbose_name='Název'),
preserve_default=False,
),
migrations.AddField(
model_name='contractee',
name='role',
field=models.CharField(blank=True, max_length=256, null=True, verbose_name='Role'),
),
migrations.AddField(
model_name='signee',
name='entity_type',
field=models.CharField(choices=[('physical_person', 'Fyzická osoba'), ('legal_entity', 'Právnická osoba'), ('business_natural_person', 'Podnikající fyzická osoba')], default='legal_entity', help_text='Důležité označit správně! Fyzickým osobám nepublikujeme adresu.', max_length=23, verbose_name='Typ'),
preserve_default=False,
),
migrations.AddField(
model_name='signee',
name='role',
field=models.CharField(blank=True, max_length=256, null=True, verbose_name='Role'),
),
migrations.AlterField(
model_name='contract',
name='legal_state',
field=models.CharField(choices=[('valid', 'Platná'), ('invalid', 'Neplatná')], max_length=13, verbose_name='Stav právního ujednání'),
),
migrations.AlterField(
model_name='contract',
name='paper_form_state',
field=models.CharField(choices=[('sent', 'Odeslaná'), ('stored', 'Uložená'), ('to_shred', 'Ke skartaci'), ('shredded', 'Skartovaná'), ('lost', 'Ztracená')], max_length=8, verbose_name='Stav fyzického dokumentu'),
),
migrations.AlterField(
model_name='contract',
name='valid_end_date',
field=models.DateField(blank=True, null=True, verbose_name='Konec účinnosti'),
),
migrations.AlterField(
model_name='contract',
name='valid_start_date',
field=models.DateField(blank=True, null=True, verbose_name='Začátek účinnosti'),
),
migrations.AlterField(
model_name='signee',
name='address_street_with_number',
field=models.CharField(help_text='Veřejné pouze, když typ není nastaven na fyzickou osobu.', max_length=256, verbose_name='Ulice, č.p.'),
),
migrations.AlterField(
model_name='signee',
name='address_zip',
field=models.CharField(help_text='Veřejné pouze, když typ není nastaven na fyzickou osobu.', max_length=16, verbose_name='PSČ'),
),
]
# Generated by Django 4.1.4 on 2023-02-23 21:08
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('contracts', '0002_remove_contract_expected_cost_hour_and_more'),
]
operations = [
migrations.RenameField(
model_name='contract',
old_name='cost_type',
new_name='cost_unit',
),
]
# Generated by Django 4.1.4 on 2023-02-23 21:33
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('contracts', '0003_rename_cost_type_contract_cost_unit'),
]
operations = [
migrations.AlterField(
model_name='contract',
name='approval_state',
field=models.CharField(blank=True, choices=[('no', 'Odmítnutá'), ('yes', 'Přijatá')], help_text='Může měnit jen schvalovatel. Pokud je smlouva veřejná, se stavem "Přijatá" se vypustí ven.', max_length=7, null=True, verbose_name='Stav schválení'),
),
]
import math
from colorfield.fields import ColorField
from django.conf import settings
from django.db import models
from markdownx.models import MarkdownxField
......@@ -18,68 +17,37 @@ class RepresentativeMixin:
def function(self):
raise NotImplementedError
@property
def role(self):
raise NotImplementedError
def __str__(self) -> str:
result = self.name
if self.function is not None:
result += f", {self.function}"
if self.role is not None:
result += f" ({self.role})"
return result
class ColorMixin(models.Model):
color = ColorField(
blank=True,
null=True,
verbose_name="Barva",
)
@property
def color_is_light(self) -> bool:
if self.color is None:
raise ValueError
# https://stackoverflow.com/a/29643643 - Thanks to John1024 and vallentin!
# https://stackoverflow.com/a/58270890 - Thanks to Kardi Teknomo!
hex_color = self.color.lstrip("#")
rgb_color = tuple(int(hex_color[i : i + 2], 16) for i in (0, 2, 4))
[r, g, b] = rgb_color
hsp = math.sqrt(0.299 * (r * r) + 0.587 * (g * g) + 0.114 * (b * b))
if hsp > 127.5:
return True
else:
return False
class Meta:
abstract = True
class Signee(ColorMixin, models.Model):
class Signee(models.Model):
name = models.CharField(
max_length=256,
verbose_name="Jméno",
)
is_legal_entity = models.BooleanField(
verbose_name="Je právnická osoba",
help_text="Důležité označit správně! Pokud není osoba právnická, zveřejňujeme pouze obec a zemi.",
class EntityTypes(models.TextChoices):
NATURAL_PERSON = "physical_person", "Fyzická osoba"
LEGAL_ENTITY = "legal_entity", "Právnická osoba"
BUSINESS_NATURAL_PERSON = "business_natural_person", "Podnikající fyzická osoba"
entity_type = models.CharField(
max_length=23,
choices=EntityTypes.choices,
verbose_name="Typ",
help_text="Důležité označit správně! Fyzickým osobám nepublikujeme adresu.",
)
address_street_with_number = models.CharField(
max_length=256,
verbose_name="Ulice, č.p.",
help_text="Viditelné pouze u právnických osob.",
help_text="Veřejné pouze, když typ není nastaven na fyzickou osobu.",
) # WARNING: Legal entity status dependent!
address_district = models.CharField(
......@@ -90,7 +58,7 @@ class Signee(ColorMixin, models.Model):
address_zip = models.CharField(
max_length=16,
verbose_name="PSČ",
help_text="Viditelné pouze u právnických osob.",
help_text="Veřejné pouze, když typ není nastaven na fyzickou osobu.",
) # WARNING: Legal entity status dependent!
address_country = models.CharField(
......@@ -119,6 +87,13 @@ class Signee(ColorMixin, models.Model):
verbose_name="Organizační složka",
)
role = models.CharField(
max_length=256,
blank=True,
null=True,
verbose_name="Role",
)
class Meta:
verbose_name = "Jiná smluvní strana"
verbose_name_plural = "Ostatní smluvní strany"
......@@ -166,19 +141,12 @@ class SigneeRepresentative(RepresentativeMixin, models.Model):
verbose_name="Funkce",
)
role = models.CharField(
max_length=256,
blank=True,
null=True,
verbose_name="Role",
)
class Meta:
verbose_name = "Zástupce"
verbose_name_plural = "Zástupci"
class Contractee(ColorMixin, models.Model):
class Contractee(models.Model):
name = models.CharField(
max_length=256,
default=settings.DEFAULT_CONTRACTEE_NAME,
......@@ -224,6 +192,13 @@ class Contractee(ColorMixin, models.Model):
verbose_name="Organizační složka",
)
role = models.CharField(
max_length=256,
blank=True,
null=True,
verbose_name="Role",
)
class Meta:
verbose_name = "Naše smluvní strana"
verbose_name_plural = "Naše smluvní strany"
......@@ -265,13 +240,6 @@ class ContracteeRepresentative(RepresentativeMixin, models.Model):
verbose_name="Funkce",
)
role = models.CharField(
max_length=256,
blank=True,
null=True,
verbose_name="Role",
)
class Meta:
verbose_name = "Zástupce"
verbose_name_plural = "Zástupci"
......@@ -346,8 +314,35 @@ class Contract(models.Model):
# END Automatically set fields
identifier = models.CharField(
# BEGIN Approval fields
class ApprovalStates(models.TextChoices):
NO = "no", "Odmítnutá"
YES = "yes", "Přijatá"
approval_state = models.CharField(
max_length=7,
choices=ApprovalStates.choices,
blank=True,
null=True,
verbose_name="Stav schválení",
help_text=(
"Může měnit jen schvalovatel. Pokud je smlouva "
"veřejná, se stavem \"Přijatá\" se vypustí ven."
),
)
# END Approval fields
name = models.CharField(
max_length=128,
verbose_name="Název",
)
id_number = models.CharField(
max_length=128,
blank=True,
null=True,
verbose_name="Identifikační číslo",
)
......@@ -363,25 +358,19 @@ class Contract(models.Model):
verbose_name="Sumarizace obsahu smlouvy",
)
is_approved = models.BooleanField(
verbose_name="Je schválená",
help_text=(
"Může měnit jen schvalovatel. Pokud je smlouva "
"veřejná, zaškrtnutím se vypustí ven."
),
)
valid_start_date = models.DateField(
blank=True,
null=True,
verbose_name="Začátek účinnosti",
)
valid_end_date = models.DateField(
blank=True,
null=True,
verbose_name="Konec účinnosti",
)
class LegalStates(models.TextChoices):
VALID = "valid", "Platná"
EFFECTIVE = "effective", "Účinná"
NOT_EFFECTIVE = "not_effective", "Neúčinná"
INVALID = "invalid", "Neplatná"
class PublicStates(models.TextChoices):
......@@ -410,7 +399,7 @@ class Contract(models.Model):
paper_form_state = models.CharField(
max_length=8,
choices=PaperFormStates.choices,
verbose_name="Stav papírové formy",
verbose_name="Stav fyzického dokumentu",
)
publishing_rejection_comment = models.CharField(
......@@ -442,24 +431,24 @@ class Contract(models.Model):
help_text='Veřejně nazváno "Poznámky".',
)
# NOTE: Could we make this into expected_cost_type and expected_cost_amount?
expected_cost_year = models.IntegerField(
blank=True,
null=True,
verbose_name="Očekávané výdaje (rok)",
)
class CostUnits(models.TextChoices):
HOUR = "hour", "Hodina"
MONTH = "month", "Měsíc"
YEAR = "year", "Rok"
TOTAL = "total", "Celkem"
expected_cost_month = models.IntegerField(
cost_amount = models.IntegerField(
blank=True,
null=True,
verbose_name="Očekávané výdaje (měsíc)",
verbose_name="Náklady (Kč)"
)
expected_cost_hour = models.IntegerField(
cost_unit = models.CharField(
max_length=5,
choices=CostUnits.choices,
blank=True,
null=True,
verbose_name="Očekávané výdaje (hodina)",
verbose_name="Jednotka nákladů"
)
filing_area = models.ForeignKey(
......@@ -496,7 +485,7 @@ class Contract(models.Model):
permissions = (("approve", "Schválit / zrušit schválení"),)
def __str__(self) -> str:
return self.identifier
return self.name
class ContractFile(NameStrMixin, models.Model):
......
......@@ -6,9 +6,9 @@
<table class="table table-auto w-full table--striped table--bordered">
<thead>
<tr>
<td class="font-bold">Identifikace</td>
<td class="font-bold">Název</td>
<td>Typy</td>
<td>Právní stav</td>
<td>Platná</td>
<td>Účinná od</td>
<td>Účinná do</td>
<td>Podepsána s</td>
......@@ -21,7 +21,7 @@
<a
class="underline"
href="{% url "contracts:view_contract" contract.id %}"
>{{ contract.identifier }}</a>
>{{ contract.name }}</a>
</td>
<td>
<ul class="flex flex-wrap gap-1.5">
......@@ -30,15 +30,16 @@
{% endfor %}
</ul>
</td>
<td>{{ contract.get_legal_state_display }}</td>
<td>
<i class="{% if contract.legal_state == contract.PublicStates.YES.0 %}ico--checkmark{% else %}ico--cross{% endif %}"></i>
</td>
<td class="whitespace-nowrap">{{ contract.valid_start_date }}</td>
<td class="whitespace-nowrap">{{ contract.valid_end_date }}</td>
<td>
<ul class="flex flex-wrap gap-1.5">
{% for signature in contract.signee_signatures.all %}
<li
class="p-1.5 rounded-sm whitespace-nowrap cursor-pointer {% if not signature.signee.color %}bg-gray-200 duration-100 hover:bg-gray-300{% endif %}{% if signature.signee.color and not signature.signee.color_is_light %} text-white{% endif %}"
{% if signature.signee.color %}style="background-color:{{ signature.signee.color }}"{% endif %}
class="p-1.5 rounded-sm whitespace-nowrap cursor-pointer bg-gray-200 duration-100 hover:bg-gray-300"
>{{ signature.signee.name }}</li>
{% endfor %}
</ul>
......
......@@ -2,12 +2,18 @@
{% load subtract %}
{% block content %}
<h1 class="head-alt-lg mb-10">{{ contract.identifier }}</h1>
<h1 class="head-alt-lg mb-10">{{ contract.name }}</h1>
<h2 class="text-xl font-bold mb-5"><i class="ico--info mr-3"></i>Základní informace</h2>
<table class="table table-auto w-full table--striped table--bordered mb-10">
<tbody>
{% if contract.id_number %}
<tr>
<td class="w-1/5 !p-2.5">Identifikační číslo</td>
<td class="w-4/5 !p-2.5">{{ contract.id_number }}</td>
</tr>
{% endif %}
<tr>
<td class="w-1/5 !p-2.5">Typy</td>
<td class="w-4/5 !p-2.5">
......@@ -29,7 +35,7 @@
<td class="w-4/5 !p-2.5">
<a
href="{% url "contracts:view_contract" contract.primary_contract.id %}"
>{{ contract.primary_contract.identifier }}</a>
>{{ contract.primary_contract.name }}</a>
</td>
</tr>
{% endif %}
......@@ -116,27 +122,16 @@
{% endwith %}
</td>
</tr>
{% if contract.expected_cost_year %}
{% if contract.cost_amount %}
<tr>
<td class="w-1/5 !p-2.5">Očekávané náklady (rok)</td>
<td class="w-1/5 !p-2.5">Náklady</td>
<td class="w-4/5 !p-2.5">
{{ contract.expected_cost_month }} Kč
</td>
</tr>
{{ contract.cost_amount }} Kč
{% if contract.cost_unit != contract.CostUnits.TOTAL.0 %}
/ {{ contract.get_cost_unit_display }}
{% else %}
celkem
{% endif %}
{% if contract.expected_cost_month %}
<tr>
<td class="w-1/5 !p-2.5">Očekávané náklady (měsíc)</td>
<td class="w-4/5 !p-2.5">
{{ contract.expected_cost_month }} Kč
</td>
</tr>
{% endif %}
{% if contract.expected_cost_hour %}
<tr>
<td class="w-1/5 !p-2.5">Očekávané náklady (hodina)</td>
<td class="w-4/5 !p-2.5">
{{ contract.expected_cost_hour }} Kč
</td>
</tr>
{% endif %}
......@@ -176,13 +171,15 @@
<address class="mb-3">
<div class="mb-1">
<a
class="inline-block p-1.5 mb-1 rounded-sm whitespace-nowrap cursor-pointer not-italic hover:no-underline {% if not signature.contractee.color %}bg-gray-200 duration-100 hover:bg-gray-300{% endif %}{% if signature.contractee.color and not signature.contractee.color_is_light %} text-white{% endif %}"
{% if signature.contractee.color %}style="background-color:{{ signature.contractee.color }}"{% endif %}
class="inline-block p-1.5 mb-1 rounded-sm whitespace-nowrap cursor-pointer not-italic hover:no-underline bg-gray-200 duration-100 hover:bg-gray-300"
>
<strong>{{ signature.contractee.name }}</strong>
{% if signature.contractee.department %}
- {{ signature.contractee.department }}
{% endif %}
{% if signature.contractee.role %}
({{ signature.contractee.role }})
{% endif %}
</a>
</div>
......@@ -226,17 +223,27 @@
<address class="mb-3">
<div class="mb-1">
<a
class="inline-block p-1.5 mb-1 rounded-sm whitespace-nowrap cursor-pointer not-italic hover:no-underline {% if not signature.signee.color %}bg-gray-200 duration-100 hover:bg-gray-300{% endif %}{% if signature.signee.color and not signature.signee.color_is_light %} text-white{% endif %}"
{% if signature.signee.color %}style="background-color:{{ signature.signee.color }}"{% endif %}
class="inline-block p-1.5 mb-1 rounded-sm whitespace-nowrap cursor-pointer not-italic hover:no-underline bg-gray-200 duration-100 hover:bg-gray-300"
>
<strong>{{ signature.signee.name }}</strong>
{% if signature.signee.department %}
- {{ signature.signee.department }}
{% endif %}
{% if signature.signee.role %}
({{ signature.signee.role }})
{% endif %}
</a>
</div>
{% if signature.signee.is_legal_entity %}
<div class="mb-1">
{{ signature.signee.get_entity_type_display }}
{% if signature.signee.entity_type == signature.signee.EntityTypes.NATURAL_PERSON.0 %}
<span class="text-gray-300">(zobrazujeme pouze obec)</span>
{% elif %}
</div>
{% if signature.signee.entity_type == signature.signee.EntityTypes.NATURAL_PERSON.0 %}
{{ signature.signee.address_street_with_number }}<br>
{{ signature.signee.address_zip }} {{ signature.signee.address_district }}<br>
{{ signature.signee.get_address_country_display }}<br>
......@@ -247,12 +254,6 @@
{% if signature.signee.ico_number %}
IČO: {{ signature.signee.ico_number }}<br>
{% endif %}
{% if not signature.signee.is_legal_entity %}
<span class="block mt-2">
<small class="font-thin">(Fyzická osoba, ukazujeme pouze obec.)</small><br>
</span>
{% endif %}
</address>
<div class="block mb-2">
......
import dal.autocomplete
from django.urls import path
from . import views
from . import views, models
app_name = "contracts"
urlpatterns = [
path("", views.index, name="index"),
path("contracts/<int:id>", views.view_contract, name="view_contract"),
path(
"contracts/autocomplete",
dal.autocomplete.Select2QuerySetView.as_view(model=models.Contract),
name="select2_djhacker_contract_autocomplete",
),
path(
"contracts/signees/signatures/autocomplete",
dal.autocomplete.Select2QuerySetView.as_view(model=models.SigneeSignature),
name="select2_djhacker_signee_signature_autocomplete",
),
path(
"contracts/contractees/signatures/autocomplete",
dal.autocomplete.Select2QuerySetView.as_view(model=models.ContracteeSignature),
name="select2_djhacker_contractee_signature_autocomplete",
),
#path(
#"contracts/signees/autocomplete",
#dal.autocomplete.Select2QuerySetView.as_view(model=models.Signee),
#name="select2_djhacker_signee_autocomplete",
#),
#path(
#"contracts/contractees/autocomplete",
#dal.autocomplete.Select2QuerySetView.as_view(model=models.Contractee),
#name="select2_djhacker_contractee_autocomplete",
#),
]
......@@ -48,7 +48,7 @@ def view_contract(request, id: int):
{
"site_url": settings.SITE_URL,
"user": request.user,
"title": contract.identifier,
"title": contract.name,
"description": "", # TODO
"contract": contract,
},
......
......@@ -8,7 +8,7 @@ OIDC_RP_REALM_URL="http://localhost:8080/realms/master/"
OIDC_RP_CLIENT_ID=contracts
OIDC_RP_CLIENT_SECRET=VCn4LVAUc6RGLSup7VaAKsmrKUbWguaP
DEFAULT_COUNTRY="CZ"
DEFAULT_COUNTRY="Česká Republika"
DEFAULT_CONTRACTEE_NAME="Česká pirátská strana"
DEFAULT_CONTRACTEE_STREET="Na Moráni 360/3"
......
......@@ -2,11 +2,11 @@ django==4.1.4
django-admin-interface==0.24.2
django-autocomplete-light==3.9.4
django-database-url==1.0.3
djhacker==0.2.3
psycopg2-binary==2.9.5
django-webpack-loader==1.8.0
nodeenv==1.7.0
pirates==0.6.0
django-colorfield==0.8.0
django-markdownx==4.0.0b1
django-environ==0.9.0
django-http-exceptions==1.4.0
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment