Added new registration system
This commit is contained in:
parent
ae663a1ef9
commit
f3dca67ec4
@ -53,7 +53,8 @@ INSTALLED_APPS = [
|
||||
'forum.apps.ForumConfig',
|
||||
'main.apps.MainConfig',
|
||||
'newsletters.apps.NewslettersConfig',
|
||||
'tinymce'
|
||||
'tinymce',
|
||||
'widget_tweaks'
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
|
@ -1,16 +1,16 @@
|
||||
from django.contrib import admin
|
||||
|
||||
from .models import Chapter, Position, Attorney
|
||||
from .models import Region, Position, Attorney
|
||||
|
||||
|
||||
admin.site.register(Chapter)
|
||||
admin.site.register(Region)
|
||||
admin.site.register(Position)
|
||||
|
||||
|
||||
@admin.register(Attorney)
|
||||
class MemberAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'position', 'chapter', 'phone_formatted', 'email', 'front_page', 'order', 'joined', 'thumbnail']
|
||||
list_filter = ['chapter', 'position', 'front_page', 'joined']
|
||||
search_fields = ['email', 'joined', 'name', 'chapter', 'position', 'website', 'phone', 'phone_formatted']
|
||||
fields = ['image_preview', 'image', 'name', 'position', 'chapter', 'biography', 'phone', 'email', 'website', 'front_page', 'order', 'joined']
|
||||
list_display = ['last_name', 'first_name', 'position', 'region', 'phone_formatted', 'email', 'front_page', 'order', 'joined', 'thumbnail']
|
||||
list_filter = ['region', 'position', 'front_page', 'joined']
|
||||
search_fields = ['email', 'joined', 'last_name', 'first_name', 'region', 'position', 'website', 'phone', 'phone_formatted']
|
||||
fields = ['image_preview', 'image', 'last_name', 'first_name', 'position', 'region', 'biography', 'phone', 'email', 'website', 'front_page', 'order', 'joined']
|
||||
readonly_fields = ['image_preview']
|
||||
|
78
charter_members/forms.py
Normal file
78
charter_members/forms.py
Normal file
@ -0,0 +1,78 @@
|
||||
from django.core import mail
|
||||
from django import forms
|
||||
from django.contrib.auth.models import Group, User
|
||||
|
||||
from tinymce import TinyMCE
|
||||
|
||||
from newsletters.models import Subscriber
|
||||
from .models import Attorney, Position
|
||||
from OACPL import settings
|
||||
from OACPL.utils import render_to_string
|
||||
|
||||
|
||||
class RegisterForm(forms.ModelForm):
|
||||
def email_validator(self):
|
||||
if User.objects.filter(email=self).exists():
|
||||
raise forms.ValidationError('This email is already registered')
|
||||
|
||||
def password_length(self):
|
||||
if len(self) < 8:
|
||||
raise forms.ValidationError('Password must be at least 8 characters long')
|
||||
|
||||
biography = forms.CharField(widget=forms.Textarea, required=False, label='Biography')
|
||||
call_to_bar = forms.CharField(max_length=4, required=False, label='Year of Call to Bar')
|
||||
case_law = forms.BooleanField(initial=True, required=False)
|
||||
email = forms.EmailField(max_length=255, validators=[email_validator])
|
||||
lso = forms.CharField(max_length=20, required=False, label='LSO #')
|
||||
newsletter = forms.BooleanField(initial=True, required=False)
|
||||
request_training = forms.CharField(max_length=255, required=False, label='Request Training For...')
|
||||
password1 = forms.CharField(widget=forms.PasswordInput(), validators=[password_length])
|
||||
password2 = forms.CharField(widget=forms.PasswordInput())
|
||||
provide_training = forms.CharField(max_length=255, required=False, label='Offer Training For...')
|
||||
|
||||
class Meta:
|
||||
model = Attorney
|
||||
fields = ['first_name', 'last_name', 'region', 'password1', 'password2', 'image', 'email', 'address', 'phone', 'website', 'call_to_bar', 'lso', 'biography', 'provide_training', 'request_training', 'case_law', 'newsletter']
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
password = cleaned_data.get('password1')
|
||||
password_confirm = cleaned_data.get('password2')
|
||||
if password != password_confirm:
|
||||
raise forms.ValidationError("The two password fields must match.")
|
||||
return cleaned_data
|
||||
|
||||
def save(self, commit=True):
|
||||
# Create attorney profile
|
||||
user = super().save(commit=False)
|
||||
member = Position.objects.filter(name='Member').first()
|
||||
if member:
|
||||
user.position = member
|
||||
user.save()
|
||||
|
||||
# Add user to default Group
|
||||
default_group = Group.objects.filter(name='default').first()
|
||||
if default_group:
|
||||
default_group[0].user_set.add(user)
|
||||
|
||||
# Send confirmation email
|
||||
mail.send_mail('OACPL Registration', 'You have successfully registered to the Ontario Association of Child Protection Lawyers!', settings.EMAIL_HOST_USER, [user.email], html_message=render_to_string('email.html', {'content': 'You have successfully registered to the Ontario Association of Child Protection Lawyers!', 'name': user.first_name + ' ' + user.last_name, 'base_url': settings.BASE_URL}))
|
||||
|
||||
# Subscribe to newsletters
|
||||
if self.cleaned_data['newsletter'] == 'on' and not Subscriber.objects.filter(email=user.email).exists():
|
||||
Subscriber.objects.create(email=user.email)
|
||||
|
||||
# Send email to register@oacpl.org
|
||||
body = '{} {} ({}) has registered with OACPL. <br><br>'.format(user.first_name, user.last_name, user.email)
|
||||
if self.cleaned_data['case_law'] == 'on':
|
||||
body += 'They have request access to case law. <br><br>'
|
||||
if self.cleaned_data['provide_training']:
|
||||
body += 'They have offered to provide training for: {}. <br><br>'.format(self.cleaned_data['provide_training'])
|
||||
if self.cleaned_data['request_training']:
|
||||
body += 'They have request training for: {}. <br><br>'.format(self.cleaned_data['request_training'])
|
||||
mail.send_mail(user.first_name + ' ' + user.last_name, body.replace('<br>', ' '), settings.EMAIL_HOST_USER, ['register@oacpl.org'], html_message=render_to_string('email.html', {'content': body, 'base_url': settings.BASE_URL}))
|
||||
|
||||
# Create Auth
|
||||
auth = User.objects.create_user(user.email, first_name=user.first_name, last_name=user.last_name, email=user.email, password=self.cleaned_data['password1'])
|
||||
auth.save()
|
||||
return auth
|
@ -1,10 +1,11 @@
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
|
||||
from tinymce import HTMLField
|
||||
|
||||
|
||||
class Chapter(models.Model):
|
||||
class Region(models.Model):
|
||||
name = models.CharField(max_length=50)
|
||||
|
||||
def __str__(self):
|
||||
@ -12,23 +13,27 @@ class Chapter(models.Model):
|
||||
|
||||
|
||||
class Position(models.Model):
|
||||
position_name = models.CharField(max_length=50)
|
||||
name = models.CharField(max_length=50)
|
||||
|
||||
def __str__(self):
|
||||
return self.position_name
|
||||
return self.name
|
||||
|
||||
|
||||
class Attorney(models.Model):
|
||||
address = models.CharField(max_length=255)
|
||||
biography = HTMLField(blank=True, null=True)
|
||||
chapter = models.ForeignKey(Chapter, blank=True, null=True)
|
||||
email = models.CharField(max_length=255, blank=True, null=True)
|
||||
call_to_bar = models.CharField(max_length=4, blank=True, null=True)
|
||||
region = models.ForeignKey(Region, blank=True, null=True)
|
||||
email = models.CharField(max_length=255)
|
||||
first_name = models.CharField(max_length=100)
|
||||
front_page = models.BooleanField(default=False)
|
||||
image = models.ImageField(upload_to='portraits', default='portraits/silhouette.png')
|
||||
joined = models.DateField(blank=True, null=True)
|
||||
name = models.CharField(max_length=100)
|
||||
joined = models.DateField(default=timezone.now)
|
||||
last_name = models.CharField(max_length=100)
|
||||
lso = models.CharField(max_length=20, blank= True, null=True)
|
||||
order = models.IntegerField(blank=True, null=True, verbose_name='Order On Front Page')
|
||||
phone = models.CharField(max_length=10, blank=True, null=True)
|
||||
position = models.ForeignKey(Position)
|
||||
phone = models.CharField(max_length=10)
|
||||
position = models.ForeignKey(Position, blank=True, null=True)
|
||||
website = models.CharField(max_length=255, blank=True, null=True)
|
||||
|
||||
def phone_formatted(self):
|
||||
@ -47,4 +52,4 @@ class Attorney(models.Model):
|
||||
image_preview.allow_tags = True
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
return self.first_name + ' ' + self.last_name
|
||||
|
@ -3,19 +3,19 @@
|
||||
{% block body %}
|
||||
<div class="container-fluid bg-dark-primary py-3">
|
||||
<div class="container">
|
||||
{% for chapter in chapters %}
|
||||
{% for region in region %}
|
||||
<div class="row bg-white mb-5">
|
||||
<div class="col-12 pt-3">
|
||||
<h2 class="text-dark-primary">{{ chapter }}</h2>
|
||||
<h2 class="text-dark-primary">{{ region }}</h2>
|
||||
<hr style="border-top: 1px solid rgba(0,0,0,0.2) !important">
|
||||
<div class="row">
|
||||
{% for attorney in attorneys %}
|
||||
{% if attorney.chapter == chapter %}
|
||||
{% if attorney.region == region %}
|
||||
<a class="col-3 text-center pt-5" href="{% url 'attorney' attorney.id %}">
|
||||
<div>
|
||||
<img class="pb-3" src="/media/{{ attorney.image }}"
|
||||
style="width: auto; max-width: 100%; height: 200px"/>
|
||||
<h5 class="text-dark-primary">{{ attorney.name }}</h5>
|
||||
<h5 class="text-dark-primary">{{ attorney.first_name }} {{ attorney.last_name }}</h5>
|
||||
<span class="text-light-primary">{{ attorney.position }}</span>
|
||||
<hr>
|
||||
</div>
|
||||
|
@ -4,7 +4,7 @@
|
||||
<div class="container-fluid">
|
||||
<div class="row bg-dark-primary">
|
||||
<div class="col-lg-3 ml-auto">
|
||||
<h2 class="d-lg-none text-white mt-3">{{ attorney.name }}</h2>
|
||||
<h2 class="d-lg-none text-white mt-3">{{ attorney.first_name }} {{ attorney.last_name }}</h2>
|
||||
<div class="p-2 my-3 shadow bg-white">
|
||||
<img class="d-block mx-auto" src="/media/{{ attorney.image }}" style="max-width: 100%"/>
|
||||
</div>
|
||||
@ -14,7 +14,7 @@
|
||||
</div>
|
||||
<div class="col-lg-6 pt-3 bg-light-blue">
|
||||
<div class="col-lg-6">
|
||||
<h2 class="d-none d-lg-inline text-dark-primary">{{ attorney.name }}</h2>
|
||||
<h2 class="d-none d-lg-inline text-dark-primary">{{ attorney.first_name }} {{ attorney.last_name }}</h2>
|
||||
<p>{{ attorney.biography | safe }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,7 +1,7 @@
|
||||
from django.db.models import Count
|
||||
from django.shortcuts import render
|
||||
|
||||
from .models import Chapter, Attorney
|
||||
from .models import Region, Attorney
|
||||
|
||||
|
||||
def index(request, id):
|
||||
@ -10,6 +10,6 @@ def index(request, id):
|
||||
|
||||
|
||||
def all(request):
|
||||
chapters = Chapter.objects.all().order_by('name')
|
||||
attorneys = Attorney.objects.all().annotate(Count('chapter'))
|
||||
return render(request, 'all.html', {'chapters': chapters, 'attorneys': attorneys})
|
||||
region = Region.objects.all().order_by('name')
|
||||
attorneys = Attorney.objects.all().annotate(Count('region'))
|
||||
return render(request, 'all.html', {'region': region, 'attorneys': attorneys})
|
||||
|
@ -141,7 +141,7 @@
|
||||
<a class="col-4 text-center pt-5" href="{% url 'attorney' attorney.id %}">
|
||||
<div>
|
||||
<img class="pb-3" src="/media/{{ attorney.image }}" style="width: auto; max-width: 100%; height: 200px"/>
|
||||
<h5 class="text-dark-primary">{{ attorney.name }}</h5>
|
||||
<h5 class="text-dark-primary">{{ attorney.first_name }} {{ attorney.last_name }}</h5>
|
||||
<span class="text-light-primary">{{ attorney.position }}</span>
|
||||
<hr>
|
||||
</div>
|
||||
|
@ -1,5 +1,6 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
{% load widget_tweaks %}
|
||||
|
||||
{% block head %}
|
||||
<style>
|
||||
@ -19,20 +20,20 @@
|
||||
</style>
|
||||
|
||||
<script>
|
||||
$(function() {
|
||||
$('#register-mode').click(function() {
|
||||
$(function () {
|
||||
$('#register-mode').click(function () {
|
||||
$('#login-form').collapse('hide');
|
||||
$('#register-form').collapse('show');
|
||||
$('#reset-form').collapse('hide');
|
||||
});
|
||||
|
||||
$('#login-mode').click(function() {
|
||||
$('#login-mode').click(function () {
|
||||
$('#login-form').collapse('show');
|
||||
$('#register-form').collapse('hide');
|
||||
$('#reset-form').collapse('hide');
|
||||
});
|
||||
|
||||
$('#forgot-password').click(function() {
|
||||
$('#forgot-password').click(function () {
|
||||
$('#login-form').collapse('hide');
|
||||
$('#register-form').collapse('hide');
|
||||
$('#reset-form').collapse('show');
|
||||
@ -42,12 +43,18 @@
|
||||
$('#login-form input[name="username"]').css('border-color', '#ff0000').effect('shake');
|
||||
$('#login-form input[name="password"]').css('border-color', '#ff0000').effect('shake');
|
||||
{% endif %}
|
||||
|
||||
{% if register.errors %}
|
||||
$('#login-form').collapse('hide');
|
||||
$('#register-form').collapse('show');
|
||||
$('#reset-form').collapse('hide');
|
||||
{% endif %}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="col-sm-11 col-md-6 col-lg-4 col-xl-2 mx-auto mt-5 p-5 login">
|
||||
<div class="col-11 col-md-8 col-lg-6 col-xl-4 mx-auto mt-5 p-5 login">
|
||||
<div class="form">
|
||||
<div class="my-4">
|
||||
<img src="{% static 'main/img/logo.png' %}" height="30px" width="auto">
|
||||
@ -62,32 +69,96 @@
|
||||
<form id="login-form" class="collapse show" method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="request" value="login">
|
||||
<input type="text" class="form-control mb-2 p-2" name="username" placeholder="Username"/>
|
||||
<input type="text" class="form-control mb-2 p-2" name="username" placeholder="Email"/>
|
||||
<input type="password" class="form-control mb-2 p-2" name="password" placeholder="Password"/>
|
||||
<button class="btn btn-primary col-12 mb-2 p-2">login</button>
|
||||
<p class="d-inline-block text-muted curs-pointer"><a id="register-mode">Register</a></p>
|
||||
{% if failed %}
|
||||
<p class="d-inline-block text-muted curs-pointer float-right"><a id="forgot-password">Forgot Password</a></p>
|
||||
<p class="d-inline-block text-muted curs-pointer float-right"><a id="forgot-password">Forgot
|
||||
Password</a></p>
|
||||
{% endif %}
|
||||
</form>
|
||||
|
||||
<form id="register-form" class="collapse" method="post">
|
||||
<form id="register-form" class="collapse" method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="request" value="register">
|
||||
<input type="text" class="form-control mb-2 p-2" name="username" placeholder="Username"/>
|
||||
<input type="text" class="form-control mb-2 p-2" name="email" placeholder="Email"/>
|
||||
<input type="password" class="form-control mb-2 p-2" name="password" placeholder="Password"/>
|
||||
<input type="password" class="form-control mb-2 p-2" placeholder="Confirm Password"/>
|
||||
<div class="col-12 px-0">
|
||||
<input id="newsletter" type="checkbox" class="mb-2 curs-pointer" name="newsletter">
|
||||
<label for="newsletter" class="curs-pointer">Newsletter</label>
|
||||
{% if register.errors %}
|
||||
<div class="alert alert-danger" role="alert">
|
||||
{% for key, value in register.errors.items %}
|
||||
{{ value }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<h3>General Information</h3>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-6">
|
||||
{{ register.first_name|add_class:"form-control"|attr:"placeholder:First Name" }}
|
||||
</div>
|
||||
<div class="form-group col-6">
|
||||
{{ register.last_name|add_class:"form-control"|attr:"placeholder:Last Name" }}
|
||||
</div>
|
||||
<div class="form-group col-6">
|
||||
{{ register.password1|add_class:"form-control"|attr:"placeholder:Password" }}
|
||||
</div>
|
||||
<div class="form-group col-6">
|
||||
{{ register.password2|add_class:"form-control"|attr:"placeholder:Confirm Password" }}
|
||||
</div>
|
||||
<div class="form-group col-6">
|
||||
{{ register.phone|add_class:"form-control"|attr:"placeholder:Phone Number" }}
|
||||
</div>
|
||||
<div class="form-group col-6">
|
||||
{{ register.email|add_class:"form-control"|attr:"placeholder:Email" }}
|
||||
</div>
|
||||
<div class="form-group col-8">
|
||||
{{ register.address|add_class:"form-control"|attr:"placeholder:Address" }}
|
||||
</div>
|
||||
<div class="form-group col-4">
|
||||
{{ register.region|add_class:"form-control"}}
|
||||
</div>
|
||||
<div class="form-group col-12">
|
||||
{{ register.website|add_class:"form-control"|attr:"placeholder:Website" }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 px-0">
|
||||
<input id="caselaw" type="checkbox" class="mb-2 curs-pointer" name="caselaw">
|
||||
<label for="caselaw" class="curs-pointer">Case Law Access</label>
|
||||
<h3>Attorney Information</h3>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-6">
|
||||
{{ register.call_to_bar|add_class:"form-control"|attr:"placeholder:Year of Call to Bar" }}
|
||||
</div>
|
||||
<div class="form-group col-6">
|
||||
{{ register.lso|add_class:"form-control"|attr:"placeholder:LSO #" }}
|
||||
</div>
|
||||
<div class="form-group col-12">
|
||||
Portrait
|
||||
{{ register.image|add_class:"form-control" }}
|
||||
</div>
|
||||
<div class="form-group col-12">
|
||||
{{ register.biography|add_class:"form-control"|attr:"placeholder:Biography" }}
|
||||
</div>
|
||||
</div>
|
||||
<h3>Other</h3>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-12">
|
||||
<div class="form-check">
|
||||
{{ register.newsletter|add_class:"form-check-input" }} <label class="form-check-label">Receive
|
||||
newsletters</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-12">
|
||||
<div class="form-check">
|
||||
{{ register.case_law|add_class:"form-check-input" }} <label class="form-check-label">Request
|
||||
access to case law database</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-12">
|
||||
{{ register.provide_training|add_class:"form-control"|attr:"placeholder:Training I am willing to provide..." }}
|
||||
</div>
|
||||
<div class="form-group col-12">
|
||||
{{ register.request_training|add_class:"form-control"|attr:"placeholder:Training I would like to recieve..." }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary col-12 mb-2 p-2">Register</button>
|
||||
<p class="text-muted curs-pointer"><a id="login-mode">Login</a></p>
|
||||
<p class="d-inline-block text-muted curs-pointer"><a id="login-mode">Login</a></p>
|
||||
</form>
|
||||
|
||||
<form id="reset-form" class="collapse" method="post">
|
||||
|
@ -1,14 +1,11 @@
|
||||
from django.http import JsonResponse
|
||||
from django.contrib import auth
|
||||
from django.contrib.auth.models import Group, User, Permission
|
||||
from django.core import mail
|
||||
from django.db.models import Q
|
||||
from django.shortcuts import render, redirect
|
||||
from django.template.loader import render_to_string
|
||||
|
||||
from charter_members.forms import RegisterForm
|
||||
from OACPL.utils import url_fix_render_to_string
|
||||
from charter_members.models import Attorney
|
||||
from newsletters.models import Subscriber
|
||||
from OACPL import settings
|
||||
|
||||
|
||||
@ -33,33 +30,23 @@ def contact(request):
|
||||
|
||||
def login(request):
|
||||
if request.method == 'POST':
|
||||
if request.POST.get('request') == 'login':
|
||||
if request.POST.get('request') == 'register':
|
||||
register_form = RegisterForm(request.POST, request.FILES)
|
||||
if register_form.is_valid():
|
||||
user = register_form.save()
|
||||
auth.login(request, user)
|
||||
return redirect('/')
|
||||
elif request.POST.get('request') == 'login':
|
||||
user = auth.authenticate(request, username=request.POST.get('username'), password=request.POST.get('password'))
|
||||
if user:
|
||||
auth.login(request, user)
|
||||
return redirect('/')
|
||||
else:
|
||||
return render(request, 'login.html', {'navbar': False, 'footer': False, 'failed': True})
|
||||
elif request.POST.get('request') == 'register':
|
||||
user = User.objects.create_user(request.POST.get('username'), email=request.POST.get('email'), password=request.POST.get('password'))
|
||||
user.save()
|
||||
|
||||
default_group = Group.objects.filter(name='default')
|
||||
if default_group:
|
||||
default_group[0].user_set.add(user)
|
||||
|
||||
if settings.EMAIL_HOST:
|
||||
mail.send_mail('OACPL Registration', 'You have successfully registered to the Ontario Association of Child Protection Lawyers!', settings.EMAIL_HOST_USER, [request.POST.get('email')], html_message=render_to_string('email.html', {'content': 'You have successfully registered to the Ontario Association of Child Protection Lawyers!', 'name': user.username, 'base_url': settings.BASE_URL}))
|
||||
if request.POST.get('newsletter'):
|
||||
Subscriber.objects.create(email=request.POST.get('email'))
|
||||
if request.POST.get('caselaw'):
|
||||
perm = Permission.objects.get(codename='change_user')
|
||||
admins = User.objects.filter(Q(groups__permissions=perm) | Q(user_permissions=perm) | Q(is_superuser=True)).distinct().values_list('email', flat=True)
|
||||
mail.send_mail('OACPL Case Law Request', '{} {} ({}) has requested access to case law'.format(user.first_name, user.last_name, user.email), settings.EMAIL_HOST_USER, admins, html_message=render_to_string('email.html', {'content': '{} {} ({}) has requested access to case law'.format(user.first_name, user.last_name, user.email), 'base_url': settings.BASE_URL}))
|
||||
auth.login(request, user)
|
||||
return redirect('/')
|
||||
else:
|
||||
return render(request, 'login.html', {'navbar': False, 'footer': False})
|
||||
if 'register_form' not in vars():
|
||||
register_form = RegisterForm()
|
||||
return render(request, 'login.html', {'navbar': False, 'footer': False, 'register': register_form})
|
||||
|
||||
|
||||
def logout(request):
|
||||
|
@ -1,7 +1,7 @@
|
||||
bootstrap-admin==0.3.7.1
|
||||
Django==1.11.5
|
||||
django-forms-bootstrap==3.1.0
|
||||
django-tinymce4-lite==1.7.0
|
||||
django-widget-tweaks==1.4.1
|
||||
mysqlclient==1.3.12
|
||||
Pillow==4.2.1
|
||||
requests==2.11.1
|
||||
|
Loading…
Reference in New Issue
Block a user