Compare commits

..

11 Commits

Author SHA1 Message Date
ynerant 1735ba25a8 Merge branch 'beta-soon' into 'master'
Beta soon

See merge request bde/nk20!85
2020-07-21 22:47:50 +02:00
Yohann D'ANELLO 5d70a809c2 🔧 Better Ansible script 2020-07-21 22:36:37 +02:00
Yohann D'ANELLO 4761d46696 🎨 Apply Django migrations 2020-07-15 11:32:08 +02:00
Yohann D'ANELLO 084d22d33f Install PSQL and init DB 2020-07-15 10:09:28 +02:00
Yohann D'ANELLO 3f0208a664 🐛 First fix Ansible installation 2020-07-15 09:27:11 +02:00
Yohann D'ANELLO 3dfed70eb1 💩 Use HTTPS rather than SSH to clone nk20-scripts (may be reverted later) 2020-07-15 08:25:52 +02:00
Yohann D'ANELLO cdc053718f 🚀 Adding Ansible configuration (not tested) 2020-07-15 07:46:42 +02:00
Yohann D'ANELLO 71f6daf0e8 Add permission for treasurers to update the validation status of a transaction 2020-07-13 12:10:01 +02:00
Yohann D'ANELLO 2c7995a79e A transaction can only be created between active notes 2020-06-21 22:47:05 +02:00
Yohann D'ANELLO ac5041f3ec Better club search bar 2020-06-21 22:27:32 +02:00
Yohann D'ANELLO b46854e479 Rework on Docker image 2020-06-21 20:27:42 +02:00
26 changed files with 353 additions and 68 deletions

3
.dockerignore Normal file
View File

@ -0,0 +1,3 @@
__pycache__
media
db.sqlite3

View File

@ -1,4 +1,4 @@
DJANGO_APP_STAGE=dev DJANGO_APP_STAGE=prod
# Only used in dev mode, change to "postgresql" if you want to use PostgreSQL in dev # Only used in dev mode, change to "postgresql" if you want to use PostgreSQL in dev
DJANGO_DEV_STORE_METHOD=sqllite DJANGO_DEV_STORE_METHOD=sqllite
DJANGO_DB_HOST=localhost DJANGO_DB_HOST=localhost
@ -13,6 +13,6 @@ NOTE_URL=localhost
# Config for mails. Only used in production # Config for mails. Only used in production
NOTE_MAIL=notekfet@localhost NOTE_MAIL=notekfet@localhost
EMAIL_HOST=smtp.localhost EMAIL_HOST=smtp.localhost
EMAIL_PORT=443 EMAIL_PORT=465
EMAIL_USER=notekfet@localhost EMAIL_USER=notekfet@localhost
EMAIL_PASSWORD=CHANGE_ME EMAIL_PASSWORD=CHANGE_ME

2
.gitmodules vendored
View File

@ -1,3 +1,3 @@
[submodule "apps/scripts"] [submodule "apps/scripts"]
path = apps/scripts path = apps/scripts
url = git@gitlab.crans.org:bde/nk20-scripts.git url = https://gitlab.crans.org/bde/nk20-scripts.git

View File

