diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..efc2616a --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +__pycache__ +media +db.sqlite3 diff --git a/.env_example b/.env_example index 0bf47c8a..91db0e31 100644 --- a/.env_example +++ b/.env_example @@ -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 DJANGO_DEV_STORE_METHOD=sqllite DJANGO_DB_HOST=localhost @@ -13,6 +13,6 @@ NOTE_URL=localhost # Config for mails. Only used in production NOTE_MAIL=notekfet@localhost EMAIL_HOST=smtp.localhost -EMAIL_PORT=443 +EMAIL_PORT=465 EMAIL_USER=notekfet@localhost EMAIL_PASSWORD=CHANGE_ME diff --git a/.gitmodules b/.gitmodules index 94cf1be6..925f7178 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "apps/scripts"] path = apps/scripts - url = git@gitlab.crans.org:bde/nk20-scripts.git + url = https://gitlab.crans.org/bde/nk20-scripts.git diff --git a/Dockerfile b/Dockerfile index 80f2c773..60acc6e3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,25 +1,27 @@ -FROM python:3-buster +FROM python:3-alpine 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 WORKDIR /code - -RUN apt update && \ - apt install -y gettext nginx uwsgi uwsgi-plugin-python3 && \ - 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 requirements /code/requirements +RUN pip install gunicorn ptpython --no-cache-dir +RUN pip install -r requirements/base.txt -r requirements/cas.txt -r requirements/production.txt --no-cache-dir COPY . /code/ -# Comment what is not needed -RUN pip install -r requirements/base.txt -RUN pip install -r requirements/cas.txt -RUN pip install -r requirements/production.txt +# Configure nginx +RUN mkdir /run/nginx +RUN ln -sf /dev/stdout /var/log/nginx/access.log && ln -sf /dev/stderr /var/log/nginx/error.log +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"] -EXPOSE 8000 +EXPOSE 80 + +CMD ["./manage.py", "shell_plus", "--ptpython"] diff --git a/README.md b/README.md index a334151e..c04c7321 100644 --- a/README.md +++ b/README.md @@ -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 NOTE_MAIL=notekfet@localhost EMAIL_HOST=smtp.localhost - EMAIL_PORT=443 + EMAIL_PORT=465 EMAIL_USER=notekfet@localhost EMAIL_PASSWORD=CHANGE_ME diff --git a/ansible/ansible.cfg b/ansible/ansible.cfg new file mode 100644 index 00000000..e7a0537b --- /dev/null +++ b/ansible/ansible.cfg @@ -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 diff --git a/ansible/base.yml b/ansible/base.yml new file mode 100755 index 00000000..d3845f27 --- /dev/null +++ b/ansible/base.yml @@ -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 diff --git a/ansible/hosts b/ansible/hosts new file mode 100644 index 00000000..beafcc55 --- /dev/null +++ b/ansible/hosts @@ -0,0 +1,5 @@ +[server] +bde-nk20-beta.adh.crans.org + +[all:vars] +ansible_python_interpreter=/usr/bin/python3 diff --git a/ansible/roles/1-apt-basic/tasks/main.yml b/ansible/roles/1-apt-basic/tasks/main.yml new file mode 100644 index 00000000..eba6e5c3 --- /dev/null +++ b/ansible/roles/1-apt-basic/tasks/main.yml @@ -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 diff --git a/ansible/roles/2-nk20/tasks/main.yml b/ansible/roles/2-nk20/tasks/main.yml new file mode 100644 index 00000000..0c6d0213 --- /dev/null +++ b/ansible/roles/2-nk20/tasks/main.yml @@ -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 diff --git a/ansible/roles/3-pip/tasks/main.yml b/ansible/roles/3-pip/tasks/main.yml new file mode 100644 index 00000000..cbc3e902 --- /dev/null +++ b/ansible/roles/3-pip/tasks/main.yml @@ -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 diff --git a/ansible/roles/4-nginx/tasks/main.yml b/ansible/roles/4-nginx/tasks/main.yml new file mode 100644 index 00000000..427fe1df --- /dev/null +++ b/ansible/roles/4-nginx/tasks/main.yml @@ -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 diff --git a/ansible/roles/5-certbot/tasks/main.yml b/ansible/roles/5-certbot/tasks/main.yml new file mode 100644 index 00000000..52bc0d67 --- /dev/null +++ b/ansible/roles/5-certbot/tasks/main.yml @@ -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 diff --git a/ansible/roles/5-certbot/templates/letsencrypt/conf.d/nk20.ini.j2 b/ansible/roles/5-certbot/templates/letsencrypt/conf.d/nk20.ini.j2 new file mode 100644 index 00000000..b02abf5a --- /dev/null +++ b/ansible/roles/5-certbot/templates/letsencrypt/conf.d/nk20.ini.j2 @@ -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 + diff --git a/ansible/roles/6-psql/tasks/main.yml b/ansible/roles/6-psql/tasks/main.yml new file mode 100644 index 00000000..ba313ee1 --- /dev/null +++ b/ansible/roles/6-psql/tasks/main.yml @@ -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 diff --git a/ansible/roles/7-postinstall/tasks/main.yml b/ansible/roles/7-postinstall/tasks/main.yml new file mode 100644 index 00000000..b1615a0b --- /dev/null +++ b/ansible/roles/7-postinstall/tasks/main.yml @@ -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 diff --git a/apps/member/tables.py b/apps/member/tables.py index 8ac64674..0fa01545 100644 --- a/apps/member/tables.py +++ b/apps/member/tables.py @@ -25,7 +25,8 @@ class ClubTable(tables.Table): order_by = ('id',) model = Club template_name = 'django_tables2/bootstrap4.html' - fields = ('id', 'name', 'email') + fields = ('name', 'email',) + order_by = ('name',) row_attrs = { 'class': 'table-row', 'id': lambda record: "row-" + str(record.pk), diff --git a/apps/member/views.py b/apps/member/views.py index 04742f32..c52522e2 100644 --- a/apps/member/views.py +++ b/apps/member/views.py @@ -299,6 +299,22 @@ class ClubListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): model = Club 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): """ diff --git a/apps/note/models/transactions.py b/apps/note/models/transactions.py index 68249b81..a8fb3c22 100644 --- a/apps/note/models/transactions.py +++ b/apps/note/models/transactions.py @@ -163,6 +163,12 @@ class Transaction(PolymorphicModel): 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 not self.source_alias: self.source_alias = str(self.source) @@ -171,7 +177,7 @@ class Transaction(PolymorphicModel): self.destination_alias = str(self.destination) 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) return diff --git a/apps/note/views.py b/apps/note/views.py index 6c0ec1e2..81d58441 100644 --- a/apps/note/views.py +++ b/apps/note/views.py @@ -121,7 +121,7 @@ class ConsoView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): table_class = HistoryTable 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): """ diff --git a/apps/permission/fixtures/initial.json b/apps/permission/fixtures/initial.json index 095bcfc1..25509b56 100644 --- a/apps/permission/fixtures/initial.json +++ b/apps/permission/fixtures/initial.json @@ -272,7 +272,7 @@ "note", "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", "mask": 1, "field": "", @@ -2200,6 +2200,22 @@ "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", "pk": 1, @@ -2287,7 +2303,8 @@ 27, 60, 61, - 62 + 62, + 127 ] } }, diff --git a/entrypoint.sh b/entrypoint.sh index 4d0177e8..09cbc3c8 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/bin/sh # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later @@ -15,4 +15,10 @@ python manage.py compilemessages python manage.py makemigrations 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 diff --git a/nginx_note.conf_docker b/nginx_note.conf_docker new file mode 100644 index 00000000..c40eb59e --- /dev/null +++ b/nginx_note.conf_docker @@ -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/; + } +} diff --git a/note_kfet/middlewares.py b/note_kfet/middlewares.py index fff824c5..8987816d 100644 --- a/note_kfet/middlewares.py +++ b/note_kfet/middlewares.py @@ -50,8 +50,10 @@ class SessionMiddleware(object): def __call__(self, request): user = request.user - if 'HTTP_X_FORWARDED_FOR' in request.META: - ip = request.META.get('HTTP_X_FORWARDED_FOR') + if 'HTTP_X_REAL_IP' in request.META: + 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: ip = request.META.get('REMOTE_ADDR') diff --git a/templates/member/club_list.html b/templates/member/club_list.html index 8ba0ef3b..b43a186c 100644 --- a/templates/member/club_list.html +++ b/templates/member/club_list.html @@ -7,7 +7,7 @@

{% trans "search clubs" %}

- +
{% trans "Create club" %} @@ -28,43 +28,32 @@ {% endblock %} {% block extrajavascript %} {% endblock %} diff --git a/templates/member/user_list.html b/templates/member/user_list.html index 018c479d..d7628882 100644 --- a/templates/member/user_list.html +++ b/templates/member/user_list.html @@ -25,6 +25,8 @@ $(document).ready(function() { let old_pattern = null; let searchbar_obj = $("#searchbar"); + var timer_on = false; + var timer; function reloadTable() { let pattern = searchbar_obj.val(); @@ -33,17 +35,19 @@ return; $("#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() { $(".table-row").click(function() { window.document.location = $(this).data("href"); + timer_on = false; }); }