@ -1,25 +1,27 @@
FROM python:3-buster FROM python:3-alpine
ENV PYTHONUNBUFFERED 1 ENV PYTHONUNBUFFERED 1
# Install LaTeX requirements
RUN apk add --no-cache gettext texlive texmf-dist-latexextra texmf-dist-fontsextra nginx gcc libc-dev libffi-dev postgresql-dev libxml2-dev libxslt-dev jpeg-dev
RUN apk add --no-cache bash
RUN mkdir /code RUN mkdir /code
WORKDIR /code WORKDIR /code
COPY requirements /code/requirements
RUN apt update && \ RUN pip install gunicorn ptpython --no-cache-dir
apt install -y gettext nginx uwsgi uwsgi-plugin-python3 && \ RUN pip install -r requirements/base.txt -r requirements/cas.txt -r requirements/production.txt --no-cache-dir
rm -rf /var/lib/apt/lists/*
# Install LaTeX requirements
RUN apt update && \
apt install -y texlive-latex-extra texlive-fonts-extra texlive-lang-french && \
rm -rf /var/lib/apt/lists/*
COPY . /code/ COPY . /code/
# Comment what is not needed # Configure nginx
RUN pip install -r requirements/base.txt RUN mkdir /run/nginx
RUN pip install -r requirements/cas.txt RUN ln -sf /dev/stdout /var/log/nginx/access.log && ln -sf /dev/stderr /var/log/nginx/error.log
RUN pip install -r requirements/production.txt RUN ln -sf /code/nginx_note.conf_docker /etc/nginx/conf.d/nginx_note.conf
RUN rm /etc/nginx/conf.d/default.conf
ENTRYPOINT ["/code/entrypoint.sh"] ENTRYPOINT ["/code/entrypoint.sh"]
EXPOSE 8000 EXPOSE 80
CMD ["./manage.py", "shell_plus", "--ptpython"]

View File

@ -121,7 +121,7 @@ On supposera pour la suite que vous utilisez Debian/Ubuntu sur un serveur tout n
# Le reste n'est utile qu'en production, pour configurer l'envoi des mails # Le reste n'est utile qu'en production, pour configurer l'envoi des mails
NOTE_MAIL=notekfet@localhost NOTE_MAIL=notekfet@localhost
EMAIL_HOST=smtp.localhost EMAIL_HOST=smtp.localhost
EMAIL_PORT=443 EMAIL_PORT=465
EMAIL_USER=notekfet@localhost EMAIL_USER=notekfet@localhost
EMAIL_PASSWORD=CHANGE_ME EMAIL_PASSWORD=CHANGE_ME

14
ansible/ansible.cfg Normal file
View File

@ -0,0 +1,14 @@
[defaults]
inventory = ./hosts
timeout = 42
[privilege_escalation]
become = True
become_ask_pass = True
[ssh_connection]
pipelining = True
retries = 3
[diff]
always = yes

12
ansible/base.yml Executable file
View File

@ -0,0 +1,12 @@
#!/usr/bin/env ansible-playbook
---
- hosts: bde-nk20-beta.adh.crans.org
roles:
- 1-apt-basic
- 2-nk20
- 3-pip
- 4-nginx
- 5-certbot
- 6-psql
- 7-postinstall

5
ansible/hosts Normal file
View File

@ -0,0 +1,5 @@
[server]
bde-nk20-beta.adh.crans.org
[all:vars]
ansible_python_interpreter=/usr/bin/python3

View File

@ -0,0 +1,21 @@
---
- name: Install basic APT packages
apt:
update_cache: true
name:
- nginx
- python3
- python3-pip
- python3-dev
- uwsgi
- uwsgi-plugin-python3
- python3-venv
- git
- acl
- gettext
- texlive-latex-extra
- texlive-fonts-extra
- texlive-lang-french
register: pkg_result
retries: 3
until: pkg_result is succeeded

View File

@ -0,0 +1,26 @@
---
- name: Create note_kfet dir with good permissions
file:
path: /var/www/note_kfet
state: directory
owner: www-data
group: www-data
mode: u=rwx,g=rwxs,o=rx
- name: Clone Note Kfet
git:
repo: https://gitlab.crans.org/bde/nk20.git
dest: /var/www/note_kfet
version: beta-soon
force: true
- name: Use default env vars (should be updated!)
command: cp /var/www/note_kfet/.env_example /var/www/note_kfet/.env
- name: Update permissions for note_kfet dir
file:
path: /var/www/note_kfet
state: directory
recurse: yes
owner: www-data
group: www-data

View File

@ -0,0 +1,14 @@
---
- name: Install PIP basic dependencies
pip:
requirements: /var/www/note_kfet/requirements/base.txt
virtualenv: /var/www/note_kfet/env
virtualenv_command: /usr/bin/python3 -m venv
become_user: www-data
- name: Install PIP production dependencies
pip:
requirements: /var/www/note_kfet/requirements/production.txt
virtualenv: /var/www/note_kfet/env
virtualenv_command: /usr/bin/python3 -m venv
become_user: www-data

View File

@ -0,0 +1,32 @@
---
- name: Copy conf of Nginx
template:
src: "nginx_note.conf"
dest: /etc/nginx/sites-available/nginx_note.conf
mode: 0644
owner: www-data
group: www-data
- name: Enable Nginx site
file:
src: /etc/nginx/sites-available/nginx_note.conf
dest: /etc/nginx/sites-enabled/nginx_note.conf
owner: www-data
group: www-data
state: link
- name: Copy conf of UWSGI
file:
src: /var/www/note_kfet/uwsgi_note.ini
dest: /etc/uwsgi/apps-enabled/uwsgi_note.ini
state: link
- name: Reload Nginx
systemd:
name: nginx
state: reloaded
- name: Restart UWSGI
systemd:
name: uwsgi
state: restarted

View File

@ -0,0 +1,21 @@
---
- name: Install basic APT packages
apt:
update_cache: true
name:
- certbot
- python3-certbot-nginx
register: pkg_result
retries: 3
until: pkg_result is succeeded
- name: Create /etc/letsencrypt/conf.d
file:
path: /etc/letsencrypt/conf.d
state: directory
- name: Add Certbot configuration
template:
src: "letsencrypt/conf.d/nk20.ini.j2"
dest: "/etc/letsencrypt/conf.d/nk20.ini"
mode: 0644

View File

@ -0,0 +1,20 @@
{{ ansible_managed | comment }}
# To generate the certificate, please use the following command
# certbot --config /etc/letsencrypt/conf.d/nk20.ini certonly
# Use a 4096 bit RSA key instead of 2048
rsa-key-size = 4096
# Always use the staging/testing server
# server = https://acme-staging.api.letsencrypt.org/directory
# Uncomment and update to register with the specified e-mail address
email = notekfet2020@lists.crans.org
# Uncomment to use a text interface instead of ncurses
text = True
# Use DNS-01 challenge
authenticator = nginx

View File

@ -0,0 +1,27 @@
---
- name: Install PostgreSQL APT packages
apt:
update_cache: true
name:
- postgresql
- postgresql-contrib
- libpq-dev
register: pkg_result
retries: 3
until: pkg_result is succeeded
- name: Install Psycopg2
pip:
name: psycopg2-binary
- name: Create role note
postgresql_user:
name: note
password: "CHANGE_ME"
become_user: postgres
- name: Create NK20 database
postgresql_db:
name: note_db
owner: note
become_user: postgres

View File

@ -0,0 +1,24 @@
---
- name: Make Django migrations
command: /var/www/note_kfet/env/bin/python manage.py makemigrations
args:
chdir: /var/www/note_kfet
become_user: www-data
- name: Migrate Django database
command: /var/www/note_kfet/env/bin/python manage.py migrate
args:
chdir: /var/www/note_kfet
become_user: www-data
- name: Compile messages
command: /var/www/note_kfet/env/bin/python manage.py compilemessages
args:
chdir: /var/www/note_kfet
become_user: www-data
- name: Install initial fixtures
command: /var/www/note_kfet/env/bin/python manage.py loaddata initial
args:
chdir: /var/www/note_kfet
become_user: www-data

View File

@ -25,7 +25,8 @@ class ClubTable(tables.Table):
order_by = ('id',) order_by = ('id',)
model = Club model = Club
template_name = 'django_tables2/bootstrap4.html' template_name = 'django_tables2/bootstrap4.html'
fields = ('id', 'name', 'email') fields = ('name', 'email',)
order_by = ('name',)
row_attrs = { row_attrs = {
'class': 'table-row', 'class': 'table-row',
'id': lambda record: "row-" + str(record.pk), 'id': lambda record: "row-" + str(record.pk),

View File

@ -299,6 +299,22 @@ class ClubListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
model = Club model = Club
table_class = ClubTable table_class = ClubTable
def get_queryset(self, **kwargs):
"""
Filter the user list with the given pattern.
"""
qs = super().get_queryset().filter()
if "search" in self.request.GET:
pattern = self.request.GET["search"]
qs = qs.filter(
Q(name__iregex=pattern)
| Q(note__alias__name__iregex="^" + pattern)
| Q(note__alias__normalized_name__iregex=Alias.normalize("^" + pattern))
)
return qs
class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
""" """

View File

@ -163,6 +163,12 @@ class Transaction(PolymorphicModel):
When saving, also transfer money between two notes When saving, also transfer money between two notes
""" """
if not self.source.is_active or not self.destination.is_active:
if 'force_insert' not in kwargs or not kwargs['force_insert']:
if 'force_update' not in kwargs or not kwargs['force_update']:
raise ValidationError(_("The transaction can't be saved since the source note "
"or the destination note is not active."))
# If the aliases are not entered, we assume that the used alias is the name of the note # If the aliases are not entered, we assume that the used alias is the name of the note
if not self.source_alias: if not self.source_alias:
self.source_alias = str(self.source) self.source_alias = str(self.source)
@ -171,7 +177,7 @@ class Transaction(PolymorphicModel):
self.destination_alias = str(self.destination) self.destination_alias = str(self.destination)
if self.source.pk == self.destination.pk: if self.source.pk == self.destination.pk:
# When source == destination, no money is transfered # When source == destination, no money is transferred
super().save(*args, **kwargs) super().save(*args, **kwargs)
return return

View File

@ -121,7 +121,7 @@ class ConsoView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
table_class = HistoryTable table_class = HistoryTable
def get_queryset(self, **kwargs): def get_queryset(self, **kwargs):
return super().get_queryset(**kwargs).order_by("-created_at", "-id").all()[:20] return super().get_queryset(**kwargs).order_by("-created_at", "-id")[:20]
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
""" """

View File

@ -272,7 +272,7 @@
"note", "note",
"alias" "alias"
], ],
"query": "[\"OR\", {\"note__in\": [\"NoteUser\", \"objects\", [\"filter\", {\"user__memberships__club__name\": \"Kfet\"}], [\"all\"]]}, {\"note__in\": [\"NoteClub\", \"objects\", [\"all\"]]}]", "query": "[\"AND\", [\"OR\", {\"note__in\": [\"NoteUser\", \"objects\", [\"filter\", {\"user__memberships__club__name\": \"Kfet\"}], [\"all\"]]}, {\"note__in\": [\"NoteClub\", \"objects\", [\"all\"]]}], {\"note__is_active\": true}]",
"type": "view", "type": "view",
"mask": 1, "mask": 1,
"field": "", "field": "",
@ -2200,6 +2200,22 @@
"description": "View my past activities" "description": "View my past activities"
} }
}, },
{
"model": "permission.permission",
"pk": 127,
"fields": {
"model": [
"note",
"transaction"
],
"query": "[\"AND\", [\"OR\", {\"source\": [\"club\", \"note\"]}, {\"destination\": [\"club\", \"note\"]}], [\"OR\", {\"amount__lte\": {\"F\": [\"ADD\", [\"F\", \"source__balance\"], 5000]}}, {\"valid\": true}]]",
"type": "change",
"mask": 1,
"field": "valid",
"permanent": false,
"description": "Update validation status of a club transaction if possible"
}
},
{ {
"model": "permission.rolepermissions", "model": "permission.rolepermissions",
"pk": 1, "pk": 1,
@ -2287,7 +2303,8 @@
27, 27,
60, 60,
61, 61,
62 62,
127
] ]
} }
}, },

View File

@ -1,4 +1,4 @@
#!/bin/bash #!/bin/sh
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
@ -15,4 +15,10 @@ python manage.py compilemessages
python manage.py makemigrations python manage.py makemigrations
python manage.py migrate python manage.py migrate
python manage.py runserver 0.0.0.0:8000 nginx
if [ "$DJANGO_APP_STAGE" = "prod" ]; then
gunicorn -b 0.0.0.0:8000 --workers=2 --threads=4 --worker-class=gthread note_kfet.wsgi --access-logfile '-' --error-logfile '-';
else
python manage.py runserver 0.0.0.0:8000;
fi

23
nginx_note.conf_docker Normal file
View File

@ -0,0 +1,23 @@
upstream note {
server 127.0.0.1:8000;
}
server {
listen 80;
server_name note;
location / {
proxy_pass http://note;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_redirect off;
}
location /static {
alias /code/static/;
}
location /media {
alias /code/media/;
}
}

View File

@ -50,8 +50,10 @@ class SessionMiddleware(object):
def __call__(self, request): def __call__(self, request):
user = request.user user = request.user
if 'HTTP_X_FORWARDED_FOR' in request.META: if 'HTTP_X_REAL_IP' in request.META:
ip = request.META.get('HTTP_X_FORWARDED_FOR') ip = request.META.get('HTTP_X_REAL_IP')
elif 'HTTP_X_FORWARDED_FOR' in request.META:
ip = request.META.get('HTTP_X_FORWARDED_FOR').split(', ')[0]
else: else:
ip = request.META.get('REMOTE_ADDR') ip = request.META.get('REMOTE_ADDR')

View File

@ -7,7 +7,7 @@
<h4> <h4>
{% trans "search clubs" %} {% trans "search clubs" %}
</h4> </h4>
<input class="form-control mx-auto w-25" type="text" onkeyup="search_field_moved();return(false);" id="search_field"/> <input class="form-control mx-auto w-25" type="text" id="search_field"/>
<hr> <hr>
<a class="btn btn-primary text-center my-4" href="{% url 'member:club_create' %}">{% trans "Create club" %}</a> <a class="btn btn-primary text-center my-4" href="{% url 'member:club_create' %}">{% trans "Create club" %}</a>
</div> </div>
@ -28,43 +28,32 @@
{% endblock %} {% endblock %}
{% block extrajavascript %} {% block extrajavascript %}
<script type="text/javascript"> <script type="text/javascript">
$(document).ready(function() {
let old_pattern = null;
let searchbar_obj = $("#search_field");
var timer_on = false;
var timer;
function getInfo() { function reloadTable() {
var asked = $("#search_field").val(); let pattern = searchbar_obj.val();
/* on ne fait la requête que si on a au moins un caractère pour chercher */ $("#club_table").load(location.href + "?search=" + pattern.replace(" ", "%20") + " #club_table", init);
var sel = $(".table-row");
if (asked.length >= 1) {
$.getJSON("/api/members/club/?format=json&search="+asked, function(buttons){
let selected_id = buttons.results.map((a => "#row-"+a.id));
$(".table-row,"+selected_id.join()).show();
$(".table-row").not(selected_id.join()).hide();
});
}else{
// show everything
$('table tr').show();
} }
}
var timer; searchbar_obj.keyup(function() {
var timer_on; if (timer_on)
/* Fontion appelée quand le texte change (délenche le timer) */
function search_field_moved(secondfield) {
if (timer_on) { // Si le timer a déjà été lancé, on réinitialise le compteur.
clearTimeout(timer); clearTimeout(timer);
timer = setTimeout("getInfo(" + secondfield + ")", 300);
}
else { // Sinon, on le lance et on enregistre le fait qu'il tourne.
timer = setTimeout("getInfo(" + secondfield + ")", 300);
timer_on = true; timer_on = true;
} setTimeout(reloadTable, 0);
} });
// clickable row function init() {
$(document).ready(function($) {
$(".table-row").click(function() { $(".table-row").click(function() {
window.document.location = $(this).data("href"); window.document.location = $(this).data("href");
timer_on = false;
}); });
}); }
init();
});
</script> </script>
{% endblock %} {% endblock %}

View File

@ -25,6 +25,8 @@
$(document).ready(function() { $(document).ready(function() {
let old_pattern = null; let old_pattern = null;
let searchbar_obj = $("#searchbar"); let searchbar_obj = $("#searchbar");
var timer_on = false;
var timer;
function reloadTable() { function reloadTable() {
let pattern = searchbar_obj.val(); let pattern = searchbar_obj.val();
@ -33,17 +35,19 @@
return; return;
$("#user_table").load(location.href + "?search=" + pattern.replace(" ", "%20") + " #user_table", init); $("#user_table").load(location.href + "?search=" + pattern.replace(" ", "%20") + " #user_table", init);
$(".table-row").click(function() {
window.document.location = $(this).data("href");
});
} }
searchbar_obj.keyup(reloadTable); searchbar_obj.keyup(function() {
if (timer_on)
clearTimeout(timer);
timer_on = true;
setTimeout(reloadTable, 0);
});
function init() { function init() {
$(".table-row").click(function() { $(".table-row").click(function() {
window.document.location = $(this).data("href"); window.document.location = $(this).data("href");
timer_on = false;
}); });
} }