mirror of
https://github.com/ae-utbm/sith.git
synced 2024-11-26 02:54:20 +00:00
Merge pull request #731 from ae-utbm/taiste
MkDocs, Ninja API, logo promo 24 et refactors
This commit is contained in:
commit
378e8b53f2
2
.github/workflows/deploy.yml
vendored
2
.github/workflows/deploy.yml
vendored
@ -37,7 +37,7 @@ jobs:
|
|||||||
pushd ${{secrets.SITH_PATH}}
|
pushd ${{secrets.SITH_PATH}}
|
||||||
|
|
||||||
git pull
|
git pull
|
||||||
poetry install
|
poetry install --with prod --without docs,tests
|
||||||
poetry run ./manage.py install_xapian
|
poetry run ./manage.py install_xapian
|
||||||
poetry run ./manage.py migrate
|
poetry run ./manage.py migrate
|
||||||
echo "yes" | poetry run ./manage.py collectstatic
|
echo "yes" | poetry run ./manage.py collectstatic
|
||||||
|
21
.github/workflows/deploy_docs.yml
vendored
Normal file
21
.github/workflows/deploy_docs.yml
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
name: deploy_docs
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: ./.github/actions/setup_project
|
||||||
|
- run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV
|
||||||
|
- uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
key: mkdocs-material-${{ env.cache_id }}
|
||||||
|
path: .cache
|
||||||
|
restore-keys: |
|
||||||
|
mkdocs-material-
|
||||||
|
- run: poetry run mkdocs gh-deploy --force
|
2
.github/workflows/taiste.yml
vendored
2
.github/workflows/taiste.yml
vendored
@ -36,7 +36,7 @@ jobs:
|
|||||||
pushd ${{secrets.SITH_PATH}}
|
pushd ${{secrets.SITH_PATH}}
|
||||||
|
|
||||||
git pull
|
git pull
|
||||||
poetry install
|
poetry install --with prod --without docs,tests
|
||||||
poetry run ./manage.py install_xapian
|
poetry run ./manage.py install_xapian
|
||||||
poetry run ./manage.py migrate
|
poetry run ./manage.py migrate
|
||||||
echo "yes" | poetry run ./manage.py collectstatic
|
echo "yes" | poetry run ./manage.py collectstatic
|
||||||
|
@ -1,26 +0,0 @@
|
|||||||
# Read the Docs configuration file
|
|
||||||
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
|
|
||||||
|
|
||||||
# Required
|
|
||||||
version: 2
|
|
||||||
|
|
||||||
# Allow installing xapian-bindings in pip
|
|
||||||
build:
|
|
||||||
apt_packages:
|
|
||||||
- libxapian-dev
|
|
||||||
|
|
||||||
# Build documentation in the doc/ directory with Sphinx
|
|
||||||
sphinx:
|
|
||||||
configuration: doc/conf.py
|
|
||||||
|
|
||||||
# Optionally build your docs in additional formats such as PDF and ePub
|
|
||||||
formats: all
|
|
||||||
|
|
||||||
# Optionally set the version of Python and requirements required to build your docs
|
|
||||||
python:
|
|
||||||
version: "3.8"
|
|
||||||
install:
|
|
||||||
- method: pip
|
|
||||||
path: .
|
|
||||||
extra_requirements:
|
|
||||||
- docs
|
|
48
README.md
48
README.md
@ -1,40 +1,20 @@
|
|||||||
<p align="center">
|
# Sith
|
||||||
<a href="#">
|
|
||||||
<img src="https://img.shields.io/badge/Code%20Style-Black-000000?style=for-the-badge">
|
|
||||||
</a>
|
|
||||||
<a href="#">
|
|
||||||
<img src="https://img.shields.io/github/checks-status/ae-utbm/sith3/master?logo=github&style=for-the-badge&label=BUILD">
|
|
||||||
</a>
|
|
||||||
<a href="https://sith-ae.readthedocs.io/">
|
|
||||||
<img src="https://img.shields.io/readthedocs/sith-ae?logo=readthedocs&style=for-the-badge">
|
|
||||||
</a>
|
|
||||||
<a href="https://discord.gg/XK9WfPsUFm">
|
|
||||||
<img src="https://img.shields.io/discord/971448179075731476?label=Discord&logo=discord&style=for-the-badge">
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h3 align="center">This is the source code of the UTBM's student association available at https://ae.utbm.fr/.</h3>
|
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](#)
|
||||||
|
[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
|
||||||
|
[![CI status](https://github.com/ae-utbm/sith3/actions/workflows/ci.yml/badge.svg)](#)
|
||||||
|
[![Docs status](https://github.com/ae-utbm/sith3/actions/workflows/deploy_docs.yml/badge.svg)](https://ae-utbm.github.io/sith3)
|
||||||
|
[![Built with Material for MkDocs](https://img.shields.io/badge/Material_for_MkDocs-526CFE?style=default&logo=MaterialForMkDocs&logoColor=white)](https://squidfunk.github.io/mkdocs-material/)
|
||||||
|
[![discord](https://img.shields.io/discord/971448179075731476?label=discord&logo=discord&style=default)](https://discord.gg/xk9wfpsufm)
|
||||||
|
|
||||||
<p align="justify">All documentation is in the <code>docs</code> directory and online at https://sith-ae.readthedocs.io/. This documentation is written in French because it targets a French audience and it's too much work to maintain two versions. The code and code comments are strictly written in English.</p>
|
### This is the source code of the UTBM's student association available at [https://ae.utbm.fr/](https://ae.utbm.fr/).
|
||||||
|
|
||||||
<h4>If you want to contribute, here's how we recommend to read the docs:</h4>
|
All documentation is in the `docs` directory and online at [https://ae-utbm.github.io/sith3](https://ae-utbm.github.io/sith3). This documentation is written in French because it targets a French audience and it's too much work to maintain two versions. The code and code comments are strictly written in English.
|
||||||
|
|
||||||
<ul>
|
#### If you want to contribute, here's how we recommend to read the docs:
|
||||||
<li>
|
|
||||||
<p align="justify">
|
* First, it's advised to read the about part of the project to understand the goals and the mindset of the current and previous maintainers and know what to expect to learn.
|
||||||
First, it's advised to read the about part of the project to understand the goals and the mindset of the current and previous maintainers and know what to expect to learn.
|
* If in the first part you realize that you need more background about what we use, we provide some links to tutorials and documentation at the end of our documentation. Feel free to use it and complete it with what you found helpful.
|
||||||
</p>
|
* Keep in mind that this documentation is thought to be read in order.
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<p align="justify">
|
|
||||||
If in the first part you realize that you need more background about what we use, we provide some links to tutorials and documentation at the end of our documentation. Feel free to use it and complete it with what you found helpful.
|
|
||||||
</p>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<p align="justify">
|
|
||||||
Keep in mind that this documentation is thought to be read in order.
|
|
||||||
</p>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
> This project is licensed under GNU GPL, see the LICENSE file at the top of the repository for more details.
|
> This project is licensed under GNU GPL, see the LICENSE file at the top of the repository for more details.
|
||||||
|
@ -29,9 +29,7 @@ from core.models import SithFile, User
|
|||||||
|
|
||||||
|
|
||||||
class CurrencyField(models.DecimalField):
|
class CurrencyField(models.DecimalField):
|
||||||
"""
|
"""Custom database field used for currency."""
|
||||||
This is a custom database field used for currency
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
kwargs["max_digits"] = 12
|
kwargs["max_digits"] = 12
|
||||||
@ -71,30 +69,22 @@ class Company(models.Model):
|
|||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
def is_owned_by(self, user):
|
def is_owned_by(self, user):
|
||||||
"""
|
"""Check if that object can be edited by the given user."""
|
||||||
Method to see if that object can be edited by the given user
|
|
||||||
"""
|
|
||||||
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
|
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def can_be_edited_by(self, user):
|
def can_be_edited_by(self, user):
|
||||||
"""
|
"""Check if that object can be edited by the given user."""
|
||||||
Method to see if that object can be edited by the given user
|
return user.memberships.filter(
|
||||||
"""
|
end_date=None, club__role=settings.SITH_CLUB_ROLES_ID["Treasurer"]
|
||||||
for club in user.memberships.filter(end_date=None).all():
|
).exists()
|
||||||
if club and club.role == settings.SITH_CLUB_ROLES_ID["Treasurer"]:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def can_be_viewed_by(self, user):
|
def can_be_viewed_by(self, user):
|
||||||
"""
|
"""Check if that object can be viewed by the given user."""
|
||||||
Method to see if that object can be viewed by the given user
|
return user.memberships.filter(
|
||||||
"""
|
end_date=None, club__role_gte=settings.SITH_CLUB_ROLES_ID["Treasurer"]
|
||||||
for club in user.memberships.filter(end_date=None).all():
|
).exists()
|
||||||
if club and club.role >= settings.SITH_CLUB_ROLES_ID["Treasurer"]:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
class BankAccount(models.Model):
|
class BankAccount(models.Model):
|
||||||
@ -119,9 +109,7 @@ class BankAccount(models.Model):
|
|||||||
return reverse("accounting:bank_details", kwargs={"b_account_id": self.id})
|
return reverse("accounting:bank_details", kwargs={"b_account_id": self.id})
|
||||||
|
|
||||||
def is_owned_by(self, user):
|
def is_owned_by(self, user):
|
||||||
"""
|
"""Check if that object can be edited by the given user."""
|
||||||
Method to see if that object can be edited by the given user
|
|
||||||
"""
|
|
||||||
if user.is_anonymous:
|
if user.is_anonymous:
|
||||||
return False
|
return False
|
||||||
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
|
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
|
||||||
@ -158,9 +146,7 @@ class ClubAccount(models.Model):
|
|||||||
return reverse("accounting:club_details", kwargs={"c_account_id": self.id})
|
return reverse("accounting:club_details", kwargs={"c_account_id": self.id})
|
||||||
|
|
||||||
def is_owned_by(self, user):
|
def is_owned_by(self, user):
|
||||||
"""
|
"""Check if that object can be edited by the given user."""
|
||||||
Method to see if that object can be edited by the given user
|
|
||||||
"""
|
|
||||||
if user.is_anonymous:
|
if user.is_anonymous:
|
||||||
return False
|
return False
|
||||||
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
|
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
|
||||||
@ -168,18 +154,14 @@ class ClubAccount(models.Model):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def can_be_edited_by(self, user):
|
def can_be_edited_by(self, user):
|
||||||
"""
|
"""Check if that object can be edited by the given user."""
|
||||||
Method to see if that object can be edited by the given user
|
|
||||||
"""
|
|
||||||
m = self.club.get_membership_for(user)
|
m = self.club.get_membership_for(user)
|
||||||
if m and m.role == settings.SITH_CLUB_ROLES_ID["Treasurer"]:
|
if m and m.role == settings.SITH_CLUB_ROLES_ID["Treasurer"]:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def can_be_viewed_by(self, user):
|
def can_be_viewed_by(self, user):
|
||||||
"""
|
"""Check if that object can be viewed by the given user."""
|
||||||
Method to see if that object can be viewed by the given user
|
|
||||||
"""
|
|
||||||
m = self.club.get_membership_for(user)
|
m = self.club.get_membership_for(user)
|
||||||
if m and m.role >= settings.SITH_CLUB_ROLES_ID["Treasurer"]:
|
if m and m.role >= settings.SITH_CLUB_ROLES_ID["Treasurer"]:
|
||||||
return True
|
return True
|
||||||
@ -202,9 +184,7 @@ class ClubAccount(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
class GeneralJournal(models.Model):
|
class GeneralJournal(models.Model):
|
||||||
"""
|
"""Class storing all the operations for a period of time."""
|
||||||
Class storing all the operations for a period of time
|
|
||||||
"""
|
|
||||||
|
|
||||||
start_date = models.DateField(_("start date"))
|
start_date = models.DateField(_("start date"))
|
||||||
end_date = models.DateField(_("end date"), null=True, blank=True, default=None)
|
end_date = models.DateField(_("end date"), null=True, blank=True, default=None)
|
||||||
@ -231,9 +211,7 @@ class GeneralJournal(models.Model):
|
|||||||
return reverse("accounting:journal_details", kwargs={"j_id": self.id})
|
return reverse("accounting:journal_details", kwargs={"j_id": self.id})
|
||||||
|
|
||||||
def is_owned_by(self, user):
|
def is_owned_by(self, user):
|
||||||
"""
|
"""Check if that object can be edited by the given user."""
|
||||||
Method to see if that object can be edited by the given user
|
|
||||||
"""
|
|
||||||
if user.is_anonymous:
|
if user.is_anonymous:
|
||||||
return False
|
return False
|
||||||
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
|
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
|
||||||
@ -243,9 +221,7 @@ class GeneralJournal(models.Model):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def can_be_edited_by(self, user):
|
def can_be_edited_by(self, user):
|
||||||
"""
|
"""Check if that object can be edited by the given user."""
|
||||||
Method to see if that object can be edited by the given user
|
|
||||||
"""
|
|
||||||
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
|
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
|
||||||
return True
|
return True
|
||||||
if self.club_account.can_be_edited_by(user):
|
if self.club_account.can_be_edited_by(user):
|
||||||
@ -271,9 +247,7 @@ class GeneralJournal(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
class Operation(models.Model):
|
class Operation(models.Model):
|
||||||
"""
|
"""An operation is a line in the journal, a debit or a credit."""
|
||||||
An operation is a line in the journal, a debit or a credit
|
|
||||||
"""
|
|
||||||
|
|
||||||
number = models.IntegerField(_("number"))
|
number = models.IntegerField(_("number"))
|
||||||
journal = models.ForeignKey(
|
journal = models.ForeignKey(
|
||||||
@ -422,9 +396,7 @@ class Operation(models.Model):
|
|||||||
return tar
|
return tar
|
||||||
|
|
||||||
def is_owned_by(self, user):
|
def is_owned_by(self, user):
|
||||||
"""
|
"""Check if that object can be edited by the given user."""
|
||||||
Method to see if that object can be edited by the given user
|
|
||||||
"""
|
|
||||||
if user.is_anonymous:
|
if user.is_anonymous:
|
||||||
return False
|
return False
|
||||||
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
|
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
|
||||||
@ -437,9 +409,7 @@ class Operation(models.Model):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def can_be_edited_by(self, user):
|
def can_be_edited_by(self, user):
|
||||||
"""
|
"""Check if that object can be edited by the given user."""
|
||||||
Method to see if that object can be edited by the given user
|
|
||||||
"""
|
|
||||||
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
|
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
|
||||||
return True
|
return True
|
||||||
if self.journal.closed:
|
if self.journal.closed:
|
||||||
@ -451,10 +421,9 @@ class Operation(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
class AccountingType(models.Model):
|
class AccountingType(models.Model):
|
||||||
"""
|
"""Accounting types.
|
||||||
Class describing the accounting types.
|
|
||||||
|
|
||||||
Thoses are numbers used in accounting to classify operations
|
Those are numbers used in accounting to classify operations
|
||||||
"""
|
"""
|
||||||
|
|
||||||
code = models.CharField(
|
code = models.CharField(
|
||||||
@ -488,9 +457,7 @@ class AccountingType(models.Model):
|
|||||||
return reverse("accounting:type_list")
|
return reverse("accounting:type_list")
|
||||||
|
|
||||||
def is_owned_by(self, user):
|
def is_owned_by(self, user):
|
||||||
"""
|
"""Check if that object can be edited by the given user."""
|
||||||
Method to see if that object can be edited by the given user
|
|
||||||
"""
|
|
||||||
if user.is_anonymous:
|
if user.is_anonymous:
|
||||||
return False
|
return False
|
||||||
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
|
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
|
||||||
@ -499,9 +466,7 @@ class AccountingType(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
class SimplifiedAccountingType(models.Model):
|
class SimplifiedAccountingType(models.Model):
|
||||||
"""
|
"""Simplified version of `AccountingType`."""
|
||||||
Class describing the simplified accounting types.
|
|
||||||
"""
|
|
||||||
|
|
||||||
label = models.CharField(_("label"), max_length=128)
|
label = models.CharField(_("label"), max_length=128)
|
||||||
accounting_type = models.ForeignKey(
|
accounting_type = models.ForeignKey(
|
||||||
@ -533,7 +498,7 @@ class SimplifiedAccountingType(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
class Label(models.Model):
|
class Label(models.Model):
|
||||||
"""Label allow a club to sort its operations"""
|
"""Label allow a club to sort its operations."""
|
||||||
|
|
||||||
name = models.CharField(_("label"), max_length=64)
|
name = models.CharField(_("label"), max_length=64)
|
||||||
club_account = models.ForeignKey(
|
club_account = models.ForeignKey(
|
||||||
|
@ -28,7 +28,7 @@ from accounting.models import (
|
|||||||
from core.models import User
|
from core.models import User
|
||||||
|
|
||||||
|
|
||||||
class RefoundAccountTest(TestCase):
|
class TestRefoundAccount(TestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
cls.skia = User.objects.get(username="skia")
|
cls.skia = User.objects.get(username="skia")
|
||||||
@ -67,7 +67,7 @@ class RefoundAccountTest(TestCase):
|
|||||||
assert self.skia.customer.amount == 0
|
assert self.skia.customer.amount == 0
|
||||||
|
|
||||||
|
|
||||||
class JournalTest(TestCase):
|
class TestJournal(TestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
cls.journal = GeneralJournal.objects.get(id=1)
|
cls.journal = GeneralJournal.objects.get(id=1)
|
||||||
@ -91,7 +91,7 @@ class JournalTest(TestCase):
|
|||||||
assert "<td>M\xc3\xa9thode de paiement</td>" not in str(response_get.content)
|
assert "<td>M\xc3\xa9thode de paiement</td>" not in str(response_get.content)
|
||||||
|
|
||||||
|
|
||||||
class OperationTest(TestCase):
|
class TestOperation(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.tomorrow_formatted = (date.today() + timedelta(days=1)).strftime(
|
self.tomorrow_formatted = (date.today() + timedelta(days=1)).strftime(
|
||||||
"%d/%m/%Y"
|
"%d/%m/%Y"
|
||||||
|
@ -53,9 +53,7 @@ from counter.models import Counter, Product, Selling
|
|||||||
|
|
||||||
|
|
||||||
class BankAccountListView(CanViewMixin, ListView):
|
class BankAccountListView(CanViewMixin, ListView):
|
||||||
"""
|
"""A list view for the admins."""
|
||||||
A list view for the admins
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = BankAccount
|
model = BankAccount
|
||||||
template_name = "accounting/bank_account_list.jinja"
|
template_name = "accounting/bank_account_list.jinja"
|
||||||
@ -66,18 +64,14 @@ class BankAccountListView(CanViewMixin, ListView):
|
|||||||
|
|
||||||
|
|
||||||
class SimplifiedAccountingTypeListView(CanViewMixin, ListView):
|
class SimplifiedAccountingTypeListView(CanViewMixin, ListView):
|
||||||
"""
|
"""A list view for the admins."""
|
||||||
A list view for the admins
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = SimplifiedAccountingType
|
model = SimplifiedAccountingType
|
||||||
template_name = "accounting/simplifiedaccountingtype_list.jinja"
|
template_name = "accounting/simplifiedaccountingtype_list.jinja"
|
||||||
|
|
||||||
|
|
||||||
class SimplifiedAccountingTypeEditView(CanViewMixin, UpdateView):
|
class SimplifiedAccountingTypeEditView(CanViewMixin, UpdateView):
|
||||||
"""
|
"""An edit view for the admins."""
|
||||||
An edit view for the admins
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = SimplifiedAccountingType
|
model = SimplifiedAccountingType
|
||||||
pk_url_kwarg = "type_id"
|
pk_url_kwarg = "type_id"
|
||||||
@ -86,9 +80,7 @@ class SimplifiedAccountingTypeEditView(CanViewMixin, UpdateView):
|
|||||||
|
|
||||||
|
|
||||||
class SimplifiedAccountingTypeCreateView(CanCreateMixin, CreateView):
|
class SimplifiedAccountingTypeCreateView(CanCreateMixin, CreateView):
|
||||||
"""
|
"""Create an accounting type (for the admins)."""
|
||||||
Create an accounting type (for the admins)
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = SimplifiedAccountingType
|
model = SimplifiedAccountingType
|
||||||
fields = ["label", "accounting_type"]
|
fields = ["label", "accounting_type"]
|
||||||
@ -99,18 +91,14 @@ class SimplifiedAccountingTypeCreateView(CanCreateMixin, CreateView):
|
|||||||
|
|
||||||
|
|
||||||
class AccountingTypeListView(CanViewMixin, ListView):
|
class AccountingTypeListView(CanViewMixin, ListView):
|
||||||
"""
|
"""A list view for the admins."""
|
||||||
A list view for the admins
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = AccountingType
|
model = AccountingType
|
||||||
template_name = "accounting/accountingtype_list.jinja"
|
template_name = "accounting/accountingtype_list.jinja"
|
||||||
|
|
||||||
|
|
||||||
class AccountingTypeEditView(CanViewMixin, UpdateView):
|
class AccountingTypeEditView(CanViewMixin, UpdateView):
|
||||||
"""
|
"""An edit view for the admins."""
|
||||||
An edit view for the admins
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = AccountingType
|
model = AccountingType
|
||||||
pk_url_kwarg = "type_id"
|
pk_url_kwarg = "type_id"
|
||||||
@ -119,9 +107,7 @@ class AccountingTypeEditView(CanViewMixin, UpdateView):
|
|||||||
|
|
||||||
|
|
||||||
class AccountingTypeCreateView(CanCreateMixin, CreateView):
|
class AccountingTypeCreateView(CanCreateMixin, CreateView):
|
||||||
"""
|
"""Create an accounting type (for the admins)."""
|
||||||
Create an accounting type (for the admins)
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = AccountingType
|
model = AccountingType
|
||||||
fields = ["code", "label", "movement_type"]
|
fields = ["code", "label", "movement_type"]
|
||||||
@ -132,9 +118,7 @@ class AccountingTypeCreateView(CanCreateMixin, CreateView):
|
|||||||
|
|
||||||
|
|
||||||
class BankAccountEditView(CanViewMixin, UpdateView):
|
class BankAccountEditView(CanViewMixin, UpdateView):
|
||||||
"""
|
"""An edit view for the admins."""
|
||||||
An edit view for the admins
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = BankAccount
|
model = BankAccount
|
||||||
pk_url_kwarg = "b_account_id"
|
pk_url_kwarg = "b_account_id"
|
||||||
@ -143,9 +127,7 @@ class BankAccountEditView(CanViewMixin, UpdateView):
|
|||||||
|
|
||||||
|
|
||||||
class BankAccountDetailView(CanViewMixin, DetailView):
|
class BankAccountDetailView(CanViewMixin, DetailView):
|
||||||
"""
|
"""A detail view, listing every club account."""
|
||||||
A detail view, listing every club account
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = BankAccount
|
model = BankAccount
|
||||||
pk_url_kwarg = "b_account_id"
|
pk_url_kwarg = "b_account_id"
|
||||||
@ -153,9 +135,7 @@ class BankAccountDetailView(CanViewMixin, DetailView):
|
|||||||
|
|
||||||
|
|
||||||
class BankAccountCreateView(CanCreateMixin, CreateView):
|
class BankAccountCreateView(CanCreateMixin, CreateView):
|
||||||
"""
|
"""Create a bank account (for the admins)."""
|
||||||
Create a bank account (for the admins)
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = BankAccount
|
model = BankAccount
|
||||||
fields = ["name", "club", "iban", "number"]
|
fields = ["name", "club", "iban", "number"]
|
||||||
@ -165,9 +145,7 @@ class BankAccountCreateView(CanCreateMixin, CreateView):
|
|||||||
class BankAccountDeleteView(
|
class BankAccountDeleteView(
|
||||||
CanEditPropMixin, DeleteView
|
CanEditPropMixin, DeleteView
|
||||||
): # TODO change Delete to Close
|
): # TODO change Delete to Close
|
||||||
"""
|
"""Delete a bank account (for the admins)."""
|
||||||
Delete a bank account (for the admins)
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = BankAccount
|
model = BankAccount
|
||||||
pk_url_kwarg = "b_account_id"
|
pk_url_kwarg = "b_account_id"
|
||||||
@ -179,9 +157,7 @@ class BankAccountDeleteView(
|
|||||||
|
|
||||||
|
|
||||||
class ClubAccountEditView(CanViewMixin, UpdateView):
|
class ClubAccountEditView(CanViewMixin, UpdateView):
|
||||||
"""
|
"""An edit view for the admins."""
|
||||||
An edit view for the admins
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = ClubAccount
|
model = ClubAccount
|
||||||
pk_url_kwarg = "c_account_id"
|
pk_url_kwarg = "c_account_id"
|
||||||
@ -190,9 +166,7 @@ class ClubAccountEditView(CanViewMixin, UpdateView):
|
|||||||
|
|
||||||
|
|
||||||
class ClubAccountDetailView(CanViewMixin, DetailView):
|
class ClubAccountDetailView(CanViewMixin, DetailView):
|
||||||
"""
|
"""A detail view, listing every journal."""
|
||||||
A detail view, listing every journal
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = ClubAccount
|
model = ClubAccount
|
||||||
pk_url_kwarg = "c_account_id"
|
pk_url_kwarg = "c_account_id"
|
||||||
@ -200,9 +174,7 @@ class ClubAccountDetailView(CanViewMixin, DetailView):
|
|||||||
|
|
||||||
|
|
||||||
class ClubAccountCreateView(CanCreateMixin, CreateView):
|
class ClubAccountCreateView(CanCreateMixin, CreateView):
|
||||||
"""
|
"""Create a club account (for the admins)."""
|
||||||
Create a club account (for the admins)
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = ClubAccount
|
model = ClubAccount
|
||||||
fields = ["name", "club", "bank_account"]
|
fields = ["name", "club", "bank_account"]
|
||||||
@ -220,9 +192,7 @@ class ClubAccountCreateView(CanCreateMixin, CreateView):
|
|||||||
class ClubAccountDeleteView(
|
class ClubAccountDeleteView(
|
||||||
CanEditPropMixin, DeleteView
|
CanEditPropMixin, DeleteView
|
||||||
): # TODO change Delete to Close
|
): # TODO change Delete to Close
|
||||||
"""
|
"""Delete a club account (for the admins)."""
|
||||||
Delete a club account (for the admins)
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = ClubAccount
|
model = ClubAccount
|
||||||
pk_url_kwarg = "c_account_id"
|
pk_url_kwarg = "c_account_id"
|
||||||
@ -282,9 +252,7 @@ class JournalTabsMixin(TabedViewMixin):
|
|||||||
|
|
||||||
|
|
||||||
class JournalCreateView(CanCreateMixin, CreateView):
|
class JournalCreateView(CanCreateMixin, CreateView):
|
||||||
"""
|
"""Create a general journal."""
|
||||||
Create a general journal
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = GeneralJournal
|
model = GeneralJournal
|
||||||
form_class = modelform_factory(
|
form_class = modelform_factory(
|
||||||
@ -304,9 +272,7 @@ class JournalCreateView(CanCreateMixin, CreateView):
|
|||||||
|
|
||||||
|
|
||||||
class JournalDetailView(JournalTabsMixin, CanViewMixin, DetailView):
|
class JournalDetailView(JournalTabsMixin, CanViewMixin, DetailView):
|
||||||
"""
|
"""A detail view, listing every operation."""
|
||||||
A detail view, listing every operation
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = GeneralJournal
|
model = GeneralJournal
|
||||||
pk_url_kwarg = "j_id"
|
pk_url_kwarg = "j_id"
|
||||||
@ -315,9 +281,7 @@ class JournalDetailView(JournalTabsMixin, CanViewMixin, DetailView):
|
|||||||
|
|
||||||
|
|
||||||
class JournalEditView(CanEditMixin, UpdateView):
|
class JournalEditView(CanEditMixin, UpdateView):
|
||||||
"""
|
"""Update a general journal."""
|
||||||
Update a general journal
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = GeneralJournal
|
model = GeneralJournal
|
||||||
pk_url_kwarg = "j_id"
|
pk_url_kwarg = "j_id"
|
||||||
@ -326,9 +290,7 @@ class JournalEditView(CanEditMixin, UpdateView):
|
|||||||
|
|
||||||
|
|
||||||
class JournalDeleteView(CanEditPropMixin, DeleteView):
|
class JournalDeleteView(CanEditPropMixin, DeleteView):
|
||||||
"""
|
"""Delete a club account (for the admins)."""
|
||||||
Delete a club account (for the admins)
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = GeneralJournal
|
model = GeneralJournal
|
||||||
pk_url_kwarg = "j_id"
|
pk_url_kwarg = "j_id"
|
||||||
@ -467,9 +429,7 @@ class OperationForm(forms.ModelForm):
|
|||||||
|
|
||||||
|
|
||||||
class OperationCreateView(CanCreateMixin, CreateView):
|
class OperationCreateView(CanCreateMixin, CreateView):
|
||||||
"""
|
"""Create an operation."""
|
||||||
Create an operation
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = Operation
|
model = Operation
|
||||||
form_class = OperationForm
|
form_class = OperationForm
|
||||||
@ -487,7 +447,7 @@ class OperationCreateView(CanCreateMixin, CreateView):
|
|||||||
return ret
|
return ret
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
"""Add journal to the context"""
|
"""Add journal to the context."""
|
||||||
kwargs = super().get_context_data(**kwargs)
|
kwargs = super().get_context_data(**kwargs)
|
||||||
if self.journal:
|
if self.journal:
|
||||||
kwargs["object"] = self.journal
|
kwargs["object"] = self.journal
|
||||||
@ -495,9 +455,7 @@ class OperationCreateView(CanCreateMixin, CreateView):
|
|||||||
|
|
||||||
|
|
||||||
class OperationEditView(CanEditMixin, UpdateView):
|
class OperationEditView(CanEditMixin, UpdateView):
|
||||||
"""
|
"""An edit view, working as detail for the moment."""
|
||||||
An edit view, working as detail for the moment
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = Operation
|
model = Operation
|
||||||
pk_url_kwarg = "op_id"
|
pk_url_kwarg = "op_id"
|
||||||
@ -505,16 +463,14 @@ class OperationEditView(CanEditMixin, UpdateView):
|
|||||||
template_name = "accounting/operation_edit.jinja"
|
template_name = "accounting/operation_edit.jinja"
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
"""Add journal to the context"""
|
"""Add journal to the context."""
|
||||||
kwargs = super().get_context_data(**kwargs)
|
kwargs = super().get_context_data(**kwargs)
|
||||||
kwargs["object"] = self.object.journal
|
kwargs["object"] = self.object.journal
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
|
|
||||||
class OperationPDFView(CanViewMixin, DetailView):
|
class OperationPDFView(CanViewMixin, DetailView):
|
||||||
"""
|
"""Display the PDF of a given operation."""
|
||||||
Display the PDF of a given operation
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = Operation
|
model = Operation
|
||||||
pk_url_kwarg = "op_id"
|
pk_url_kwarg = "op_id"
|
||||||
@ -666,9 +622,7 @@ class OperationPDFView(CanViewMixin, DetailView):
|
|||||||
|
|
||||||
|
|
||||||
class JournalNatureStatementView(JournalTabsMixin, CanViewMixin, DetailView):
|
class JournalNatureStatementView(JournalTabsMixin, CanViewMixin, DetailView):
|
||||||
"""
|
"""Display a statement sorted by labels."""
|
||||||
Display a statement sorted by labels
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = GeneralJournal
|
model = GeneralJournal
|
||||||
pk_url_kwarg = "j_id"
|
pk_url_kwarg = "j_id"
|
||||||
@ -726,16 +680,14 @@ class JournalNatureStatementView(JournalTabsMixin, CanViewMixin, DetailView):
|
|||||||
return statement
|
return statement
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
"""Add infos to the context"""
|
"""Add infos to the context."""
|
||||||
kwargs = super().get_context_data(**kwargs)
|
kwargs = super().get_context_data(**kwargs)
|
||||||
kwargs["statement"] = self.big_statement()
|
kwargs["statement"] = self.big_statement()
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
|
|
||||||
class JournalPersonStatementView(JournalTabsMixin, CanViewMixin, DetailView):
|
class JournalPersonStatementView(JournalTabsMixin, CanViewMixin, DetailView):
|
||||||
"""
|
"""Calculate a dictionary with operation target and sum of operations."""
|
||||||
Calculate a dictionary with operation target and sum of operations
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = GeneralJournal
|
model = GeneralJournal
|
||||||
pk_url_kwarg = "j_id"
|
pk_url_kwarg = "j_id"
|
||||||
@ -765,7 +717,7 @@ class JournalPersonStatementView(JournalTabsMixin, CanViewMixin, DetailView):
|
|||||||
return sum(self.statement(movement_type).values())
|
return sum(self.statement(movement_type).values())
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
"""Add journal to the context"""
|
"""Add journal to the context."""
|
||||||
kwargs = super().get_context_data(**kwargs)
|
kwargs = super().get_context_data(**kwargs)
|
||||||
kwargs["credit_statement"] = self.statement("CREDIT")
|
kwargs["credit_statement"] = self.statement("CREDIT")
|
||||||
kwargs["debit_statement"] = self.statement("DEBIT")
|
kwargs["debit_statement"] = self.statement("DEBIT")
|
||||||
@ -775,9 +727,7 @@ class JournalPersonStatementView(JournalTabsMixin, CanViewMixin, DetailView):
|
|||||||
|
|
||||||
|
|
||||||
class JournalAccountingStatementView(JournalTabsMixin, CanViewMixin, DetailView):
|
class JournalAccountingStatementView(JournalTabsMixin, CanViewMixin, DetailView):
|
||||||
"""
|
"""Calculate a dictionary with operation type and sum of operations."""
|
||||||
Calculate a dictionary with operation type and sum of operations
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = GeneralJournal
|
model = GeneralJournal
|
||||||
pk_url_kwarg = "j_id"
|
pk_url_kwarg = "j_id"
|
||||||
@ -795,7 +745,7 @@ class JournalAccountingStatementView(JournalTabsMixin, CanViewMixin, DetailView)
|
|||||||
return statement
|
return statement
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
"""Add journal to the context"""
|
"""Add journal to the context."""
|
||||||
kwargs = super().get_context_data(**kwargs)
|
kwargs = super().get_context_data(**kwargs)
|
||||||
kwargs["statement"] = self.statement()
|
kwargs["statement"] = self.statement()
|
||||||
return kwargs
|
return kwargs
|
||||||
@ -810,9 +760,7 @@ class CompanyListView(CanViewMixin, ListView):
|
|||||||
|
|
||||||
|
|
||||||
class CompanyCreateView(CanCreateMixin, CreateView):
|
class CompanyCreateView(CanCreateMixin, CreateView):
|
||||||
"""
|
"""Create a company."""
|
||||||
Create a company
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = Company
|
model = Company
|
||||||
fields = ["name"]
|
fields = ["name"]
|
||||||
@ -821,9 +769,7 @@ class CompanyCreateView(CanCreateMixin, CreateView):
|
|||||||
|
|
||||||
|
|
||||||
class CompanyEditView(CanCreateMixin, UpdateView):
|
class CompanyEditView(CanCreateMixin, UpdateView):
|
||||||
"""
|
"""Edit a company."""
|
||||||
Edit a company
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = Company
|
model = Company
|
||||||
pk_url_kwarg = "co_id"
|
pk_url_kwarg = "co_id"
|
||||||
@ -882,9 +828,7 @@ class CloseCustomerAccountForm(forms.Form):
|
|||||||
|
|
||||||
|
|
||||||
class RefoundAccountView(FormView):
|
class RefoundAccountView(FormView):
|
||||||
"""
|
"""Create a selling with the same amount than the current user money."""
|
||||||
Create a selling with the same amount than the current user money
|
|
||||||
"""
|
|
||||||
|
|
||||||
template_name = "accounting/refound_account.jinja"
|
template_name = "accounting/refound_account.jinja"
|
||||||
form_class = CloseCustomerAccountForm
|
form_class = CloseCustomerAccountForm
|
||||||
|
@ -1,14 +0,0 @@
|
|||||||
#
|
|
||||||
# Copyright 2023 © AE UTBM
|
|
||||||
# ae@utbm.fr / ae.info@utbm.fr
|
|
||||||
#
|
|
||||||
# This file is part of the website of the UTBM Student Association (AE UTBM),
|
|
||||||
# https://ae.utbm.fr.
|
|
||||||
#
|
|
||||||
# You can find the source code of the website at https://github.com/ae-utbm/sith3
|
|
||||||
#
|
|
||||||
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
|
|
||||||
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
|
|
||||||
# OR WITHIN THE LOCAL FILE "LICENSE"
|
|
||||||
#
|
|
||||||
#
|
|
16
api/admin.py
16
api/admin.py
@ -1,16 +0,0 @@
|
|||||||
#
|
|
||||||
# Copyright 2023 © AE UTBM
|
|
||||||
# ae@utbm.fr / ae.info@utbm.fr
|
|
||||||
#
|
|
||||||
# This file is part of the website of the UTBM Student Association (AE UTBM),
|
|
||||||
# https://ae.utbm.fr.
|
|
||||||
#
|
|
||||||
# You can find the source code of the website at https://github.com/ae-utbm/sith3
|
|
||||||
#
|
|
||||||
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
|
|
||||||
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
|
|
||||||
# OR WITHIN THE LOCAL FILE "LICENSE"
|
|
||||||
#
|
|
||||||
#
|
|
||||||
|
|
||||||
# Register your models here.
|
|
@ -1,16 +0,0 @@
|
|||||||
#
|
|
||||||
# Copyright 2023 © AE UTBM
|
|
||||||
# ae@utbm.fr / ae.info@utbm.fr
|
|
||||||
#
|
|
||||||
# This file is part of the website of the UTBM Student Association (AE UTBM),
|
|
||||||
# https://ae.utbm.fr.
|
|
||||||
#
|
|
||||||
# You can find the source code of the website at https://github.com/ae-utbm/sith3
|
|
||||||
#
|
|
||||||
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
|
|
||||||
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
|
|
||||||
# OR WITHIN THE LOCAL FILE "LICENSE"
|
|
||||||
#
|
|
||||||
#
|
|
||||||
|
|
||||||
# Create your models here.
|
|
49
api/urls.py
49
api/urls.py
@ -1,49 +0,0 @@
|
|||||||
#
|
|
||||||
# Copyright 2023 © AE UTBM
|
|
||||||
# ae@utbm.fr / ae.info@utbm.fr
|
|
||||||
#
|
|
||||||
# This file is part of the website of the UTBM Student Association (AE UTBM),
|
|
||||||
# https://ae.utbm.fr.
|
|
||||||
#
|
|
||||||
# You can find the source code of the website at https://github.com/ae-utbm/sith3
|
|
||||||
#
|
|
||||||
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
|
|
||||||
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
|
|
||||||
# OR WITHIN THE LOCAL FILE "LICENSE"
|
|
||||||
#
|
|
||||||
#
|
|
||||||
|
|
||||||
from django.urls import include, path, re_path
|
|
||||||
from rest_framework import routers
|
|
||||||
|
|
||||||
from api.views import *
|
|
||||||
|
|
||||||
# Router config
|
|
||||||
router = routers.DefaultRouter()
|
|
||||||
router.register(r"counter", CounterViewSet, basename="api_counter")
|
|
||||||
router.register(r"user", UserViewSet, basename="api_user")
|
|
||||||
router.register(r"club", ClubViewSet, basename="api_club")
|
|
||||||
router.register(r"group", GroupViewSet, basename="api_group")
|
|
||||||
|
|
||||||
# Launderette
|
|
||||||
router.register(
|
|
||||||
r"launderette/place", LaunderettePlaceViewSet, basename="api_launderette_place"
|
|
||||||
)
|
|
||||||
router.register(
|
|
||||||
r"launderette/machine",
|
|
||||||
LaunderetteMachineViewSet,
|
|
||||||
basename="api_launderette_machine",
|
|
||||||
)
|
|
||||||
router.register(
|
|
||||||
r"launderette/token", LaunderetteTokenViewSet, basename="api_launderette_token"
|
|
||||||
)
|
|
||||||
|
|
||||||
urlpatterns = [
|
|
||||||
# API
|
|
||||||
re_path(r"^", include(router.urls)),
|
|
||||||
re_path(r"^login/", include("rest_framework.urls", namespace="rest_framework")),
|
|
||||||
re_path(r"^markdown$", RenderMarkdown, name="api_markdown"),
|
|
||||||
re_path(r"^mailings$", FetchMailingLists, name="mailings_fetch"),
|
|
||||||
re_path(r"^uv$", uv_endpoint, name="uv_endpoint"),
|
|
||||||
path("sas/<int:user>", all_pictures_of_user_endpoint, name="all_pictures_of_user"),
|
|
||||||
]
|
|
@ -1,72 +0,0 @@
|
|||||||
#
|
|
||||||
# Copyright 2023 © AE UTBM
|
|
||||||
# ae@utbm.fr / ae.info@utbm.fr
|
|
||||||
#
|
|
||||||
# This file is part of the website of the UTBM Student Association (AE UTBM),
|
|
||||||
# https://ae.utbm.fr.
|
|
||||||
#
|
|
||||||
# You can find the source code of the website at https://github.com/ae-utbm/sith3
|
|
||||||
#
|
|
||||||
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
|
|
||||||
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
|
|
||||||
# OR WITHIN THE LOCAL FILE "LICENSE"
|
|
||||||
#
|
|
||||||
#
|
|
||||||
|
|
||||||
from django.core.exceptions import PermissionDenied
|
|
||||||
from django.db.models.query import QuerySet
|
|
||||||
from rest_framework import viewsets
|
|
||||||
from rest_framework.decorators import action
|
|
||||||
from rest_framework.response import Response
|
|
||||||
|
|
||||||
from core.views import can_edit, can_view
|
|
||||||
|
|
||||||
|
|
||||||
def check_if(obj, user, test):
|
|
||||||
"""
|
|
||||||
Detect if it's a single object or a queryset
|
|
||||||
aply a given test on individual object and return global permission
|
|
||||||
"""
|
|
||||||
if isinstance(obj, QuerySet):
|
|
||||||
for o in obj:
|
|
||||||
if test(o, user) is False:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return test(obj, user)
|
|
||||||
|
|
||||||
|
|
||||||
class ManageModelMixin:
|
|
||||||
@action(detail=True)
|
|
||||||
def id(self, request, pk=None):
|
|
||||||
"""
|
|
||||||
Get by id (api/v1/router/{pk}/id/)
|
|
||||||
"""
|
|
||||||
self.queryset = get_object_or_404(self.queryset.filter(id=pk))
|
|
||||||
serializer = self.get_serializer(self.queryset)
|
|
||||||
return Response(serializer.data)
|
|
||||||
|
|
||||||
|
|
||||||
class RightModelViewSet(ManageModelMixin, viewsets.ModelViewSet):
|
|
||||||
def dispatch(self, request, *arg, **kwargs):
|
|
||||||
res = super().dispatch(request, *arg, **kwargs)
|
|
||||||
obj = self.queryset
|
|
||||||
user = self.request.user
|
|
||||||
try:
|
|
||||||
if request.method == "GET" and check_if(obj, user, can_view):
|
|
||||||
return res
|
|
||||||
if request.method != "GET" and check_if(obj, user, can_edit):
|
|
||||||
return res
|
|
||||||
except:
|
|
||||||
pass # To prevent bug with Anonymous user
|
|
||||||
raise PermissionDenied
|
|
||||||
|
|
||||||
|
|
||||||
from .api import *
|
|
||||||
from .club import *
|
|
||||||
from .counter import *
|
|
||||||
from .group import *
|
|
||||||
from .launderette import *
|
|
||||||
from .sas import *
|
|
||||||
from .user import *
|
|
||||||
from .uv import *
|
|
@ -1,33 +0,0 @@
|
|||||||
#
|
|
||||||
# Copyright 2023 © AE UTBM
|
|
||||||
# ae@utbm.fr / ae.info@utbm.fr
|
|
||||||
#
|
|
||||||
# This file is part of the website of the UTBM Student Association (AE UTBM),
|
|
||||||
# https://ae.utbm.fr.
|
|
||||||
#
|
|
||||||
# You can find the source code of the website at https://github.com/ae-utbm/sith3
|
|
||||||
#
|
|
||||||
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
|
|
||||||
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
|
|
||||||
# OR WITHIN THE LOCAL FILE "LICENSE"
|
|
||||||
#
|
|
||||||
#
|
|
||||||
|
|
||||||
from rest_framework.decorators import api_view, renderer_classes
|
|
||||||
from rest_framework.renderers import StaticHTMLRenderer
|
|
||||||
from rest_framework.response import Response
|
|
||||||
|
|
||||||
from core.templatetags.renderer import markdown
|
|
||||||
|
|
||||||
|
|
||||||
@api_view(["POST"])
|
|
||||||
@renderer_classes((StaticHTMLRenderer,))
|
|
||||||
def RenderMarkdown(request):
|
|
||||||
"""
|
|
||||||
Render Markdown
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
data = markdown(request.POST["text"])
|
|
||||||
except:
|
|
||||||
data = "Error"
|
|
||||||
return Response(data)
|
|
@ -1,53 +0,0 @@
|
|||||||
#
|
|
||||||
# Copyright 2023 © AE UTBM
|
|
||||||
# ae@utbm.fr / ae.info@utbm.fr
|
|
||||||
#
|
|
||||||
# This file is part of the website of the UTBM Student Association (AE UTBM),
|
|
||||||
# https://ae.utbm.fr.
|
|
||||||
#
|
|
||||||
# You can find the source code of the website at https://github.com/ae-utbm/sith3
|
|
||||||
#
|
|
||||||
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
|
|
||||||
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
|
|
||||||
# OR WITHIN THE LOCAL FILE "LICENSE"
|
|
||||||
#
|
|
||||||
#
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.core.exceptions import PermissionDenied
|
|
||||||
from rest_framework import serializers
|
|
||||||
from rest_framework.decorators import api_view, renderer_classes
|
|
||||||
from rest_framework.renderers import StaticHTMLRenderer
|
|
||||||
from rest_framework.response import Response
|
|
||||||
|
|
||||||
from api.views import RightModelViewSet
|
|
||||||
from club.models import Club, Mailing
|
|
||||||
|
|
||||||
|
|
||||||
class ClubSerializer(serializers.ModelSerializer):
|
|
||||||
class Meta:
|
|
||||||
model = Club
|
|
||||||
fields = ("id", "name", "unix_name", "address", "members")
|
|
||||||
|
|
||||||
|
|
||||||
class ClubViewSet(RightModelViewSet):
|
|
||||||
"""
|
|
||||||
Manage Clubs (api/v1/club/)
|
|
||||||
"""
|
|
||||||
|
|
||||||
serializer_class = ClubSerializer
|
|
||||||
queryset = Club.objects.all()
|
|
||||||
|
|
||||||
|
|
||||||
@api_view(["GET"])
|
|
||||||
@renderer_classes((StaticHTMLRenderer,))
|
|
||||||
def FetchMailingLists(request):
|
|
||||||
key = request.GET.get("key", "")
|
|
||||||
if key != settings.SITH_MAILING_FETCH_KEY:
|
|
||||||
raise PermissionDenied
|
|
||||||
data = ""
|
|
||||||
for mailing in Mailing.objects.filter(
|
|
||||||
is_moderated=True, club__is_active=True
|
|
||||||
).all():
|
|
||||||
data += mailing.fetch_format() + "\n"
|
|
||||||
return Response(data)
|
|
@ -1,50 +0,0 @@
|
|||||||
#
|
|
||||||
# Copyright 2023 © AE UTBM
|
|
||||||
# ae@utbm.fr / ae.info@utbm.fr
|
|
||||||
#
|
|
||||||
# This file is part of the website of the UTBM Student Association (AE UTBM),
|
|
||||||
# https://ae.utbm.fr.
|
|
||||||
#
|
|
||||||
# You can find the source code of the website at https://github.com/ae-utbm/sith3
|
|
||||||
#
|
|
||||||
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
|
|
||||||
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
|
|
||||||
# OR WITHIN THE LOCAL FILE "LICENSE"
|
|
||||||
#
|
|
||||||
#
|
|
||||||
|
|
||||||
from rest_framework import serializers
|
|
||||||
from rest_framework.decorators import action
|
|
||||||
from rest_framework.response import Response
|
|
||||||
|
|
||||||
from api.views import RightModelViewSet
|
|
||||||
from counter.models import Counter
|
|
||||||
|
|
||||||
|
|
||||||
class CounterSerializer(serializers.ModelSerializer):
|
|
||||||
is_open = serializers.BooleanField(read_only=True)
|
|
||||||
barman_list = serializers.ListField(
|
|
||||||
child=serializers.IntegerField(), read_only=True
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Counter
|
|
||||||
fields = ("id", "name", "type", "club", "products", "is_open", "barman_list")
|
|
||||||
|
|
||||||
|
|
||||||
class CounterViewSet(RightModelViewSet):
|
|
||||||
"""
|
|
||||||
Manage Counters (api/v1/counter/)
|
|
||||||
"""
|
|
||||||
|
|
||||||
serializer_class = CounterSerializer
|
|
||||||
queryset = Counter.objects.all()
|
|
||||||
|
|
||||||
@action(detail=False)
|
|
||||||
def bar(self, request):
|
|
||||||
"""
|
|
||||||
Return all bars (api/v1/counter/bar/)
|
|
||||||
"""
|
|
||||||
self.queryset = self.queryset.filter(type="BAR")
|
|
||||||
serializer = self.get_serializer(self.queryset, many=True)
|
|
||||||
return Response(serializer.data)
|
|
@ -1,33 +0,0 @@
|
|||||||
#
|
|
||||||
# Copyright 2023 © AE UTBM
|
|
||||||
# ae@utbm.fr / ae.info@utbm.fr
|
|
||||||
#
|
|
||||||
# This file is part of the website of the UTBM Student Association (AE UTBM),
|
|
||||||
# https://ae.utbm.fr.
|
|
||||||
#
|
|
||||||
# You can find the source code of the website at https://github.com/ae-utbm/sith3
|
|
||||||
#
|
|
||||||
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
|
|
||||||
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
|
|
||||||
# OR WITHIN THE LOCAL FILE "LICENSE"
|
|
||||||
#
|
|
||||||
#
|
|
||||||
|
|
||||||
from rest_framework import serializers
|
|
||||||
|
|
||||||
from api.views import RightModelViewSet
|
|
||||||
from core.models import RealGroup
|
|
||||||
|
|
||||||
|
|
||||||
class GroupSerializer(serializers.ModelSerializer):
|
|
||||||
class Meta:
|
|
||||||
model = RealGroup
|
|
||||||
|
|
||||||
|
|
||||||
class GroupViewSet(RightModelViewSet):
|
|
||||||
"""
|
|
||||||
Manage Groups (api/v1/group/)
|
|
||||||
"""
|
|
||||||
|
|
||||||
serializer_class = GroupSerializer
|
|
||||||
queryset = RealGroup.objects.all()
|
|
@ -1,126 +0,0 @@
|
|||||||
#
|
|
||||||
# Copyright 2023 © AE UTBM
|
|
||||||
# ae@utbm.fr / ae.info@utbm.fr
|
|
||||||
#
|
|
||||||
# This file is part of the website of the UTBM Student Association (AE UTBM),
|
|
||||||
# https://ae.utbm.fr.
|
|
||||||
#
|
|
||||||
# You can find the source code of the website at https://github.com/ae-utbm/sith3
|
|
||||||
#
|
|
||||||
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
|
|
||||||
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
|
|
||||||
# OR WITHIN THE LOCAL FILE "LICENSE"
|
|
||||||
#
|
|
||||||
#
|
|
||||||
|
|
||||||
from rest_framework import serializers
|
|
||||||
from rest_framework.decorators import action
|
|
||||||
from rest_framework.response import Response
|
|
||||||
|
|
||||||
from api.views import RightModelViewSet
|
|
||||||
from launderette.models import Launderette, Machine, Token
|
|
||||||
|
|
||||||
|
|
||||||
class LaunderettePlaceSerializer(serializers.ModelSerializer):
|
|
||||||
machine_list = serializers.ListField(
|
|
||||||
child=serializers.IntegerField(), read_only=True
|
|
||||||
)
|
|
||||||
token_list = serializers.ListField(child=serializers.IntegerField(), read_only=True)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Launderette
|
|
||||||
fields = (
|
|
||||||
"id",
|
|
||||||
"name",
|
|
||||||
"counter",
|
|
||||||
"machine_list",
|
|
||||||
"token_list",
|
|
||||||
"get_absolute_url",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class LaunderetteMachineSerializer(serializers.ModelSerializer):
|
|
||||||
class Meta:
|
|
||||||
model = Machine
|
|
||||||
fields = ("id", "name", "type", "is_working", "launderette")
|
|
||||||
|
|
||||||
|
|
||||||
class LaunderetteTokenSerializer(serializers.ModelSerializer):
|
|
||||||
class Meta:
|
|
||||||
model = Token
|
|
||||||
fields = (
|
|
||||||
"id",
|
|
||||||
"name",
|
|
||||||
"type",
|
|
||||||
"launderette",
|
|
||||||
"borrow_date",
|
|
||||||
"user",
|
|
||||||
"is_avaliable",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class LaunderettePlaceViewSet(RightModelViewSet):
|
|
||||||
"""
|
|
||||||
Manage Launderette (api/v1/launderette/place/)
|
|
||||||
"""
|
|
||||||
|
|
||||||
serializer_class = LaunderettePlaceSerializer
|
|
||||||
queryset = Launderette.objects.all()
|
|
||||||
|
|
||||||
|
|
||||||
class LaunderetteMachineViewSet(RightModelViewSet):
|
|
||||||
"""
|
|
||||||
Manage Washing Machines (api/v1/launderette/machine/)
|
|
||||||
"""
|
|
||||||
|
|
||||||
serializer_class = LaunderetteMachineSerializer
|
|
||||||
queryset = Machine.objects.all()
|
|
||||||
|
|
||||||
|
|
||||||
class LaunderetteTokenViewSet(RightModelViewSet):
|
|
||||||
"""
|
|
||||||
Manage Launderette's tokens (api/v1/launderette/token/)
|
|
||||||
"""
|
|
||||||
|
|
||||||
serializer_class = LaunderetteTokenSerializer
|
|
||||||
queryset = Token.objects.all()
|
|
||||||
|
|
||||||
@action(detail=False)
|
|
||||||
def washing(self, request):
|
|
||||||
"""
|
|
||||||
Return all washing tokens (api/v1/launderette/token/washing)
|
|
||||||
"""
|
|
||||||
self.queryset = self.queryset.filter(type="WASHING")
|
|
||||||
serializer = self.get_serializer(self.queryset, many=True)
|
|
||||||
return Response(serializer.data)
|
|
||||||
|
|
||||||
@action(detail=False)
|
|
||||||
def drying(self, request):
|
|
||||||
"""
|
|
||||||
Return all drying tokens (api/v1/launderette/token/drying)
|
|
||||||
"""
|
|
||||||
self.queryset = self.queryset.filter(type="DRYING")
|
|
||||||
serializer = self.get_serializer(self.queryset, many=True)
|
|
||||||
return Response(serializer.data)
|
|
||||||
|
|
||||||
@action(detail=False)
|
|
||||||
def avaliable(self, request):
|
|
||||||
"""
|
|
||||||
Return all avaliable tokens (api/v1/launderette/token/avaliable)
|
|
||||||
"""
|
|
||||||
self.queryset = self.queryset.filter(
|
|
||||||
borrow_date__isnull=True, user__isnull=True
|
|
||||||
)
|
|
||||||
serializer = self.get_serializer(self.queryset, many=True)
|
|
||||||
return Response(serializer.data)
|
|
||||||
|
|
||||||
@action(detail=False)
|
|
||||||
def unavaliable(self, request):
|
|
||||||
"""
|
|
||||||
Return all unavaliable tokens (api/v1/launderette/token/unavaliable)
|
|
||||||
"""
|
|
||||||
self.queryset = self.queryset.filter(
|
|
||||||
borrow_date__isnull=False, user__isnull=False
|
|
||||||
)
|
|
||||||
serializer = self.get_serializer(self.queryset, many=True)
|
|
||||||
return Response(serializer.data)
|
|
@ -1,43 +0,0 @@
|
|||||||
from typing import List
|
|
||||||
|
|
||||||
from rest_framework.decorators import api_view, renderer_classes
|
|
||||||
from rest_framework.exceptions import PermissionDenied
|
|
||||||
from rest_framework.generics import get_object_or_404
|
|
||||||
from rest_framework.renderers import JSONRenderer
|
|
||||||
from rest_framework.request import Request
|
|
||||||
from rest_framework.response import Response
|
|
||||||
|
|
||||||
from core.models import User
|
|
||||||
from core.views import can_edit
|
|
||||||
from sas.models import Picture
|
|
||||||
|
|
||||||
|
|
||||||
def all_pictures_of_user(user: User) -> List[Picture]:
|
|
||||||
return [
|
|
||||||
relation.picture
|
|
||||||
for relation in user.pictures.exclude(picture=None)
|
|
||||||
.order_by("-picture__parent__date", "id")
|
|
||||||
.select_related("picture__parent")
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
@api_view(["GET"])
|
|
||||||
@renderer_classes((JSONRenderer,))
|
|
||||||
def all_pictures_of_user_endpoint(request: Request, user: int):
|
|
||||||
requested_user: User = get_object_or_404(User, pk=user)
|
|
||||||
if not can_edit(requested_user, request.user):
|
|
||||||
raise PermissionDenied
|
|
||||||
|
|
||||||
return Response(
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"name": f"{picture.parent.name} - {picture.name}",
|
|
||||||
"date": picture.date,
|
|
||||||
"author": str(picture.owner),
|
|
||||||
"full_size_url": picture.get_download_url(),
|
|
||||||
"compressed_url": picture.get_download_compressed_url(),
|
|
||||||
"thumb_url": picture.get_download_thumb_url(),
|
|
||||||
}
|
|
||||||
for picture in all_pictures_of_user(requested_user)
|
|
||||||
]
|
|
||||||
)
|
|
@ -1,58 +0,0 @@
|
|||||||
#
|
|
||||||
# Copyright 2023 © AE UTBM
|
|
||||||
# ae@utbm.fr / ae.info@utbm.fr
|
|
||||||
#
|
|
||||||
# This file is part of the website of the UTBM Student Association (AE UTBM),
|
|
||||||
# https://ae.utbm.fr.
|
|
||||||
#
|
|
||||||
# You can find the source code of the website at https://github.com/ae-utbm/sith3
|
|
||||||
#
|
|
||||||
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
|
|
||||||
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
|
|
||||||
# OR WITHIN THE LOCAL FILE "LICENSE"
|
|
||||||
#
|
|
||||||
#
|
|
||||||
|
|
||||||
import datetime
|
|
||||||
|
|
||||||
from rest_framework import serializers
|
|
||||||
from rest_framework.decorators import action
|
|
||||||
from rest_framework.response import Response
|
|
||||||
|
|
||||||
from api.views import RightModelViewSet
|
|
||||||
from core.models import User
|
|
||||||
|
|
||||||
|
|
||||||
class UserSerializer(serializers.ModelSerializer):
|
|
||||||
class Meta:
|
|
||||||
model = User
|
|
||||||
fields = (
|
|
||||||
"id",
|
|
||||||
"first_name",
|
|
||||||
"last_name",
|
|
||||||
"email",
|
|
||||||
"date_of_birth",
|
|
||||||
"nick_name",
|
|
||||||
"is_active",
|
|
||||||
"date_joined",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class UserViewSet(RightModelViewSet):
|
|
||||||
"""
|
|
||||||
Manage Users (api/v1/user/)
|
|
||||||
Only show active users
|
|
||||||
"""
|
|
||||||
|
|
||||||
serializer_class = UserSerializer
|
|
||||||
queryset = User.objects.filter(is_active=True)
|
|
||||||
|
|
||||||
@action(detail=False)
|
|
||||||
def birthday(self, request):
|
|
||||||
"""
|
|
||||||
Return all users born today (api/v1/user/birstdays)
|
|
||||||
"""
|
|
||||||
date = datetime.datetime.today()
|
|
||||||
self.queryset = self.queryset.filter(date_of_birth=date)
|
|
||||||
serializer = self.get_serializer(self.queryset, many=True)
|
|
||||||
return Response(serializer.data)
|
|
128
api/views/uv.py
128
api/views/uv.py
@ -1,128 +0,0 @@
|
|||||||
import json
|
|
||||||
import urllib.request
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.core.exceptions import PermissionDenied
|
|
||||||
from rest_framework import serializers
|
|
||||||
from rest_framework.decorators import api_view, renderer_classes
|
|
||||||
from rest_framework.renderers import JSONRenderer
|
|
||||||
from rest_framework.response import Response
|
|
||||||
|
|
||||||
from pedagogy.views import CanCreateUVFunctionMixin
|
|
||||||
|
|
||||||
|
|
||||||
@api_view(["GET"])
|
|
||||||
@renderer_classes((JSONRenderer,))
|
|
||||||
def uv_endpoint(request):
|
|
||||||
if not CanCreateUVFunctionMixin.can_create_uv(request.user):
|
|
||||||
raise PermissionDenied
|
|
||||||
|
|
||||||
params = request.query_params
|
|
||||||
if "year" not in params or "code" not in params:
|
|
||||||
raise serializers.ValidationError("Missing query parameter")
|
|
||||||
|
|
||||||
short_uv, full_uv = find_uv("fr", params["year"], params["code"])
|
|
||||||
if short_uv is None or full_uv is None:
|
|
||||||
return Response(status=204)
|
|
||||||
|
|
||||||
return Response(make_clean_uv(short_uv, full_uv))
|
|
||||||
|
|
||||||
|
|
||||||
def find_uv(lang, year, code):
|
|
||||||
"""
|
|
||||||
Uses the UTBM API to find an UV.
|
|
||||||
short_uv is the UV entry in the UV list. It is returned as it contains
|
|
||||||
information which are not in full_uv.
|
|
||||||
full_uv is the detailed representation of an UV.
|
|
||||||
"""
|
|
||||||
# query the UV list
|
|
||||||
uvs_url = settings.SITH_PEDAGOGY_UTBM_API + "/uvs/{}/{}".format(lang, year)
|
|
||||||
response = urllib.request.urlopen(uvs_url)
|
|
||||||
uvs = json.loads(response.read().decode("utf-8"))
|
|
||||||
|
|
||||||
try:
|
|
||||||
# find the first UV which matches the code
|
|
||||||
short_uv = next(uv for uv in uvs if uv["code"] == code)
|
|
||||||
except StopIteration:
|
|
||||||
return (None, None)
|
|
||||||
|
|
||||||
# get detailed information about the UV
|
|
||||||
uv_url = settings.SITH_PEDAGOGY_UTBM_API + "/uv/{}/{}/{}/{}".format(
|
|
||||||
lang, year, code, short_uv["codeFormation"]
|
|
||||||
)
|
|
||||||
response = urllib.request.urlopen(uv_url)
|
|
||||||
full_uv = json.loads(response.read().decode("utf-8"))
|
|
||||||
|
|
||||||
return (short_uv, full_uv)
|
|
||||||
|
|
||||||
|
|
||||||
def make_clean_uv(short_uv, full_uv):
|
|
||||||
"""
|
|
||||||
Cleans the data up so that it corresponds to our data representation.
|
|
||||||
"""
|
|
||||||
res = {}
|
|
||||||
|
|
||||||
res["credit_type"] = short_uv["codeCategorie"]
|
|
||||||
|
|
||||||
# probably wrong on a few UVs as we pick the first UV we find but
|
|
||||||
# availability depends on the formation
|
|
||||||
semesters = {
|
|
||||||
(True, True): "AUTUMN_AND_SPRING",
|
|
||||||
(True, False): "AUTUMN",
|
|
||||||
(False, True): "SPRING",
|
|
||||||
}
|
|
||||||
res["semester"] = semesters.get(
|
|
||||||
(short_uv["ouvertAutomne"], short_uv["ouvertPrintemps"]), "CLOSED"
|
|
||||||
)
|
|
||||||
|
|
||||||
langs = {"es": "SP", "en": "EN", "de": "DE"}
|
|
||||||
res["language"] = langs.get(full_uv["codeLangue"], "FR")
|
|
||||||
|
|
||||||
if full_uv["departement"] == "Pôle Humanités":
|
|
||||||
res["department"] = "HUMA"
|
|
||||||
else:
|
|
||||||
departments = {
|
|
||||||
"AL": "IMSI",
|
|
||||||
"AE": "EE",
|
|
||||||
"GI": "GI",
|
|
||||||
"GC": "EE",
|
|
||||||
"GM": "MC",
|
|
||||||
"TC": "TC",
|
|
||||||
"GP": "IMSI",
|
|
||||||
"ED": "EDIM",
|
|
||||||
"AI": "GI",
|
|
||||||
"AM": "MC",
|
|
||||||
}
|
|
||||||
res["department"] = departments.get(full_uv["codeFormation"], "NA")
|
|
||||||
|
|
||||||
res["credits"] = full_uv["creditsEcts"]
|
|
||||||
|
|
||||||
activities = ("CM", "TD", "TP", "THE", "TE")
|
|
||||||
for activity in activities:
|
|
||||||
res["hours_{}".format(activity)] = 0
|
|
||||||
for activity in full_uv["activites"]:
|
|
||||||
if activity["code"] in activities:
|
|
||||||
res["hours_{}".format(activity["code"])] += activity["nbh"] // 60
|
|
||||||
|
|
||||||
# wrong if the manager changes depending on the semester
|
|
||||||
semester = full_uv.get("automne", None)
|
|
||||||
if not semester:
|
|
||||||
semester = full_uv.get("printemps", {})
|
|
||||||
res["manager"] = semester.get("responsable", "")
|
|
||||||
|
|
||||||
res["title"] = full_uv["libelle"]
|
|
||||||
|
|
||||||
descriptions = {
|
|
||||||
"objectives": "objectifs",
|
|
||||||
"program": "programme",
|
|
||||||
"skills": "acquisitionCompetences",
|
|
||||||
"key_concepts": "acquisitionNotions",
|
|
||||||
}
|
|
||||||
|
|
||||||
for res_key, full_uv_key in descriptions.items():
|
|
||||||
res[res_key] = full_uv[full_uv_key]
|
|
||||||
# if not found or the API did not return a string
|
|
||||||
if type(res[res_key]) != str:
|
|
||||||
res[res_key] = ""
|
|
||||||
|
|
||||||
return res
|
|
@ -44,9 +44,7 @@ class ClubEditForm(forms.ModelForm):
|
|||||||
|
|
||||||
|
|
||||||
class MailingForm(forms.Form):
|
class MailingForm(forms.Form):
|
||||||
"""
|
"""Form handling mailing lists right."""
|
||||||
Form handling mailing lists right
|
|
||||||
"""
|
|
||||||
|
|
||||||
ACTION_NEW_MAILING = 1
|
ACTION_NEW_MAILING = 1
|
||||||
ACTION_NEW_SUBSCRIPTION = 2
|
ACTION_NEW_SUBSCRIPTION = 2
|
||||||
@ -105,16 +103,12 @@ class MailingForm(forms.Form):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def check_required(self, cleaned_data, field):
|
def check_required(self, cleaned_data, field):
|
||||||
"""
|
"""If the given field doesn't exist or has no value, add a required error on it."""
|
||||||
If the given field doesn't exist or has no value, add a required error on it
|
|
||||||
"""
|
|
||||||
if not cleaned_data.get(field, None):
|
if not cleaned_data.get(field, None):
|
||||||
self.add_error(field, _("This field is required"))
|
self.add_error(field, _("This field is required"))
|
||||||
|
|
||||||
def clean_subscription_users(self):
|
def clean_subscription_users(self):
|
||||||
"""
|
"""Convert given users into real users and check their validity."""
|
||||||
Convert given users into real users and check their validity
|
|
||||||
"""
|
|
||||||
cleaned_data = super().clean()
|
cleaned_data = super().clean()
|
||||||
users = []
|
users = []
|
||||||
for user in cleaned_data["subscription_users"]:
|
for user in cleaned_data["subscription_users"]:
|
||||||
@ -177,9 +171,7 @@ class SellingsForm(forms.Form):
|
|||||||
|
|
||||||
|
|
||||||
class ClubMemberForm(forms.Form):
|
class ClubMemberForm(forms.Form):
|
||||||
"""
|
"""Form handling the members of a club."""
|
||||||
Form handling the members of a club
|
|
||||||
"""
|
|
||||||
|
|
||||||
error_css_class = "error"
|
error_css_class = "error"
|
||||||
required_css_class = "required"
|
required_css_class = "required"
|
||||||
@ -236,9 +228,9 @@ class ClubMemberForm(forms.Form):
|
|||||||
self.fields.pop("start_date")
|
self.fields.pop("start_date")
|
||||||
|
|
||||||
def clean_users(self):
|
def clean_users(self):
|
||||||
"""
|
"""Check that the user is not trying to add an user already in the club.
|
||||||
Check that the user is not trying to add an user already in the club
|
|
||||||
Also check that the user is valid and has a valid subscription
|
Also check that the user is valid and has a valid subscription.
|
||||||
"""
|
"""
|
||||||
cleaned_data = super().clean()
|
cleaned_data = super().clean()
|
||||||
users = []
|
users = []
|
||||||
@ -260,9 +252,7 @@ class ClubMemberForm(forms.Form):
|
|||||||
return users
|
return users
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
"""
|
"""Check user rights for adding an user."""
|
||||||
Check user rights for adding an user
|
|
||||||
"""
|
|
||||||
cleaned_data = super().clean()
|
cleaned_data = super().clean()
|
||||||
|
|
||||||
if "start_date" in cleaned_data and not cleaned_data["start_date"]:
|
if "start_date" in cleaned_data and not cleaned_data["start_date"]:
|
||||||
|
@ -21,7 +21,7 @@
|
|||||||
# Place - Suite 330, Boston, MA 02111-1307, USA.
|
# Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
from typing import Optional
|
from __future__ import annotations
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core import validators
|
from django.core import validators
|
||||||
@ -46,9 +46,7 @@ def get_default_owner_group():
|
|||||||
|
|
||||||
|
|
||||||
class Club(models.Model):
|
class Club(models.Model):
|
||||||
"""
|
"""The Club class, made as a tree to allow nice tidy organization."""
|
||||||
The Club class, made as a tree to allow nice tidy organization
|
|
||||||
"""
|
|
||||||
|
|
||||||
id = models.AutoField(primary_key=True, db_index=True)
|
id = models.AutoField(primary_key=True, db_index=True)
|
||||||
name = models.CharField(_("name"), max_length=64)
|
name = models.CharField(_("name"), max_length=64)
|
||||||
@ -141,7 +139,7 @@ class Club(models.Model):
|
|||||||
).first()
|
).first()
|
||||||
|
|
||||||
def check_loop(self):
|
def check_loop(self):
|
||||||
"""Raise a validation error when a loop is found within the parent list"""
|
"""Raise a validation error when a loop is found within the parent list."""
|
||||||
objs = []
|
objs = []
|
||||||
cur = self
|
cur = self
|
||||||
while cur.parent is not None:
|
while cur.parent is not None:
|
||||||
@ -223,9 +221,7 @@ class Club(models.Model):
|
|||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
def is_owned_by(self, user):
|
def is_owned_by(self, user):
|
||||||
"""
|
"""Method to see if that object can be super edited by the given user."""
|
||||||
Method to see if that object can be super edited by the given user
|
|
||||||
"""
|
|
||||||
if user.is_anonymous:
|
if user.is_anonymous:
|
||||||
return False
|
return False
|
||||||
return user.is_board_member
|
return user.is_board_member
|
||||||
@ -234,23 +230,20 @@ class Club(models.Model):
|
|||||||
return "https://%s%s" % (settings.SITH_URL, self.logo.url)
|
return "https://%s%s" % (settings.SITH_URL, self.logo.url)
|
||||||
|
|
||||||
def can_be_edited_by(self, user):
|
def can_be_edited_by(self, user):
|
||||||
"""
|
"""Method to see if that object can be edited by the given user."""
|
||||||
Method to see if that object can be edited by the given user
|
|
||||||
"""
|
|
||||||
return self.has_rights_in_club(user)
|
return self.has_rights_in_club(user)
|
||||||
|
|
||||||
def can_be_viewed_by(self, user):
|
def can_be_viewed_by(self, user):
|
||||||
"""
|
"""Method to see if that object can be seen by the given user."""
|
||||||
Method to see if that object can be seen by the given user
|
|
||||||
"""
|
|
||||||
sub = User.objects.filter(pk=user.pk).first()
|
sub = User.objects.filter(pk=user.pk).first()
|
||||||
if sub is None:
|
if sub is None:
|
||||||
return False
|
return False
|
||||||
return sub.was_subscribed
|
return sub.was_subscribed
|
||||||
|
|
||||||
def get_membership_for(self, user: User) -> Optional["Membership"]:
|
def get_membership_for(self, user: User) -> Membership | None:
|
||||||
"""
|
"""Return the current membership the given user.
|
||||||
Return the current membership the given user.
|
|
||||||
|
Note:
|
||||||
The result is cached.
|
The result is cached.
|
||||||
"""
|
"""
|
||||||
if user.is_anonymous:
|
if user.is_anonymous:
|
||||||
@ -273,15 +266,12 @@ class Club(models.Model):
|
|||||||
|
|
||||||
class MembershipQuerySet(models.QuerySet):
|
class MembershipQuerySet(models.QuerySet):
|
||||||
def ongoing(self) -> "MembershipQuerySet":
|
def ongoing(self) -> "MembershipQuerySet":
|
||||||
"""
|
"""Filter all memberships which are not finished yet."""
|
||||||
Filter all memberships which are not finished yet
|
|
||||||
"""
|
|
||||||
# noinspection PyTypeChecker
|
# noinspection PyTypeChecker
|
||||||
return self.filter(Q(end_date=None) | Q(end_date__gte=timezone.now()))
|
return self.filter(Q(end_date=None) | Q(end_date__gte=timezone.now()))
|
||||||
|
|
||||||
def board(self) -> "MembershipQuerySet":
|
def board(self) -> "MembershipQuerySet":
|
||||||
"""
|
"""Filter all memberships where the user is/was in the board.
|
||||||
Filter all memberships where the user is/was in the board.
|
|
||||||
|
|
||||||
Be aware that users who were in the board in the past
|
Be aware that users who were in the board in the past
|
||||||
are included, even if there are no more members.
|
are included, even if there are no more members.
|
||||||
@ -293,9 +283,9 @@ class MembershipQuerySet(models.QuerySet):
|
|||||||
return self.filter(role__gt=settings.SITH_MAXIMUM_FREE_ROLE)
|
return self.filter(role__gt=settings.SITH_MAXIMUM_FREE_ROLE)
|
||||||
|
|
||||||
def update(self, **kwargs):
|
def update(self, **kwargs):
|
||||||
"""
|
"""Refresh the cache for the elements of the queryset.
|
||||||
Work just like the default Django's update() method,
|
|
||||||
but add a cache refresh for the elements of the queryset.
|
Besides that, does the same job as a regular update method.
|
||||||
|
|
||||||
Be aware that this adds a db query to retrieve the updated objects
|
Be aware that this adds a db query to retrieve the updated objects
|
||||||
"""
|
"""
|
||||||
@ -315,8 +305,7 @@ class MembershipQuerySet(models.QuerySet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def delete(self):
|
def delete(self):
|
||||||
"""
|
"""Work just like the default Django's delete() method,
|
||||||
Work just like the default Django's delete() method,
|
|
||||||
but add a cache invalidation for the elements of the queryset
|
but add a cache invalidation for the elements of the queryset
|
||||||
before the deletion.
|
before the deletion.
|
||||||
|
|
||||||
@ -332,8 +321,7 @@ class MembershipQuerySet(models.QuerySet):
|
|||||||
|
|
||||||
|
|
||||||
class Membership(models.Model):
|
class Membership(models.Model):
|
||||||
"""
|
"""The Membership class makes the connection between User and Clubs.
|
||||||
The Membership class makes the connection between User and Clubs
|
|
||||||
|
|
||||||
Both Users and Clubs can have many Membership objects:
|
Both Users and Clubs can have many Membership objects:
|
||||||
- a user can be a member of many clubs at a time
|
- a user can be a member of many clubs at a time
|
||||||
@ -390,17 +378,13 @@ class Membership(models.Model):
|
|||||||
return reverse("club:club_members", kwargs={"club_id": self.club_id})
|
return reverse("club:club_members", kwargs={"club_id": self.club_id})
|
||||||
|
|
||||||
def is_owned_by(self, user):
|
def is_owned_by(self, user):
|
||||||
"""
|
"""Method to see if that object can be super edited by the given user."""
|
||||||
Method to see if that object can be super edited by the given user
|
|
||||||
"""
|
|
||||||
if user.is_anonymous:
|
if user.is_anonymous:
|
||||||
return False
|
return False
|
||||||
return user.is_board_member
|
return user.is_board_member
|
||||||
|
|
||||||
def can_be_edited_by(self, user: User) -> bool:
|
def can_be_edited_by(self, user: User) -> bool:
|
||||||
"""
|
"""Check if that object can be edited by the given user."""
|
||||||
Check if that object can be edited by the given user
|
|
||||||
"""
|
|
||||||
if user.is_root or user.is_board_member:
|
if user.is_root or user.is_board_member:
|
||||||
return True
|
return True
|
||||||
membership = self.club.get_membership_for(user)
|
membership = self.club.get_membership_for(user)
|
||||||
@ -414,9 +398,10 @@ class Membership(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
class Mailing(models.Model):
|
class Mailing(models.Model):
|
||||||
"""
|
"""A Mailing list for a club.
|
||||||
This class correspond to a mailing list
|
|
||||||
Remember that mailing lists should be validated by UTBM
|
Warning:
|
||||||
|
Remember that mailing lists should be validated by UTBM.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
club = models.ForeignKey(
|
club = models.ForeignKey(
|
||||||
@ -501,16 +486,12 @@ class Mailing(models.Model):
|
|||||||
super().delete()
|
super().delete()
|
||||||
|
|
||||||
def fetch_format(self):
|
def fetch_format(self):
|
||||||
resp = self.email + ": "
|
destination = "".join(s.fetch_format() for s in self.subscriptions.all())
|
||||||
for sub in self.subscriptions.all():
|
return f"{self.email}: {destination}"
|
||||||
resp += sub.fetch_format()
|
|
||||||
return resp
|
|
||||||
|
|
||||||
|
|
||||||
class MailingSubscription(models.Model):
|
class MailingSubscription(models.Model):
|
||||||
"""
|
"""Link between user and mailing list."""
|
||||||
This class makes the link between user and mailing list
|
|
||||||
"""
|
|
||||||
|
|
||||||
mailing = models.ForeignKey(
|
mailing = models.ForeignKey(
|
||||||
Mailing,
|
Mailing,
|
||||||
|
136
club/tests.py
136
club/tests.py
@ -28,9 +28,9 @@ from core.models import AnonymousUser, User
|
|||||||
from sith.settings import SITH_BAR_MANAGER, SITH_MAIN_CLUB_ID
|
from sith.settings import SITH_BAR_MANAGER, SITH_MAIN_CLUB_ID
|
||||||
|
|
||||||
|
|
||||||
class ClubTest(TestCase):
|
class TestClub(TestCase):
|
||||||
"""
|
"""Set up data for test cases related to clubs and membership.
|
||||||
Set up data for test cases related to clubs and membership
|
|
||||||
The generated dataset is the one created by the populate command,
|
The generated dataset is the one created by the populate command,
|
||||||
plus the following modifications :
|
plus the following modifications :
|
||||||
|
|
||||||
@ -92,10 +92,9 @@ class ClubTest(TestCase):
|
|||||||
cache.clear()
|
cache.clear()
|
||||||
|
|
||||||
|
|
||||||
class MembershipQuerySetTest(ClubTest):
|
class TestMembershipQuerySet(TestClub):
|
||||||
def test_ongoing(self):
|
def test_ongoing(self):
|
||||||
"""
|
"""Test that the ongoing queryset method returns the memberships that
|
||||||
Test that the ongoing queryset method returns the memberships that
|
|
||||||
are not ended.
|
are not ended.
|
||||||
"""
|
"""
|
||||||
current_members = list(self.club.members.ongoing().order_by("id"))
|
current_members = list(self.club.members.ongoing().order_by("id"))
|
||||||
@ -108,9 +107,8 @@ class MembershipQuerySetTest(ClubTest):
|
|||||||
assert current_members == expected
|
assert current_members == expected
|
||||||
|
|
||||||
def test_board(self):
|
def test_board(self):
|
||||||
"""
|
"""Test that the board queryset method returns the memberships
|
||||||
Test that the board queryset method returns the memberships
|
of user in the club board.
|
||||||
of user in the club board
|
|
||||||
"""
|
"""
|
||||||
board_members = list(self.club.members.board().order_by("id"))
|
board_members = list(self.club.members.board().order_by("id"))
|
||||||
expected = [
|
expected = [
|
||||||
@ -123,9 +121,8 @@ class MembershipQuerySetTest(ClubTest):
|
|||||||
assert board_members == expected
|
assert board_members == expected
|
||||||
|
|
||||||
def test_ongoing_board(self):
|
def test_ongoing_board(self):
|
||||||
"""
|
"""Test that combining ongoing and board returns users
|
||||||
Test that combining ongoing and board returns users
|
who are currently board members of the club.
|
||||||
who are currently board members of the club
|
|
||||||
"""
|
"""
|
||||||
members = list(self.club.members.ongoing().board().order_by("id"))
|
members = list(self.club.members.ongoing().board().order_by("id"))
|
||||||
expected = [
|
expected = [
|
||||||
@ -136,9 +133,7 @@ class MembershipQuerySetTest(ClubTest):
|
|||||||
assert members == expected
|
assert members == expected
|
||||||
|
|
||||||
def test_update_invalidate_cache(self):
|
def test_update_invalidate_cache(self):
|
||||||
"""
|
"""Test that the `update` queryset method properly invalidate cache."""
|
||||||
Test that the `update` queryset method properly invalidate cache
|
|
||||||
"""
|
|
||||||
mem_skia = self.skia.memberships.get(club=self.club)
|
mem_skia = self.skia.memberships.get(club=self.club)
|
||||||
cache.set(f"membership_{mem_skia.club_id}_{mem_skia.user_id}", mem_skia)
|
cache.set(f"membership_{mem_skia.club_id}_{mem_skia.user_id}", mem_skia)
|
||||||
self.skia.memberships.update(end_date=localtime(now()).date())
|
self.skia.memberships.update(end_date=localtime(now()).date())
|
||||||
@ -157,10 +152,7 @@ class MembershipQuerySetTest(ClubTest):
|
|||||||
assert new_mem.role == 5
|
assert new_mem.role == 5
|
||||||
|
|
||||||
def test_delete_invalidate_cache(self):
|
def test_delete_invalidate_cache(self):
|
||||||
"""
|
"""Test that the `delete` queryset properly invalidate cache."""
|
||||||
Test that the `delete` queryset properly invalidate cache
|
|
||||||
"""
|
|
||||||
|
|
||||||
mem_skia = self.skia.memberships.get(club=self.club)
|
mem_skia = self.skia.memberships.get(club=self.club)
|
||||||
mem_comptable = self.comptable.memberships.get(club=self.club)
|
mem_comptable = self.comptable.memberships.get(club=self.club)
|
||||||
cache.set(f"membership_{mem_skia.club_id}_{mem_skia.user_id}", mem_skia)
|
cache.set(f"membership_{mem_skia.club_id}_{mem_skia.user_id}", mem_skia)
|
||||||
@ -178,11 +170,9 @@ class MembershipQuerySetTest(ClubTest):
|
|||||||
assert cached_mem == "not_member"
|
assert cached_mem == "not_member"
|
||||||
|
|
||||||
|
|
||||||
class ClubModelTest(ClubTest):
|
class TestClubModel(TestClub):
|
||||||
def assert_membership_started_today(self, user: User, role: int):
|
def assert_membership_started_today(self, user: User, role: int):
|
||||||
"""
|
"""Assert that the given membership is active and started today."""
|
||||||
Assert that the given membership is active and started today
|
|
||||||
"""
|
|
||||||
membership = user.memberships.ongoing().filter(club=self.club).first()
|
membership = user.memberships.ongoing().filter(club=self.club).first()
|
||||||
assert membership is not None
|
assert membership is not None
|
||||||
assert localtime(now()).date() == membership.start_date
|
assert localtime(now()).date() == membership.start_date
|
||||||
@ -195,17 +185,14 @@ class ClubModelTest(ClubTest):
|
|||||||
assert user.is_in_group(name=board_group)
|
assert user.is_in_group(name=board_group)
|
||||||
|
|
||||||
def assert_membership_ended_today(self, user: User):
|
def assert_membership_ended_today(self, user: User):
|
||||||
"""
|
"""Assert that the given user have a membership which ended today."""
|
||||||
Assert that the given user have a membership which ended today
|
|
||||||
"""
|
|
||||||
today = localtime(now()).date()
|
today = localtime(now()).date()
|
||||||
assert user.memberships.filter(club=self.club, end_date=today).exists()
|
assert user.memberships.filter(club=self.club, end_date=today).exists()
|
||||||
assert self.club.get_membership_for(user) is None
|
assert self.club.get_membership_for(user) is None
|
||||||
|
|
||||||
def test_access_unauthorized(self):
|
def test_access_unauthorized(self):
|
||||||
"""
|
"""Test that users who never subscribed and anonymous users
|
||||||
Test that users who never subscribed and anonymous users
|
cannot see the page.
|
||||||
cannot see the page
|
|
||||||
"""
|
"""
|
||||||
response = self.client.post(self.members_url)
|
response = self.client.post(self.members_url)
|
||||||
assert response.status_code == 403
|
assert response.status_code == 403
|
||||||
@ -215,8 +202,7 @@ class ClubModelTest(ClubTest):
|
|||||||
assert response.status_code == 403
|
assert response.status_code == 403
|
||||||
|
|
||||||
def test_display(self):
|
def test_display(self):
|
||||||
"""
|
"""Test that a GET request return a page where the requested
|
||||||
Test that a GET request return a page where the requested
|
|
||||||
information are displayed.
|
information are displayed.
|
||||||
"""
|
"""
|
||||||
self.client.force_login(self.skia)
|
self.client.force_login(self.skia)
|
||||||
@ -251,9 +237,7 @@ class ClubModelTest(ClubTest):
|
|||||||
self.assertInHTML(expected_html, response.content.decode())
|
self.assertInHTML(expected_html, response.content.decode())
|
||||||
|
|
||||||
def test_root_add_one_club_member(self):
|
def test_root_add_one_club_member(self):
|
||||||
"""
|
"""Test that root users can add members to clubs, one at a time."""
|
||||||
Test that root users can add members to clubs, one at a time
|
|
||||||
"""
|
|
||||||
self.client.force_login(self.root)
|
self.client.force_login(self.root)
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
self.members_url,
|
self.members_url,
|
||||||
@ -264,9 +248,7 @@ class ClubModelTest(ClubTest):
|
|||||||
self.assert_membership_started_today(self.subscriber, role=3)
|
self.assert_membership_started_today(self.subscriber, role=3)
|
||||||
|
|
||||||
def test_root_add_multiple_club_member(self):
|
def test_root_add_multiple_club_member(self):
|
||||||
"""
|
"""Test that root users can add multiple members at once to clubs."""
|
||||||
Test that root users can add multiple members at once to clubs
|
|
||||||
"""
|
|
||||||
self.client.force_login(self.root)
|
self.client.force_login(self.root)
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
self.members_url,
|
self.members_url,
|
||||||
@ -281,8 +263,7 @@ class ClubModelTest(ClubTest):
|
|||||||
self.assert_membership_started_today(self.krophil, role=3)
|
self.assert_membership_started_today(self.krophil, role=3)
|
||||||
|
|
||||||
def test_add_unauthorized_members(self):
|
def test_add_unauthorized_members(self):
|
||||||
"""
|
"""Test that users who are not currently subscribed
|
||||||
Test that users who are not currently subscribed
|
|
||||||
cannot be members of clubs.
|
cannot be members of clubs.
|
||||||
"""
|
"""
|
||||||
self.client.force_login(self.root)
|
self.client.force_login(self.root)
|
||||||
@ -302,9 +283,8 @@ class ClubModelTest(ClubTest):
|
|||||||
assert '<ul class="errorlist"><li>' in response.content.decode()
|
assert '<ul class="errorlist"><li>' in response.content.decode()
|
||||||
|
|
||||||
def test_add_members_already_members(self):
|
def test_add_members_already_members(self):
|
||||||
"""
|
"""Test that users who are already members of a club
|
||||||
Test that users who are already members of a club
|
cannot be added again to this club.
|
||||||
cannot be added again to this club
|
|
||||||
"""
|
"""
|
||||||
self.client.force_login(self.root)
|
self.client.force_login(self.root)
|
||||||
current_membership = self.skia.memberships.ongoing().get(club=self.club)
|
current_membership = self.skia.memberships.ongoing().get(club=self.club)
|
||||||
@ -320,8 +300,7 @@ class ClubModelTest(ClubTest):
|
|||||||
assert self.club.get_membership_for(self.skia) == new_membership
|
assert self.club.get_membership_for(self.skia) == new_membership
|
||||||
|
|
||||||
def test_add_not_existing_users(self):
|
def test_add_not_existing_users(self):
|
||||||
"""
|
"""Test that not existing users cannot be added in clubs.
|
||||||
Test that not existing users cannot be added in clubs.
|
|
||||||
If one user in the request is invalid, no membership creation at all
|
If one user in the request is invalid, no membership creation at all
|
||||||
can take place.
|
can take place.
|
||||||
"""
|
"""
|
||||||
@ -349,9 +328,7 @@ class ClubModelTest(ClubTest):
|
|||||||
assert self.club.members.count() == nb_memberships
|
assert self.club.members.count() == nb_memberships
|
||||||
|
|
||||||
def test_president_add_members(self):
|
def test_president_add_members(self):
|
||||||
"""
|
"""Test that the president of the club can add members."""
|
||||||
Test that the president of the club can add members
|
|
||||||
"""
|
|
||||||
president = self.club.members.get(role=10).user
|
president = self.club.members.get(role=10).user
|
||||||
nb_club_membership = self.club.members.count()
|
nb_club_membership = self.club.members.count()
|
||||||
nb_subscriber_memberships = self.subscriber.memberships.count()
|
nb_subscriber_memberships = self.subscriber.memberships.count()
|
||||||
@ -368,8 +345,7 @@ class ClubModelTest(ClubTest):
|
|||||||
self.assert_membership_started_today(self.subscriber, role=9)
|
self.assert_membership_started_today(self.subscriber, role=9)
|
||||||
|
|
||||||
def test_add_member_greater_role(self):
|
def test_add_member_greater_role(self):
|
||||||
"""
|
"""Test that a member of the club member cannot create
|
||||||
Test that a member of the club member cannot create
|
|
||||||
a membership with a greater role than its own.
|
a membership with a greater role than its own.
|
||||||
"""
|
"""
|
||||||
self.client.force_login(self.skia)
|
self.client.force_login(self.skia)
|
||||||
@ -388,9 +364,7 @@ class ClubModelTest(ClubTest):
|
|||||||
assert not self.subscriber.memberships.filter(club=self.club).exists()
|
assert not self.subscriber.memberships.filter(club=self.club).exists()
|
||||||
|
|
||||||
def test_add_member_without_role(self):
|
def test_add_member_without_role(self):
|
||||||
"""
|
"""Test that trying to add members without specifying their role fails."""
|
||||||
Test that trying to add members without specifying their role fails
|
|
||||||
"""
|
|
||||||
self.client.force_login(self.root)
|
self.client.force_login(self.root)
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
self.members_url,
|
self.members_url,
|
||||||
@ -402,9 +376,7 @@ class ClubModelTest(ClubTest):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def test_end_membership_self(self):
|
def test_end_membership_self(self):
|
||||||
"""
|
"""Test that a member can end its own membership."""
|
||||||
Test that a member can end its own membership
|
|
||||||
"""
|
|
||||||
self.client.force_login(self.skia)
|
self.client.force_login(self.skia)
|
||||||
self.client.post(
|
self.client.post(
|
||||||
self.members_url,
|
self.members_url,
|
||||||
@ -414,9 +386,8 @@ class ClubModelTest(ClubTest):
|
|||||||
self.assert_membership_ended_today(self.skia)
|
self.assert_membership_ended_today(self.skia)
|
||||||
|
|
||||||
def test_end_membership_lower_role(self):
|
def test_end_membership_lower_role(self):
|
||||||
"""
|
"""Test that board members of the club can end memberships
|
||||||
Test that board members of the club can end memberships
|
of users with lower roles.
|
||||||
of users with lower roles
|
|
||||||
"""
|
"""
|
||||||
# remainder : skia has role 3, comptable has role 10, richard has role 1
|
# remainder : skia has role 3, comptable has role 10, richard has role 1
|
||||||
self.client.force_login(self.skia)
|
self.client.force_login(self.skia)
|
||||||
@ -429,9 +400,8 @@ class ClubModelTest(ClubTest):
|
|||||||
self.assert_membership_ended_today(self.richard)
|
self.assert_membership_ended_today(self.richard)
|
||||||
|
|
||||||
def test_end_membership_higher_role(self):
|
def test_end_membership_higher_role(self):
|
||||||
"""
|
"""Test that board members of the club cannot end memberships
|
||||||
Test that board members of the club cannot end memberships
|
of users with higher roles.
|
||||||
of users with higher roles
|
|
||||||
"""
|
"""
|
||||||
membership = self.comptable.memberships.filter(club=self.club).first()
|
membership = self.comptable.memberships.filter(club=self.club).first()
|
||||||
self.client.force_login(self.skia)
|
self.client.force_login(self.skia)
|
||||||
@ -448,9 +418,8 @@ class ClubModelTest(ClubTest):
|
|||||||
assert membership.end_date is None
|
assert membership.end_date is None
|
||||||
|
|
||||||
def test_end_membership_as_main_club_board(self):
|
def test_end_membership_as_main_club_board(self):
|
||||||
"""
|
"""Test that board members of the main club can end the membership
|
||||||
Test that board members of the main club can end the membership
|
of anyone.
|
||||||
of anyone
|
|
||||||
"""
|
"""
|
||||||
# make subscriber a board member
|
# make subscriber a board member
|
||||||
self.subscriber.memberships.all().delete()
|
self.subscriber.memberships.all().delete()
|
||||||
@ -467,9 +436,7 @@ class ClubModelTest(ClubTest):
|
|||||||
assert self.club.members.ongoing().count() == nb_memberships - 1
|
assert self.club.members.ongoing().count() == nb_memberships - 1
|
||||||
|
|
||||||
def test_end_membership_as_root(self):
|
def test_end_membership_as_root(self):
|
||||||
"""
|
"""Test that root users can end the membership of anyone."""
|
||||||
Test that root users can end the membership of anyone
|
|
||||||
"""
|
|
||||||
nb_memberships = self.club.members.count()
|
nb_memberships = self.club.members.count()
|
||||||
self.client.force_login(self.root)
|
self.client.force_login(self.root)
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
@ -482,9 +449,7 @@ class ClubModelTest(ClubTest):
|
|||||||
assert self.club.members.count() == nb_memberships
|
assert self.club.members.count() == nb_memberships
|
||||||
|
|
||||||
def test_end_membership_as_foreigner(self):
|
def test_end_membership_as_foreigner(self):
|
||||||
"""
|
"""Test that users who are not in this club cannot end its memberships."""
|
||||||
Test that users who are not in this club cannot end its memberships
|
|
||||||
"""
|
|
||||||
nb_memberships = self.club.members.count()
|
nb_memberships = self.club.members.count()
|
||||||
membership = self.richard.memberships.filter(club=self.club).first()
|
membership = self.richard.memberships.filter(club=self.club).first()
|
||||||
self.client.force_login(self.subscriber)
|
self.client.force_login(self.subscriber)
|
||||||
@ -498,9 +463,8 @@ class ClubModelTest(ClubTest):
|
|||||||
assert membership == new_mem
|
assert membership == new_mem
|
||||||
|
|
||||||
def test_delete_remove_from_meta_group(self):
|
def test_delete_remove_from_meta_group(self):
|
||||||
"""
|
"""Test that when a club is deleted, all its members are removed from the
|
||||||
Test that when a club is deleted, all its members are removed from the
|
associated metagroup.
|
||||||
associated metagroup
|
|
||||||
"""
|
"""
|
||||||
memberships = self.club.members.select_related("user")
|
memberships = self.club.members.select_related("user")
|
||||||
users = [membership.user for membership in memberships]
|
users = [membership.user for membership in memberships]
|
||||||
@ -511,9 +475,7 @@ class ClubModelTest(ClubTest):
|
|||||||
assert not user.is_in_group(name=meta_group)
|
assert not user.is_in_group(name=meta_group)
|
||||||
|
|
||||||
def test_add_to_meta_group(self):
|
def test_add_to_meta_group(self):
|
||||||
"""
|
"""Test that when a membership begins, the user is added to the meta group."""
|
||||||
Test that when a membership begins, the user is added to the meta group
|
|
||||||
"""
|
|
||||||
group_members = self.club.unix_name + settings.SITH_MEMBER_SUFFIX
|
group_members = self.club.unix_name + settings.SITH_MEMBER_SUFFIX
|
||||||
board_members = self.club.unix_name + settings.SITH_BOARD_SUFFIX
|
board_members = self.club.unix_name + settings.SITH_BOARD_SUFFIX
|
||||||
assert not self.subscriber.is_in_group(name=group_members)
|
assert not self.subscriber.is_in_group(name=group_members)
|
||||||
@ -523,9 +485,7 @@ class ClubModelTest(ClubTest):
|
|||||||
assert self.subscriber.is_in_group(name=board_members)
|
assert self.subscriber.is_in_group(name=board_members)
|
||||||
|
|
||||||
def test_remove_from_meta_group(self):
|
def test_remove_from_meta_group(self):
|
||||||
"""
|
"""Test that when a membership ends, the user is removed from meta group."""
|
||||||
Test that when a membership ends, the user is removed from meta group
|
|
||||||
"""
|
|
||||||
group_members = self.club.unix_name + settings.SITH_MEMBER_SUFFIX
|
group_members = self.club.unix_name + settings.SITH_MEMBER_SUFFIX
|
||||||
board_members = self.club.unix_name + settings.SITH_BOARD_SUFFIX
|
board_members = self.club.unix_name + settings.SITH_BOARD_SUFFIX
|
||||||
assert self.comptable.is_in_group(name=group_members)
|
assert self.comptable.is_in_group(name=group_members)
|
||||||
@ -535,9 +495,7 @@ class ClubModelTest(ClubTest):
|
|||||||
assert not self.comptable.is_in_group(name=board_members)
|
assert not self.comptable.is_in_group(name=board_members)
|
||||||
|
|
||||||
def test_club_owner(self):
|
def test_club_owner(self):
|
||||||
"""
|
"""Test that a club is owned only by board members of the main club."""
|
||||||
Test that a club is owned only by board members of the main club
|
|
||||||
"""
|
|
||||||
anonymous = AnonymousUser()
|
anonymous = AnonymousUser()
|
||||||
assert not self.club.is_owned_by(anonymous)
|
assert not self.club.is_owned_by(anonymous)
|
||||||
assert not self.club.is_owned_by(self.subscriber)
|
assert not self.club.is_owned_by(self.subscriber)
|
||||||
@ -548,8 +506,8 @@ class ClubModelTest(ClubTest):
|
|||||||
assert self.club.is_owned_by(self.sli)
|
assert self.club.is_owned_by(self.sli)
|
||||||
|
|
||||||
|
|
||||||
class MailingFormTest(TestCase):
|
class TestMailingForm(TestCase):
|
||||||
"""Perform validation tests for MailingForm"""
|
"""Perform validation tests for MailingForm."""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
@ -864,10 +822,8 @@ class MailingFormTest(TestCase):
|
|||||||
assert "krophil@git.an" not in content
|
assert "krophil@git.an" not in content
|
||||||
|
|
||||||
|
|
||||||
class ClubSellingViewTest(TestCase):
|
class TestClubSellingView(TestCase):
|
||||||
"""
|
"""Perform basics tests to ensure that the page is available."""
|
||||||
Perform basics tests to ensure that the page is available
|
|
||||||
"""
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
@ -875,9 +831,7 @@ class ClubSellingViewTest(TestCase):
|
|||||||
cls.skia = User.objects.get(username="skia")
|
cls.skia = User.objects.get(username="skia")
|
||||||
|
|
||||||
def test_page_not_internal_error(self):
|
def test_page_not_internal_error(self):
|
||||||
"""
|
"""Test that the page does not return and internal error."""
|
||||||
Test that the page does not return and internal error
|
|
||||||
"""
|
|
||||||
self.client.force_login(self.skia)
|
self.client.force_login(self.skia)
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
reverse("club:club_sellings", kwargs={"club_id": self.ae.id})
|
reverse("club:club_sellings", kwargs={"club_id": self.ae.id})
|
||||||
|
@ -175,18 +175,14 @@ class ClubTabsMixin(TabedViewMixin):
|
|||||||
|
|
||||||
|
|
||||||
class ClubListView(ListView):
|
class ClubListView(ListView):
|
||||||
"""
|
"""List the Clubs."""
|
||||||
List the Clubs
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = Club
|
model = Club
|
||||||
template_name = "club/club_list.jinja"
|
template_name = "club/club_list.jinja"
|
||||||
|
|
||||||
|
|
||||||
class ClubView(ClubTabsMixin, DetailView):
|
class ClubView(ClubTabsMixin, DetailView):
|
||||||
"""
|
"""Front page of a Club."""
|
||||||
Front page of a Club
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = Club
|
model = Club
|
||||||
pk_url_kwarg = "club_id"
|
pk_url_kwarg = "club_id"
|
||||||
@ -201,9 +197,7 @@ class ClubView(ClubTabsMixin, DetailView):
|
|||||||
|
|
||||||
|
|
||||||
class ClubRevView(ClubView):
|
class ClubRevView(ClubView):
|
||||||
"""
|
"""Display a specific page revision."""
|
||||||
Display a specific page revision
|
|
||||||
"""
|
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
obj = self.get_object()
|
obj = self.get_object()
|
||||||
@ -235,9 +229,7 @@ class ClubPageEditView(ClubTabsMixin, PageEditViewBase):
|
|||||||
|
|
||||||
|
|
||||||
class ClubPageHistView(ClubTabsMixin, CanViewMixin, DetailView):
|
class ClubPageHistView(ClubTabsMixin, CanViewMixin, DetailView):
|
||||||
"""
|
"""Modification hostory of the page."""
|
||||||
Modification hostory of the page
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = Club
|
model = Club
|
||||||
pk_url_kwarg = "club_id"
|
pk_url_kwarg = "club_id"
|
||||||
@ -246,9 +238,7 @@ class ClubPageHistView(ClubTabsMixin, CanViewMixin, DetailView):
|
|||||||
|
|
||||||
|
|
||||||
class ClubToolsView(ClubTabsMixin, CanEditMixin, DetailView):
|
class ClubToolsView(ClubTabsMixin, CanEditMixin, DetailView):
|
||||||
"""
|
"""Tools page of a Club."""
|
||||||
Tools page of a Club
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = Club
|
model = Club
|
||||||
pk_url_kwarg = "club_id"
|
pk_url_kwarg = "club_id"
|
||||||
@ -257,9 +247,7 @@ class ClubToolsView(ClubTabsMixin, CanEditMixin, DetailView):
|
|||||||
|
|
||||||
|
|
||||||
class ClubMembersView(ClubTabsMixin, CanViewMixin, DetailFormView):
|
class ClubMembersView(ClubTabsMixin, CanViewMixin, DetailFormView):
|
||||||
"""
|
"""View of a club's members."""
|
||||||
View of a club's members
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = Club
|
model = Club
|
||||||
pk_url_kwarg = "club_id"
|
pk_url_kwarg = "club_id"
|
||||||
@ -280,9 +268,7 @@ class ClubMembersView(ClubTabsMixin, CanViewMixin, DetailFormView):
|
|||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
"""
|
"""Check user rights."""
|
||||||
Check user rights
|
|
||||||
"""
|
|
||||||
resp = super().form_valid(form)
|
resp = super().form_valid(form)
|
||||||
|
|
||||||
data = form.clean()
|
data = form.clean()
|
||||||
@ -307,9 +293,7 @@ class ClubMembersView(ClubTabsMixin, CanViewMixin, DetailFormView):
|
|||||||
|
|
||||||
|
|
||||||
class ClubOldMembersView(ClubTabsMixin, CanViewMixin, DetailView):
|
class ClubOldMembersView(ClubTabsMixin, CanViewMixin, DetailView):
|
||||||
"""
|
"""Old members of a club."""
|
||||||
Old members of a club
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = Club
|
model = Club
|
||||||
pk_url_kwarg = "club_id"
|
pk_url_kwarg = "club_id"
|
||||||
@ -318,9 +302,7 @@ class ClubOldMembersView(ClubTabsMixin, CanViewMixin, DetailView):
|
|||||||
|
|
||||||
|
|
||||||
class ClubSellingView(ClubTabsMixin, CanEditMixin, DetailFormView):
|
class ClubSellingView(ClubTabsMixin, CanEditMixin, DetailFormView):
|
||||||
"""
|
"""Sellings of a club."""
|
||||||
Sellings of a club
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = Club
|
model = Club
|
||||||
pk_url_kwarg = "club_id"
|
pk_url_kwarg = "club_id"
|
||||||
@ -396,12 +378,10 @@ class ClubSellingView(ClubTabsMixin, CanEditMixin, DetailFormView):
|
|||||||
|
|
||||||
|
|
||||||
class ClubSellingCSVView(ClubSellingView):
|
class ClubSellingCSVView(ClubSellingView):
|
||||||
"""
|
"""Generate sellings in csv for a given period."""
|
||||||
Generate sellings in csv for a given period
|
|
||||||
"""
|
|
||||||
|
|
||||||
class StreamWriter:
|
class StreamWriter:
|
||||||
"""Implements a file-like interface for streaming the CSV"""
|
"""Implements a file-like interface for streaming the CSV."""
|
||||||
|
|
||||||
def write(self, value):
|
def write(self, value):
|
||||||
"""Write the value by returning it, instead of storing in a buffer."""
|
"""Write the value by returning it, instead of storing in a buffer."""
|
||||||
@ -475,9 +455,7 @@ class ClubSellingCSVView(ClubSellingView):
|
|||||||
|
|
||||||
|
|
||||||
class ClubEditView(ClubTabsMixin, CanEditMixin, UpdateView):
|
class ClubEditView(ClubTabsMixin, CanEditMixin, UpdateView):
|
||||||
"""
|
"""Edit a Club's main informations (for the club's members)."""
|
||||||
Edit a Club's main informations (for the club's members)
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = Club
|
model = Club
|
||||||
pk_url_kwarg = "club_id"
|
pk_url_kwarg = "club_id"
|
||||||
@ -487,9 +465,7 @@ class ClubEditView(ClubTabsMixin, CanEditMixin, UpdateView):
|
|||||||
|
|
||||||
|
|
||||||
class ClubEditPropView(ClubTabsMixin, CanEditPropMixin, UpdateView):
|
class ClubEditPropView(ClubTabsMixin, CanEditPropMixin, UpdateView):
|
||||||
"""
|
"""Edit the properties of a Club object (for the Sith admins)."""
|
||||||
Edit the properties of a Club object (for the Sith admins)
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = Club
|
model = Club
|
||||||
pk_url_kwarg = "club_id"
|
pk_url_kwarg = "club_id"
|
||||||
@ -499,9 +475,7 @@ class ClubEditPropView(ClubTabsMixin, CanEditPropMixin, UpdateView):
|
|||||||
|
|
||||||
|
|
||||||
class ClubCreateView(CanCreateMixin, CreateView):
|
class ClubCreateView(CanCreateMixin, CreateView):
|
||||||
"""
|
"""Create a club (for the Sith admin)."""
|
||||||
Create a club (for the Sith admin)
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = Club
|
model = Club
|
||||||
pk_url_kwarg = "club_id"
|
pk_url_kwarg = "club_id"
|
||||||
@ -510,9 +484,7 @@ class ClubCreateView(CanCreateMixin, CreateView):
|
|||||||
|
|
||||||
|
|
||||||
class MembershipSetOldView(CanEditMixin, DetailView):
|
class MembershipSetOldView(CanEditMixin, DetailView):
|
||||||
"""
|
"""Set a membership as beeing old."""
|
||||||
Set a membership as beeing old
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = Membership
|
model = Membership
|
||||||
pk_url_kwarg = "membership_id"
|
pk_url_kwarg = "membership_id"
|
||||||
@ -541,9 +513,7 @@ class MembershipSetOldView(CanEditMixin, DetailView):
|
|||||||
|
|
||||||
|
|
||||||
class MembershipDeleteView(UserIsRootMixin, DeleteView):
|
class MembershipDeleteView(UserIsRootMixin, DeleteView):
|
||||||
"""
|
"""Delete a membership (for admins only)."""
|
||||||
Delete a membership (for admins only)
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = Membership
|
model = Membership
|
||||||
pk_url_kwarg = "membership_id"
|
pk_url_kwarg = "membership_id"
|
||||||
@ -563,9 +533,7 @@ class ClubStatView(TemplateView):
|
|||||||
|
|
||||||
|
|
||||||
class ClubMailingView(ClubTabsMixin, CanEditMixin, DetailFormView):
|
class ClubMailingView(ClubTabsMixin, CanEditMixin, DetailFormView):
|
||||||
"""
|
"""A list of mailing for a given club."""
|
||||||
A list of mailing for a given club
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = Club
|
model = Club
|
||||||
form_class = MailingForm
|
form_class = MailingForm
|
||||||
@ -603,9 +571,7 @@ class ClubMailingView(ClubTabsMixin, CanEditMixin, DetailFormView):
|
|||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
def add_new_mailing(self, cleaned_data) -> ValidationError | None:
|
def add_new_mailing(self, cleaned_data) -> ValidationError | None:
|
||||||
"""
|
"""Create a new mailing list from the form."""
|
||||||
Create a new mailing list from the form
|
|
||||||
"""
|
|
||||||
mailing = Mailing(
|
mailing = Mailing(
|
||||||
club=self.get_object(),
|
club=self.get_object(),
|
||||||
email=cleaned_data["mailing_email"],
|
email=cleaned_data["mailing_email"],
|
||||||
@ -620,9 +586,7 @@ class ClubMailingView(ClubTabsMixin, CanEditMixin, DetailFormView):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def add_new_subscription(self, cleaned_data) -> ValidationError | None:
|
def add_new_subscription(self, cleaned_data) -> ValidationError | None:
|
||||||
"""
|
"""Add mailing subscriptions for each user given and/or for the specified email in form."""
|
||||||
Add mailing subscriptions for each user given and/or for the specified email in form
|
|
||||||
"""
|
|
||||||
users_to_save = []
|
users_to_save = []
|
||||||
|
|
||||||
for user in cleaned_data["subscription_users"]:
|
for user in cleaned_data["subscription_users"]:
|
||||||
@ -656,9 +620,7 @@ class ClubMailingView(ClubTabsMixin, CanEditMixin, DetailFormView):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def remove_subscription(self, cleaned_data):
|
def remove_subscription(self, cleaned_data):
|
||||||
"""
|
"""Remove specified users from a mailing list."""
|
||||||
Remove specified users from a mailing list
|
|
||||||
"""
|
|
||||||
fields = [
|
fields = [
|
||||||
cleaned_data[key]
|
cleaned_data[key]
|
||||||
for key in cleaned_data.keys()
|
for key in cleaned_data.keys()
|
||||||
@ -742,7 +704,7 @@ class MailingAutoGenerationView(View):
|
|||||||
|
|
||||||
|
|
||||||
class PosterListView(ClubTabsMixin, PosterListBaseView, CanViewMixin):
|
class PosterListView(ClubTabsMixin, PosterListBaseView, CanViewMixin):
|
||||||
"""List communication posters"""
|
"""List communication posters."""
|
||||||
|
|
||||||
def get_object(self):
|
def get_object(self):
|
||||||
return self.club
|
return self.club
|
||||||
@ -755,7 +717,7 @@ class PosterListView(ClubTabsMixin, PosterListBaseView, CanViewMixin):
|
|||||||
|
|
||||||
|
|
||||||
class PosterCreateView(PosterCreateBaseView, CanCreateMixin):
|
class PosterCreateView(PosterCreateBaseView, CanCreateMixin):
|
||||||
"""Create communication poster"""
|
"""Create communication poster."""
|
||||||
|
|
||||||
pk_url_kwarg = "club_id"
|
pk_url_kwarg = "club_id"
|
||||||
|
|
||||||
@ -770,7 +732,7 @@ class PosterCreateView(PosterCreateBaseView, CanCreateMixin):
|
|||||||
|
|
||||||
|
|
||||||
class PosterEditView(ClubTabsMixin, PosterEditBaseView, CanEditMixin):
|
class PosterEditView(ClubTabsMixin, PosterEditBaseView, CanEditMixin):
|
||||||
"""Edit communication poster"""
|
"""Edit communication poster."""
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
return reverse_lazy("club:poster_list", kwargs={"club_id": self.club.id})
|
return reverse_lazy("club:poster_list", kwargs={"club_id": self.club.id})
|
||||||
@ -782,7 +744,7 @@ class PosterEditView(ClubTabsMixin, PosterEditBaseView, CanEditMixin):
|
|||||||
|
|
||||||
|
|
||||||
class PosterDeleteView(PosterDeleteBaseView, ClubTabsMixin, CanEditMixin):
|
class PosterDeleteView(PosterDeleteBaseView, ClubTabsMixin, CanEditMixin):
|
||||||
"""Delete communication poster"""
|
"""Delete communication poster."""
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
return reverse_lazy("club:poster_list", kwargs={"club_id": self.club.id})
|
return reverse_lazy("club:poster_list", kwargs={"club_id": self.club.id})
|
||||||
|
@ -39,7 +39,7 @@ from core.models import Notification, Preferences, RealGroup, User
|
|||||||
|
|
||||||
|
|
||||||
class Sith(models.Model):
|
class Sith(models.Model):
|
||||||
"""A one instance class storing all the modifiable infos"""
|
"""A one instance class storing all the modifiable infos."""
|
||||||
|
|
||||||
alert_msg = models.TextField(_("alert message"), default="", blank=True)
|
alert_msg = models.TextField(_("alert message"), default="", blank=True)
|
||||||
info_msg = models.TextField(_("info message"), default="", blank=True)
|
info_msg = models.TextField(_("info message"), default="", blank=True)
|
||||||
@ -64,7 +64,7 @@ NEWS_TYPES = [
|
|||||||
|
|
||||||
|
|
||||||
class News(models.Model):
|
class News(models.Model):
|
||||||
"""The news class"""
|
"""The news class."""
|
||||||
|
|
||||||
title = models.CharField(_("title"), max_length=64)
|
title = models.CharField(_("title"), max_length=64)
|
||||||
summary = models.TextField(_("summary"))
|
summary = models.TextField(_("summary"))
|
||||||
@ -143,8 +143,7 @@ def news_notification_callback(notif):
|
|||||||
|
|
||||||
|
|
||||||
class NewsDate(models.Model):
|
class NewsDate(models.Model):
|
||||||
"""
|
"""A date class, useful for weekly events, or for events that just have no date.
|
||||||
A date class, useful for weekly events, or for events that just have no date
|
|
||||||
|
|
||||||
This class allows more flexibilty managing the dates related to a news, particularly when this news is weekly, since
|
This class allows more flexibilty managing the dates related to a news, particularly when this news is weekly, since
|
||||||
we don't have to make copies
|
we don't have to make copies
|
||||||
@ -164,8 +163,7 @@ class NewsDate(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
class Weekmail(models.Model):
|
class Weekmail(models.Model):
|
||||||
"""
|
"""The weekmail class.
|
||||||
The weekmail class
|
|
||||||
|
|
||||||
:ivar title: Title of the weekmail
|
:ivar title: Title of the weekmail
|
||||||
:ivar intro: Introduction of the weekmail
|
:ivar intro: Introduction of the weekmail
|
||||||
@ -189,8 +187,8 @@ class Weekmail(models.Model):
|
|||||||
return f"Weekmail {self.id} (sent: {self.sent}) - {self.title}"
|
return f"Weekmail {self.id} (sent: {self.sent}) - {self.title}"
|
||||||
|
|
||||||
def send(self):
|
def send(self):
|
||||||
"""
|
"""Send the weekmail to all users with the receive weekmail option opt-in.
|
||||||
Send the weekmail to all users with the receive weekmail option opt-in.
|
|
||||||
Also send the weekmail to the mailing list in settings.SITH_COM_EMAIL.
|
Also send the weekmail to the mailing list in settings.SITH_COM_EMAIL.
|
||||||
"""
|
"""
|
||||||
dest = [
|
dest = [
|
||||||
@ -214,33 +212,25 @@ class Weekmail(models.Model):
|
|||||||
Weekmail().save()
|
Weekmail().save()
|
||||||
|
|
||||||
def render_text(self):
|
def render_text(self):
|
||||||
"""
|
"""Renders a pure text version of the mail for readers without HTML support."""
|
||||||
Renders a pure text version of the mail for readers without HTML support.
|
|
||||||
"""
|
|
||||||
return render(
|
return render(
|
||||||
None, "com/weekmail_renderer_text.jinja", context={"weekmail": self}
|
None, "com/weekmail_renderer_text.jinja", context={"weekmail": self}
|
||||||
).content.decode("utf-8")
|
).content.decode("utf-8")
|
||||||
|
|
||||||
def render_html(self):
|
def render_html(self):
|
||||||
"""
|
"""Renders an HTML version of the mail with images and fancy CSS."""
|
||||||
Renders an HTML version of the mail with images and fancy CSS.
|
|
||||||
"""
|
|
||||||
return render(
|
return render(
|
||||||
None, "com/weekmail_renderer_html.jinja", context={"weekmail": self}
|
None, "com/weekmail_renderer_html.jinja", context={"weekmail": self}
|
||||||
).content.decode("utf-8")
|
).content.decode("utf-8")
|
||||||
|
|
||||||
def get_banner(self):
|
def get_banner(self):
|
||||||
"""
|
"""Return an absolute link to the banner."""
|
||||||
Return an absolute link to the banner.
|
|
||||||
"""
|
|
||||||
return (
|
return (
|
||||||
"http://" + settings.SITH_URL + static("com/img/weekmail_bannerV2P22.png")
|
"http://" + settings.SITH_URL + static("com/img/weekmail_bannerV2P22.png")
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_footer(self):
|
def get_footer(self):
|
||||||
"""
|
"""Return an absolute link to the footer."""
|
||||||
Return an absolute link to the footer.
|
|
||||||
"""
|
|
||||||
return "http://" + settings.SITH_URL + static("com/img/weekmail_footerP22.png")
|
return "http://" + settings.SITH_URL + static("com/img/weekmail_footerP22.png")
|
||||||
|
|
||||||
def is_owned_by(self, user):
|
def is_owned_by(self, user):
|
||||||
|
38
com/tests.py
38
com/tests.py
@ -45,7 +45,7 @@ def test_com_page_is_working(client, url, user_community):
|
|||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
class ComTest(TestCase):
|
class TestCom(TestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
cls.skia = User.objects.get(username="skia")
|
cls.skia = User.objects.get(username="skia")
|
||||||
@ -113,12 +113,9 @@ class ComTest(TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class SithTest(TestCase):
|
class TestSith(TestCase):
|
||||||
def test_sith_owner(self):
|
def test_sith_owner(self):
|
||||||
"""
|
"""Test that the sith instance is owned by com admins and nobody else."""
|
||||||
Test that the sith instance is owned by com admins
|
|
||||||
and nobody else
|
|
||||||
"""
|
|
||||||
sith: Sith = Sith.objects.first()
|
sith: Sith = Sith.objects.first()
|
||||||
|
|
||||||
com_admin = User.objects.get(username="comunity")
|
com_admin = User.objects.get(username="comunity")
|
||||||
@ -131,7 +128,7 @@ class SithTest(TestCase):
|
|||||||
assert not sith.is_owned_by(sli)
|
assert not sith.is_owned_by(sli)
|
||||||
|
|
||||||
|
|
||||||
class NewsTest(TestCase):
|
class TestNews(TestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
cls.com_admin = User.objects.get(username="comunity")
|
cls.com_admin = User.objects.get(username="comunity")
|
||||||
@ -148,20 +145,17 @@ class NewsTest(TestCase):
|
|||||||
cls.anonymous = AnonymousUser()
|
cls.anonymous = AnonymousUser()
|
||||||
|
|
||||||
def test_news_owner(self):
|
def test_news_owner(self):
|
||||||
|
"""Test that news are owned by com admins
|
||||||
|
or by their author but nobody else.
|
||||||
"""
|
"""
|
||||||
Test that news are owned by com admins
|
|
||||||
or by their author but nobody else
|
|
||||||
"""
|
|
||||||
|
|
||||||
assert self.new.is_owned_by(self.com_admin)
|
assert self.new.is_owned_by(self.com_admin)
|
||||||
assert self.new.is_owned_by(self.author)
|
assert self.new.is_owned_by(self.author)
|
||||||
assert not self.new.is_owned_by(self.anonymous)
|
assert not self.new.is_owned_by(self.anonymous)
|
||||||
assert not self.new.is_owned_by(self.sli)
|
assert not self.new.is_owned_by(self.sli)
|
||||||
|
|
||||||
def test_news_viewer(self):
|
def test_news_viewer(self):
|
||||||
"""
|
"""Test that moderated news can be viewed by anyone
|
||||||
Test that moderated news can be viewed by anyone
|
and not moderated news only by com admins.
|
||||||
and not moderated news only by com admins
|
|
||||||
"""
|
"""
|
||||||
# by default a news isn't moderated
|
# by default a news isn't moderated
|
||||||
assert self.new.can_be_viewed_by(self.com_admin)
|
assert self.new.can_be_viewed_by(self.com_admin)
|
||||||
@ -177,16 +171,14 @@ class NewsTest(TestCase):
|
|||||||
assert self.new.can_be_viewed_by(self.author)
|
assert self.new.can_be_viewed_by(self.author)
|
||||||
|
|
||||||
def test_news_editor(self):
|
def test_news_editor(self):
|
||||||
"""
|
"""Test that only com admins can edit news."""
|
||||||
Test that only com admins can edit news
|
|
||||||
"""
|
|
||||||
assert self.new.can_be_edited_by(self.com_admin)
|
assert self.new.can_be_edited_by(self.com_admin)
|
||||||
assert not self.new.can_be_edited_by(self.sli)
|
assert not self.new.can_be_edited_by(self.sli)
|
||||||
assert not self.new.can_be_edited_by(self.anonymous)
|
assert not self.new.can_be_edited_by(self.anonymous)
|
||||||
assert not self.new.can_be_edited_by(self.author)
|
assert not self.new.can_be_edited_by(self.author)
|
||||||
|
|
||||||
|
|
||||||
class WeekmailArticleTest(TestCase):
|
class TestWeekmailArticle(TestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
cls.com_admin = User.objects.get(username="comunity")
|
cls.com_admin = User.objects.get(username="comunity")
|
||||||
@ -203,16 +195,14 @@ class WeekmailArticleTest(TestCase):
|
|||||||
cls.anonymous = AnonymousUser()
|
cls.anonymous = AnonymousUser()
|
||||||
|
|
||||||
def test_weekmail_owner(self):
|
def test_weekmail_owner(self):
|
||||||
"""
|
"""Test that weekmails are owned only by com admins."""
|
||||||
Test that weekmails are owned only by com admins
|
|
||||||
"""
|
|
||||||
assert self.article.is_owned_by(self.com_admin)
|
assert self.article.is_owned_by(self.com_admin)
|
||||||
assert not self.article.is_owned_by(self.author)
|
assert not self.article.is_owned_by(self.author)
|
||||||
assert not self.article.is_owned_by(self.anonymous)
|
assert not self.article.is_owned_by(self.anonymous)
|
||||||
assert not self.article.is_owned_by(self.sli)
|
assert not self.article.is_owned_by(self.sli)
|
||||||
|
|
||||||
|
|
||||||
class PosterTest(TestCase):
|
class TestPoster(TestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
cls.com_admin = User.objects.get(username="comunity")
|
cls.com_admin = User.objects.get(username="comunity")
|
||||||
@ -229,9 +219,7 @@ class PosterTest(TestCase):
|
|||||||
cls.anonymous = AnonymousUser()
|
cls.anonymous = AnonymousUser()
|
||||||
|
|
||||||
def test_poster_owner(self):
|
def test_poster_owner(self):
|
||||||
"""
|
"""Test that poster are owned by com admins and board members in clubs."""
|
||||||
Test that poster are owned by com admins and board members in clubs
|
|
||||||
"""
|
|
||||||
assert self.poster.is_owned_by(self.com_admin)
|
assert self.poster.is_owned_by(self.com_admin)
|
||||||
assert not self.poster.is_owned_by(self.anonymous)
|
assert not self.poster.is_owned_by(self.anonymous)
|
||||||
|
|
||||||
|
40
com/views.py
40
com/views.py
@ -427,7 +427,7 @@ class WeekmailPreviewView(ComTabsMixin, QuickNotifMixin, CanEditPropMixin, Detai
|
|||||||
return self.model.objects.filter(sent=False).order_by("-id").first()
|
return self.model.objects.filter(sent=False).order_by("-id").first()
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
"""Add rendered weekmail"""
|
"""Add rendered weekmail."""
|
||||||
kwargs = super().get_context_data(**kwargs)
|
kwargs = super().get_context_data(**kwargs)
|
||||||
kwargs["weekmail_rendered"] = self.object.render_html()
|
kwargs["weekmail_rendered"] = self.object.render_html()
|
||||||
kwargs["bad_recipients"] = self.bad_recipients
|
kwargs["bad_recipients"] = self.bad_recipients
|
||||||
@ -507,7 +507,7 @@ class WeekmailEditView(ComTabsMixin, QuickNotifMixin, CanEditPropMixin, UpdateVi
|
|||||||
return super().get(request, *args, **kwargs)
|
return super().get(request, *args, **kwargs)
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
"""Add orphan articles"""
|
"""Add orphan articles."""
|
||||||
kwargs = super().get_context_data(**kwargs)
|
kwargs = super().get_context_data(**kwargs)
|
||||||
kwargs["orphans"] = WeekmailArticle.objects.filter(weekmail=None)
|
kwargs["orphans"] = WeekmailArticle.objects.filter(weekmail=None)
|
||||||
return kwargs
|
return kwargs
|
||||||
@ -516,7 +516,7 @@ class WeekmailEditView(ComTabsMixin, QuickNotifMixin, CanEditPropMixin, UpdateVi
|
|||||||
class WeekmailArticleEditView(
|
class WeekmailArticleEditView(
|
||||||
ComTabsMixin, QuickNotifMixin, CanEditPropMixin, UpdateView
|
ComTabsMixin, QuickNotifMixin, CanEditPropMixin, UpdateView
|
||||||
):
|
):
|
||||||
"""Edit an article"""
|
"""Edit an article."""
|
||||||
|
|
||||||
model = WeekmailArticle
|
model = WeekmailArticle
|
||||||
form_class = modelform_factory(
|
form_class = modelform_factory(
|
||||||
@ -532,7 +532,7 @@ class WeekmailArticleEditView(
|
|||||||
|
|
||||||
|
|
||||||
class WeekmailArticleCreateView(QuickNotifMixin, CreateView):
|
class WeekmailArticleCreateView(QuickNotifMixin, CreateView):
|
||||||
"""Post an article"""
|
"""Post an article."""
|
||||||
|
|
||||||
model = WeekmailArticle
|
model = WeekmailArticle
|
||||||
form_class = modelform_factory(
|
form_class = modelform_factory(
|
||||||
@ -574,7 +574,7 @@ class WeekmailArticleCreateView(QuickNotifMixin, CreateView):
|
|||||||
|
|
||||||
|
|
||||||
class WeekmailArticleDeleteView(CanEditPropMixin, DeleteView):
|
class WeekmailArticleDeleteView(CanEditPropMixin, DeleteView):
|
||||||
"""Delete an article"""
|
"""Delete an article."""
|
||||||
|
|
||||||
model = WeekmailArticle
|
model = WeekmailArticle
|
||||||
template_name = "core/delete_confirm.jinja"
|
template_name = "core/delete_confirm.jinja"
|
||||||
@ -614,7 +614,7 @@ class MailingModerateView(View):
|
|||||||
|
|
||||||
|
|
||||||
class PosterListBaseView(ListView):
|
class PosterListBaseView(ListView):
|
||||||
"""List communication posters"""
|
"""List communication posters."""
|
||||||
|
|
||||||
current_tab = "posters"
|
current_tab = "posters"
|
||||||
model = Poster
|
model = Poster
|
||||||
@ -641,7 +641,7 @@ class PosterListBaseView(ListView):
|
|||||||
|
|
||||||
|
|
||||||
class PosterCreateBaseView(CreateView):
|
class PosterCreateBaseView(CreateView):
|
||||||
"""Create communication poster"""
|
"""Create communication poster."""
|
||||||
|
|
||||||
current_tab = "posters"
|
current_tab = "posters"
|
||||||
form_class = PosterForm
|
form_class = PosterForm
|
||||||
@ -673,7 +673,7 @@ class PosterCreateBaseView(CreateView):
|
|||||||
|
|
||||||
|
|
||||||
class PosterEditBaseView(UpdateView):
|
class PosterEditBaseView(UpdateView):
|
||||||
"""Edit communication poster"""
|
"""Edit communication poster."""
|
||||||
|
|
||||||
pk_url_kwarg = "poster_id"
|
pk_url_kwarg = "poster_id"
|
||||||
current_tab = "posters"
|
current_tab = "posters"
|
||||||
@ -721,7 +721,7 @@ class PosterEditBaseView(UpdateView):
|
|||||||
|
|
||||||
|
|
||||||
class PosterDeleteBaseView(DeleteView):
|
class PosterDeleteBaseView(DeleteView):
|
||||||
"""Edit communication poster"""
|
"""Edit communication poster."""
|
||||||
|
|
||||||
pk_url_kwarg = "poster_id"
|
pk_url_kwarg = "poster_id"
|
||||||
current_tab = "posters"
|
current_tab = "posters"
|
||||||
@ -738,7 +738,7 @@ class PosterDeleteBaseView(DeleteView):
|
|||||||
|
|
||||||
|
|
||||||
class PosterListView(IsComAdminMixin, ComTabsMixin, PosterListBaseView):
|
class PosterListView(IsComAdminMixin, ComTabsMixin, PosterListBaseView):
|
||||||
"""List communication posters"""
|
"""List communication posters."""
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
kwargs = super().get_context_data(**kwargs)
|
kwargs = super().get_context_data(**kwargs)
|
||||||
@ -747,7 +747,7 @@ class PosterListView(IsComAdminMixin, ComTabsMixin, PosterListBaseView):
|
|||||||
|
|
||||||
|
|
||||||
class PosterCreateView(IsComAdminMixin, ComTabsMixin, PosterCreateBaseView):
|
class PosterCreateView(IsComAdminMixin, ComTabsMixin, PosterCreateBaseView):
|
||||||
"""Create communication poster"""
|
"""Create communication poster."""
|
||||||
|
|
||||||
success_url = reverse_lazy("com:poster_list")
|
success_url = reverse_lazy("com:poster_list")
|
||||||
|
|
||||||
@ -758,7 +758,7 @@ class PosterCreateView(IsComAdminMixin, ComTabsMixin, PosterCreateBaseView):
|
|||||||
|
|
||||||
|
|
||||||
class PosterEditView(IsComAdminMixin, ComTabsMixin, PosterEditBaseView):
|
class PosterEditView(IsComAdminMixin, ComTabsMixin, PosterEditBaseView):
|
||||||
"""Edit communication poster"""
|
"""Edit communication poster."""
|
||||||
|
|
||||||
success_url = reverse_lazy("com:poster_list")
|
success_url = reverse_lazy("com:poster_list")
|
||||||
|
|
||||||
@ -769,13 +769,13 @@ class PosterEditView(IsComAdminMixin, ComTabsMixin, PosterEditBaseView):
|
|||||||
|
|
||||||
|
|
||||||
class PosterDeleteView(IsComAdminMixin, ComTabsMixin, PosterDeleteBaseView):
|
class PosterDeleteView(IsComAdminMixin, ComTabsMixin, PosterDeleteBaseView):
|
||||||
"""Delete communication poster"""
|
"""Delete communication poster."""
|
||||||
|
|
||||||
success_url = reverse_lazy("com:poster_list")
|
success_url = reverse_lazy("com:poster_list")
|
||||||
|
|
||||||
|
|
||||||
class PosterModerateListView(IsComAdminMixin, ComTabsMixin, ListView):
|
class PosterModerateListView(IsComAdminMixin, ComTabsMixin, ListView):
|
||||||
"""Moderate list communication poster"""
|
"""Moderate list communication poster."""
|
||||||
|
|
||||||
current_tab = "posters"
|
current_tab = "posters"
|
||||||
model = Poster
|
model = Poster
|
||||||
@ -789,7 +789,7 @@ class PosterModerateListView(IsComAdminMixin, ComTabsMixin, ListView):
|
|||||||
|
|
||||||
|
|
||||||
class PosterModerateView(IsComAdminMixin, ComTabsMixin, View):
|
class PosterModerateView(IsComAdminMixin, ComTabsMixin, View):
|
||||||
"""Moderate communication poster"""
|
"""Moderate communication poster."""
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
obj = get_object_or_404(Poster, pk=kwargs["object_id"])
|
obj = get_object_or_404(Poster, pk=kwargs["object_id"])
|
||||||
@ -807,7 +807,7 @@ class PosterModerateView(IsComAdminMixin, ComTabsMixin, View):
|
|||||||
|
|
||||||
|
|
||||||
class ScreenListView(IsComAdminMixin, ComTabsMixin, ListView):
|
class ScreenListView(IsComAdminMixin, ComTabsMixin, ListView):
|
||||||
"""List communication screens"""
|
"""List communication screens."""
|
||||||
|
|
||||||
current_tab = "screens"
|
current_tab = "screens"
|
||||||
model = Screen
|
model = Screen
|
||||||
@ -815,7 +815,7 @@ class ScreenListView(IsComAdminMixin, ComTabsMixin, ListView):
|
|||||||
|
|
||||||
|
|
||||||
class ScreenSlideshowView(DetailView):
|
class ScreenSlideshowView(DetailView):
|
||||||
"""Slideshow of actives posters"""
|
"""Slideshow of actives posters."""
|
||||||
|
|
||||||
pk_url_kwarg = "screen_id"
|
pk_url_kwarg = "screen_id"
|
||||||
model = Screen
|
model = Screen
|
||||||
@ -828,7 +828,7 @@ class ScreenSlideshowView(DetailView):
|
|||||||
|
|
||||||
|
|
||||||
class ScreenCreateView(IsComAdminMixin, ComTabsMixin, CreateView):
|
class ScreenCreateView(IsComAdminMixin, ComTabsMixin, CreateView):
|
||||||
"""Create communication screen"""
|
"""Create communication screen."""
|
||||||
|
|
||||||
current_tab = "screens"
|
current_tab = "screens"
|
||||||
model = Screen
|
model = Screen
|
||||||
@ -838,7 +838,7 @@ class ScreenCreateView(IsComAdminMixin, ComTabsMixin, CreateView):
|
|||||||
|
|
||||||
|
|
||||||
class ScreenEditView(IsComAdminMixin, ComTabsMixin, UpdateView):
|
class ScreenEditView(IsComAdminMixin, ComTabsMixin, UpdateView):
|
||||||
"""Edit communication screen"""
|
"""Edit communication screen."""
|
||||||
|
|
||||||
pk_url_kwarg = "screen_id"
|
pk_url_kwarg = "screen_id"
|
||||||
current_tab = "screens"
|
current_tab = "screens"
|
||||||
@ -849,7 +849,7 @@ class ScreenEditView(IsComAdminMixin, ComTabsMixin, UpdateView):
|
|||||||
|
|
||||||
|
|
||||||
class ScreenDeleteView(IsComAdminMixin, ComTabsMixin, DeleteView):
|
class ScreenDeleteView(IsComAdminMixin, ComTabsMixin, DeleteView):
|
||||||
"""Delete communication screen"""
|
"""Delete communication screen."""
|
||||||
|
|
||||||
pk_url_kwarg = "screen_id"
|
pk_url_kwarg = "screen_id"
|
||||||
current_tab = "screens"
|
current_tab = "screens"
|
||||||
|
29
core/api.py
Normal file
29
core/api.py
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
from django.conf import settings
|
||||||
|
from django.http import HttpResponse
|
||||||
|
from ninja_extra import ControllerBase, api_controller, route
|
||||||
|
from ninja_extra.exceptions import PermissionDenied
|
||||||
|
|
||||||
|
from club.models import Mailing
|
||||||
|
from core.schemas import MarkdownSchema
|
||||||
|
from core.templatetags.renderer import markdown
|
||||||
|
|
||||||
|
|
||||||
|
@api_controller("/markdown")
|
||||||
|
class MarkdownController(ControllerBase):
|
||||||
|
@route.post("", url_name="markdown")
|
||||||
|
def render_markdown(self, body: MarkdownSchema):
|
||||||
|
"""Convert the markdown text into html."""
|
||||||
|
return HttpResponse(markdown(body.text), content_type="text/html")
|
||||||
|
|
||||||
|
|
||||||
|
@api_controller("/mailings")
|
||||||
|
class MailingListController(ControllerBase):
|
||||||
|
@route.get("", response=str)
|
||||||
|
def fetch_mailing_lists(self, key: str):
|
||||||
|
if key != settings.SITH_MAILING_FETCH_KEY:
|
||||||
|
raise PermissionDenied
|
||||||
|
mailings = Mailing.objects.filter(
|
||||||
|
is_moderated=True, club__is_active=True
|
||||||
|
).prefetch_related("subscriptions")
|
||||||
|
data = "\n".join(m.fetch_format() for m in mailings)
|
||||||
|
return data
|
122
core/api_permissions.py
Normal file
122
core/api_permissions.py
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
"""Permission classes to be used within ninja-extra controllers.
|
||||||
|
|
||||||
|
Some permissions are global (like `IsInGroup` or `IsRoot`),
|
||||||
|
and some others are per-object (like `CanView` or `CanEdit`).
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
# restrict all the routes of this controller
|
||||||
|
# to subscribed users
|
||||||
|
@api_controller("/foo", permissions=[IsSubscriber])
|
||||||
|
class FooController(ControllerBase):
|
||||||
|
@route.get("/bar")
|
||||||
|
def bar_get(self):
|
||||||
|
# This route inherits the permissions of the controller
|
||||||
|
# ...
|
||||||
|
|
||||||
|
@route.bar("/bar/{bar_id}", permissions=[CanView])
|
||||||
|
def bar_get_one(self, bar_id: int):
|
||||||
|
# per-object permission resolution happens
|
||||||
|
# when calling either the `get_object_or_exception`
|
||||||
|
# or `get_object_or_none` method.
|
||||||
|
bar = self.get_object_or_exception(Counter, pk=bar_id)
|
||||||
|
|
||||||
|
# you can also call the `check_object_permission` manually
|
||||||
|
other_bar = Counter.objects.first()
|
||||||
|
self.check_object_permissions(other_bar)
|
||||||
|
|
||||||
|
# ...
|
||||||
|
|
||||||
|
# This route is restricted to counter admins and root users
|
||||||
|
@route.delete(
|
||||||
|
"/bar/{bar_id}",
|
||||||
|
permissions=[IsRoot | IsInGroup(settings.SITH_GROUP_COUNTER_ADMIN_ID)
|
||||||
|
]
|
||||||
|
def bar_delete(self, bar_id: int):
|
||||||
|
# ...
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from django.http import HttpRequest
|
||||||
|
from ninja_extra import ControllerBase
|
||||||
|
from ninja_extra.permissions import BasePermission
|
||||||
|
|
||||||
|
|
||||||
|
class IsInGroup(BasePermission):
|
||||||
|
"""Check that the user is in the group whose primary key is given."""
|
||||||
|
|
||||||
|
def __init__(self, group_pk: int):
|
||||||
|
self._group_pk = group_pk
|
||||||
|
|
||||||
|
def has_permission(self, request: HttpRequest, controller: ControllerBase) -> bool:
|
||||||
|
return request.user.is_in_group(pk=self._group_pk)
|
||||||
|
|
||||||
|
|
||||||
|
class IsRoot(BasePermission):
|
||||||
|
"""Check that the user is root."""
|
||||||
|
|
||||||
|
def has_permission(self, request: HttpRequest, controller: ControllerBase) -> bool:
|
||||||
|
return request.user.is_root
|
||||||
|
|
||||||
|
|
||||||
|
class IsSubscriber(BasePermission):
|
||||||
|
"""Check that the user is currently subscribed."""
|
||||||
|
|
||||||
|
def has_permission(self, request: HttpRequest, controller: ControllerBase) -> bool:
|
||||||
|
return request.user.is_subscribed
|
||||||
|
|
||||||
|
|
||||||
|
class IsOldSubscriber(BasePermission):
|
||||||
|
"""Check that the user has at least one subscription in its history."""
|
||||||
|
|
||||||
|
def has_permission(self, request: HttpRequest, controller: ControllerBase) -> bool:
|
||||||
|
return request.user.was_subscribed
|
||||||
|
|
||||||
|
|
||||||
|
class CanView(BasePermission):
|
||||||
|
"""Check that this user has the permission to view the object of this route.
|
||||||
|
|
||||||
|
Wrap the `user.can_view(obj)` method.
|
||||||
|
To see an example, look at the exemple in the module docstring.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def has_permission(self, request: HttpRequest, controller: ControllerBase) -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
def has_object_permission(
|
||||||
|
self, request: HttpRequest, controller: ControllerBase, obj: Any
|
||||||
|
) -> bool:
|
||||||
|
return request.user.can_view(obj)
|
||||||
|
|
||||||
|
|
||||||
|
class CanEdit(BasePermission):
|
||||||
|
"""Check that this user has the permission to edit the object of this route.
|
||||||
|
|
||||||
|
Wrap the `user.can_edit(obj)` method.
|
||||||
|
To see an example, look at the exemple in the module docstring.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def has_permission(self, request: HttpRequest, controller: ControllerBase) -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
def has_object_permission(
|
||||||
|
self, request: HttpRequest, controller: ControllerBase, obj: Any
|
||||||
|
) -> bool:
|
||||||
|
return request.user.can_edit(obj)
|
||||||
|
|
||||||
|
|
||||||
|
class IsOwner(BasePermission):
|
||||||
|
"""Check that this user owns the object of this route.
|
||||||
|
|
||||||
|
Wrap the `user.is_owner(obj)` method.
|
||||||
|
To see an example, look at the exemple in the module docstring.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def has_permission(self, request: HttpRequest, controller: ControllerBase) -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
def has_object_permission(
|
||||||
|
self, request: HttpRequest, controller: ControllerBase, obj: Any
|
||||||
|
) -> bool:
|
||||||
|
return request.user.is_owner(obj)
|
32
core/baker_recipes.py
Normal file
32
core/baker_recipes.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from django.utils.timezone import now
|
||||||
|
from model_bakery import seq
|
||||||
|
from model_bakery.recipe import Recipe, related
|
||||||
|
|
||||||
|
from core.models import User
|
||||||
|
from subscription.models import Subscription
|
||||||
|
|
||||||
|
active_subscription = Recipe(
|
||||||
|
Subscription,
|
||||||
|
subscription_start=now() - timedelta(days=30),
|
||||||
|
subscription_end=now() + timedelta(days=30),
|
||||||
|
)
|
||||||
|
ended_subscription = Recipe(
|
||||||
|
Subscription,
|
||||||
|
subscription_start=now() - timedelta(days=60),
|
||||||
|
subscription_end=now() - timedelta(days=30),
|
||||||
|
)
|
||||||
|
|
||||||
|
subscriber_user = Recipe(
|
||||||
|
User,
|
||||||
|
first_name="subscriber",
|
||||||
|
last_name=seq("user "),
|
||||||
|
subscriptions=related(active_subscription),
|
||||||
|
)
|
||||||
|
old_subscriber_user = Recipe(
|
||||||
|
User,
|
||||||
|
first_name="old subscriber",
|
||||||
|
last_name=seq("user "),
|
||||||
|
subscriptions=related(ended_subscription),
|
||||||
|
)
|
@ -19,9 +19,7 @@ class TwoDigitMonthConverter:
|
|||||||
|
|
||||||
|
|
||||||
class BooleanStringConverter:
|
class BooleanStringConverter:
|
||||||
"""
|
"""Converter whose regex match either True or False."""
|
||||||
Converter whose regex match either True or False
|
|
||||||
"""
|
|
||||||
|
|
||||||
regex = r"(True)|(False)"
|
regex = r"(True)|(False)"
|
||||||
|
|
||||||
|
@ -90,12 +90,15 @@ def list_tags(s):
|
|||||||
yield parts[1][len(tag_prefix) :]
|
yield parts[1][len(tag_prefix) :]
|
||||||
|
|
||||||
|
|
||||||
def parse_semver(s):
|
def parse_semver(s) -> tuple[int, int, int] | None:
|
||||||
"""
|
"""Parse a semver string.
|
||||||
Turns a semver string into a 3-tuple or None if the parsing failed, it is a
|
|
||||||
prerelease or it has build metadata.
|
|
||||||
|
|
||||||
See https://semver.org
|
See https://semver.org
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A tuple, if the parsing was successful, else None.
|
||||||
|
In the latter case, it must probably be a prerelease
|
||||||
|
or include build metadata.
|
||||||
"""
|
"""
|
||||||
m = semver_regex.match(s)
|
m = semver_regex.match(s)
|
||||||
|
|
||||||
@ -106,7 +109,7 @@ def parse_semver(s):
|
|||||||
):
|
):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return (int(m.group("major")), int(m.group("minor")), int(m.group("patch")))
|
return int(m.group("major")), int(m.group("minor")), int(m.group("patch"))
|
||||||
|
|
||||||
|
|
||||||
def semver_to_s(t):
|
def semver_to_s(t):
|
||||||
|
@ -29,9 +29,7 @@ from django.core.management.commands import compilemessages
|
|||||||
|
|
||||||
|
|
||||||
class Command(compilemessages.Command):
|
class Command(compilemessages.Command):
|
||||||
"""
|
"""Wrap call to compilemessages to avoid building whole env."""
|
||||||
Wrap call to compilemessages to avoid building whole env
|
|
||||||
"""
|
|
||||||
|
|
||||||
help = """
|
help = """
|
||||||
The usage is the same as the real compilemessages
|
The usage is the same as the real compilemessages
|
||||||
|
@ -30,9 +30,7 @@ from django.core.management.base import BaseCommand
|
|||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
"""
|
"""Compiles scss in static folder for production."""
|
||||||
Compiles scss in static folder for production
|
|
||||||
"""
|
|
||||||
|
|
||||||
help = "Compile scss files from static folder"
|
help = "Compile scss files from static folder"
|
||||||
|
|
||||||
|
@ -1,90 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
#
|
|
||||||
# Copyright 2019
|
|
||||||
# - Sli <antoine@bartuccio.fr>
|
|
||||||
#
|
|
||||||
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
|
|
||||||
# http://ae.utbm.fr.
|
|
||||||
#
|
|
||||||
# This program is free software; you can redistribute it and/or modify it under
|
|
||||||
# the terms of the GNU General Public License a published by the Free Software
|
|
||||||
# Foundation; either version 3 of the License, or (at your option) any later
|
|
||||||
# version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
|
||||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
|
||||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
|
||||||
# details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License along with
|
|
||||||
# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple
|
|
||||||
# Place - Suite 330, Boston, MA 02111-1307, USA.
|
|
||||||
#
|
|
||||||
#
|
|
||||||
|
|
||||||
import os
|
|
||||||
import signal
|
|
||||||
import sys
|
|
||||||
from http.server import CGIHTTPRequestHandler, test
|
|
||||||
|
|
||||||
from django.core.management.base import BaseCommand
|
|
||||||
from django.utils import autoreload
|
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
|
||||||
help = "Generate Sphinx documentation and launch basic server"
|
|
||||||
|
|
||||||
default_addr = "127.0.0.1"
|
|
||||||
default_port = "8080"
|
|
||||||
|
|
||||||
def add_arguments(self, parser):
|
|
||||||
parser.add_argument(
|
|
||||||
"addrport", nargs="?", help="Optional port number, or ipaddr:port"
|
|
||||||
)
|
|
||||||
|
|
||||||
def build_documentation(self):
|
|
||||||
os.chdir(os.path.join(self.project_dir, "doc"))
|
|
||||||
err = os.system("make html")
|
|
||||||
|
|
||||||
if err != 0:
|
|
||||||
self.stdout.write("A build error occured")
|
|
||||||
|
|
||||||
def start_server(self, **kwargs):
|
|
||||||
os.chdir(os.path.join(self.project_dir, "doc", "_build/html"))
|
|
||||||
addr = self.default_addr
|
|
||||||
port = self.default_port
|
|
||||||
if kwargs["addrport"]:
|
|
||||||
addrport = kwargs["addrport"].split(":")
|
|
||||||
|
|
||||||
addr = addrport[0]
|
|
||||||
|
|
||||||
if len(addrport) > 1:
|
|
||||||
port = addrport[1]
|
|
||||||
|
|
||||||
if not port.isnumeric():
|
|
||||||
self.stdout.write("%s is not a valid port" % (port,))
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
test(HandlerClass=CGIHTTPRequestHandler, port=int(port), bind=addr)
|
|
||||||
|
|
||||||
def build_and_start_server(self, **kwargs):
|
|
||||||
self.build_documentation()
|
|
||||||
self.start_server(**kwargs)
|
|
||||||
|
|
||||||
def handle(self, *args, **kwargs):
|
|
||||||
self.project_dir = os.getcwd()
|
|
||||||
|
|
||||||
signal.signal(signal.SIGTERM, lambda *args: sys.exit(0))
|
|
||||||
try:
|
|
||||||
if os.environ.get(autoreload.DJANGO_AUTORELOAD_ENV) == "true":
|
|
||||||
reloader = autoreload.get_reloader()
|
|
||||||
reloader.watch_dir(os.path.join(self.project_dir, "doc"), "**/*.rst")
|
|
||||||
autoreload.logger.info(
|
|
||||||
"Watching for file changes with %s", reloader.__class__.__name__
|
|
||||||
)
|
|
||||||
autoreload.start_django(reloader, self.build_and_start_server, **kwargs)
|
|
||||||
else:
|
|
||||||
exit_code = autoreload.restart_with_reloader()
|
|
||||||
sys.exit(exit_code)
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
pass
|
|
@ -21,20 +21,19 @@
|
|||||||
#
|
#
|
||||||
#
|
#
|
||||||
|
|
||||||
import os
|
from pathlib import Path
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
|
|
||||||
from core.markdown import markdown
|
from core.markdown import markdown
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
help = "Output the fully rendered doc/SYNTAX.md file"
|
help = "Output the fully rendered SYNTAX.md file"
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
root_path = os.path.dirname(
|
root_path = Path(settings.BASE_DIR)
|
||||||
os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
with open(root_path / "core/fixtures/SYNTAX.md", "r") as md:
|
||||||
)
|
|
||||||
with open(os.path.join(root_path) + "/doc/SYNTAX.md", "r") as md:
|
|
||||||
result = markdown(md.read())
|
result = markdown(md.read())
|
||||||
print(result, end="")
|
print(result, end="")
|
||||||
|
@ -383,7 +383,7 @@ Welcome to the wiki page!
|
|||||||
# Adding syntax help page
|
# Adding syntax help page
|
||||||
p = Page(name="Aide_sur_la_syntaxe")
|
p = Page(name="Aide_sur_la_syntaxe")
|
||||||
p.save(force_lock=True)
|
p.save(force_lock=True)
|
||||||
with open(root_path / "doc" / "SYNTAX.md", "r") as rm:
|
with open(root_path / "core" / "fixtures" / "SYNTAX.md", "r") as rm:
|
||||||
PageRev(
|
PageRev(
|
||||||
page=p, title="Aide sur la syntaxe", author=skia, content=rm.read()
|
page=p, title="Aide sur la syntaxe", author=skia, content=rm.read()
|
||||||
).save()
|
).save()
|
||||||
|
@ -14,7 +14,6 @@
|
|||||||
#
|
#
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
|
||||||
import re
|
import re
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
@ -131,9 +130,3 @@ markdown = mistune.create_markdown(
|
|||||||
"url",
|
"url",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
root_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
||||||
with open(os.path.join(root_path) + "/doc/SYNTAX.md", "r") as md:
|
|
||||||
result = markdown(md.read())
|
|
||||||
print(result, end="")
|
|
||||||
|
@ -53,13 +53,14 @@ _threadlocal = threading.local()
|
|||||||
|
|
||||||
|
|
||||||
def get_signal_request():
|
def get_signal_request():
|
||||||
"""
|
"""Allow to access current request in signals.
|
||||||
!!! Do not use if your operation is asynchronus !!!
|
|
||||||
Allow to access current request in signals
|
|
||||||
This is a hack that looks into the thread
|
|
||||||
Mainly used for log purpose
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
This is a hack that looks into the thread
|
||||||
|
Mainly used for log purpose.
|
||||||
|
|
||||||
|
!!!danger
|
||||||
|
Do not use if your operation is asynchronous.
|
||||||
|
"""
|
||||||
return getattr(_threadlocal, "request", None)
|
return getattr(_threadlocal, "request", None)
|
||||||
|
|
||||||
|
|
||||||
|
224
core/models.py
224
core/models.py
@ -21,11 +21,13 @@
|
|||||||
# Place - Suite 330, Boston, MA 02111-1307, USA.
|
# Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import importlib
|
import importlib
|
||||||
import os
|
import os
|
||||||
import unicodedata
|
import unicodedata
|
||||||
from datetime import date, timedelta
|
from datetime import date, timedelta
|
||||||
from typing import List, Optional, Union
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import (
|
from django.contrib.auth.models import (
|
||||||
@ -56,6 +58,9 @@ from phonenumber_field.modelfields import PhoneNumberField
|
|||||||
|
|
||||||
from core import utils
|
from core import utils
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from club.models import Club
|
||||||
|
|
||||||
|
|
||||||
class RealGroupManager(AuthGroupManager):
|
class RealGroupManager(AuthGroupManager):
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
@ -68,8 +73,7 @@ class MetaGroupManager(AuthGroupManager):
|
|||||||
|
|
||||||
|
|
||||||
class Group(AuthGroup):
|
class Group(AuthGroup):
|
||||||
"""
|
"""Implement both RealGroups and Meta groups.
|
||||||
Implement both RealGroups and Meta groups
|
|
||||||
|
|
||||||
Groups are sorted by their is_meta property
|
Groups are sorted by their is_meta property
|
||||||
"""
|
"""
|
||||||
@ -87,9 +91,6 @@ class Group(AuthGroup):
|
|||||||
ordering = ["name"]
|
ordering = ["name"]
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
"""
|
|
||||||
This is needed for black magic powered UpdateView's children
|
|
||||||
"""
|
|
||||||
return reverse("core:group_list")
|
return reverse("core:group_list")
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
@ -104,8 +105,8 @@ class Group(AuthGroup):
|
|||||||
|
|
||||||
|
|
||||||
class MetaGroup(Group):
|
class MetaGroup(Group):
|
||||||
"""
|
"""MetaGroups are dynamically created groups.
|
||||||
MetaGroups are dynamically created groups.
|
|
||||||
Generally used with clubs where creating a club creates two groups:
|
Generally used with clubs where creating a club creates two groups:
|
||||||
|
|
||||||
* club-SITH_BOARD_SUFFIX
|
* club-SITH_BOARD_SUFFIX
|
||||||
@ -123,14 +124,14 @@ class MetaGroup(Group):
|
|||||||
self.is_meta = True
|
self.is_meta = True
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def associated_club(self):
|
def associated_club(self) -> Club | None:
|
||||||
"""
|
"""Return the group associated with this meta group.
|
||||||
Return the group associated with this meta group
|
|
||||||
|
|
||||||
The result of this function is cached
|
The result of this function is cached
|
||||||
|
|
||||||
:return: The associated club if it exists, else None
|
|
||||||
:rtype: club.models.Club | None
|
Returns:
|
||||||
|
The associated club if it exists, else None
|
||||||
"""
|
"""
|
||||||
from club.models import Club
|
from club.models import Club
|
||||||
|
|
||||||
@ -150,8 +151,8 @@ class MetaGroup(Group):
|
|||||||
|
|
||||||
|
|
||||||
class RealGroup(Group):
|
class RealGroup(Group):
|
||||||
"""
|
"""RealGroups are created by the developer.
|
||||||
RealGroups are created by the developer.
|
|
||||||
Most of the time they match a number in settings to be easily used for permissions.
|
Most of the time they match a number in settings to be easily used for permissions.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -173,22 +174,26 @@ def validate_promo(value):
|
|||||||
|
|
||||||
|
|
||||||
def get_group(*, pk: int = None, name: str = None) -> Optional[Group]:
|
def get_group(*, pk: int = None, name: str = None) -> Optional[Group]:
|
||||||
"""
|
"""Search for a group by its primary key or its name.
|
||||||
Search for a group by its primary key or its name.
|
|
||||||
Either one of the two must be set.
|
Either one of the two must be set.
|
||||||
|
|
||||||
The result is cached for the default duration (should be 5 minutes).
|
The result is cached for the default duration (should be 5 minutes).
|
||||||
|
|
||||||
:param pk: The primary key of the group
|
Args:
|
||||||
:param name: The name of the group
|
pk: The primary key of the group
|
||||||
:return: The group if it exists, else None
|
name: The name of the group
|
||||||
:raise ValueError: If no group matches the criteria
|
|
||||||
|
Returns:
|
||||||
|
The group if it exists, else None
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If no group matches the criteria
|
||||||
"""
|
"""
|
||||||
if pk is None and name is None:
|
if pk is None and name is None:
|
||||||
raise ValueError("Either pk or name must be set")
|
raise ValueError("Either pk or name must be set")
|
||||||
|
|
||||||
# replace space characters to hide warnings with memcached backend
|
# replace space characters to hide warnings with memcached backend
|
||||||
pk_or_name: Union[str, int] = pk if pk is not None else name.replace(" ", "_")
|
pk_or_name: str | int = pk if pk is not None else name.replace(" ", "_")
|
||||||
group = cache.get(f"sith_group_{pk_or_name}")
|
group = cache.get(f"sith_group_{pk_or_name}")
|
||||||
|
|
||||||
if group == "not_found":
|
if group == "not_found":
|
||||||
@ -211,8 +216,7 @@ def get_group(*, pk: int = None, name: str = None) -> Optional[Group]:
|
|||||||
|
|
||||||
|
|
||||||
class User(AbstractBaseUser):
|
class User(AbstractBaseUser):
|
||||||
"""
|
"""Defines the base user class, useable in every app.
|
||||||
Defines the base user class, useable in every app
|
|
||||||
|
|
||||||
This is almost the same as the auth module AbstractUser since it inherits from it,
|
This is almost the same as the auth module AbstractUser since it inherits from it,
|
||||||
but some fields are required, and the username is generated automatically with the
|
but some fields are required, and the username is generated automatically with the
|
||||||
@ -382,9 +386,6 @@ class User(AbstractBaseUser):
|
|||||||
return self.is_active and self.is_superuser
|
return self.is_active and self.is_superuser
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
"""
|
|
||||||
This is needed for black magic powered UpdateView's children
|
|
||||||
"""
|
|
||||||
return reverse("core:user_profile", kwargs={"user_id": self.pk})
|
return reverse("core:user_profile", kwargs={"user_id": self.pk})
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
@ -412,8 +413,7 @@ class User(AbstractBaseUser):
|
|||||||
return 0
|
return 0
|
||||||
|
|
||||||
def is_in_group(self, *, pk: int = None, name: str = None) -> bool:
|
def is_in_group(self, *, pk: int = None, name: str = None) -> bool:
|
||||||
"""
|
"""Check if this user is in the given group.
|
||||||
Check if this user is in the given group.
|
|
||||||
Either a group id or a group name must be provided.
|
Either a group id or a group name must be provided.
|
||||||
If both are passed, only the id will be considered.
|
If both are passed, only the id will be considered.
|
||||||
|
|
||||||
@ -421,7 +421,8 @@ class User(AbstractBaseUser):
|
|||||||
If no group is found, return False.
|
If no group is found, return False.
|
||||||
If a group is found, check if this user is in the latter.
|
If a group is found, check if this user is in the latter.
|
||||||
|
|
||||||
:return: True if the user is the group, else False
|
Returns:
|
||||||
|
True if the user is the group, else False
|
||||||
"""
|
"""
|
||||||
if pk is not None:
|
if pk is not None:
|
||||||
group: Optional[Group] = get_group(pk=pk)
|
group: Optional[Group] = get_group(pk=pk)
|
||||||
@ -454,11 +455,12 @@ class User(AbstractBaseUser):
|
|||||||
return group in self.cached_groups
|
return group in self.cached_groups
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def cached_groups(self) -> List[Group]:
|
def cached_groups(self) -> list[Group]:
|
||||||
"""
|
"""Get the list of groups this user is in.
|
||||||
Get the list of groups this user is in.
|
|
||||||
The result is cached for the default duration (should be 5 minutes)
|
The result is cached for the default duration (should be 5 minutes)
|
||||||
:return: A list of all the groups this user is in
|
|
||||||
|
Returns: A list of all the groups this user is in.
|
||||||
"""
|
"""
|
||||||
groups = cache.get(f"user_{self.id}_groups")
|
groups = cache.get(f"user_{self.id}_groups")
|
||||||
if groups is None:
|
if groups is None:
|
||||||
@ -523,9 +525,8 @@ class User(AbstractBaseUser):
|
|||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def age(self) -> int:
|
def age(self) -> int:
|
||||||
"""
|
"""Return the age this user has the day the method is called.
|
||||||
Return the age this user has the day the method is called.
|
If the user has not filled his age, return 0.
|
||||||
If the user has not filled his age, return 0
|
|
||||||
"""
|
"""
|
||||||
if self.date_of_birth is None:
|
if self.date_of_birth is None:
|
||||||
return 0
|
return 0
|
||||||
@ -576,31 +577,27 @@ class User(AbstractBaseUser):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def get_full_name(self):
|
def get_full_name(self):
|
||||||
"""
|
"""Returns the first_name plus the last_name, with a space in between."""
|
||||||
Returns the first_name plus the last_name, with a space in between.
|
|
||||||
"""
|
|
||||||
full_name = "%s %s" % (self.first_name, self.last_name)
|
full_name = "%s %s" % (self.first_name, self.last_name)
|
||||||
return full_name.strip()
|
return full_name.strip()
|
||||||
|
|
||||||
def get_short_name(self):
|
def get_short_name(self):
|
||||||
"Returns the short name for the user."
|
"""Returns the short name for the user."""
|
||||||
if self.nick_name:
|
if self.nick_name:
|
||||||
return self.nick_name
|
return self.nick_name
|
||||||
return self.first_name + " " + self.last_name
|
return self.first_name + " " + self.last_name
|
||||||
|
|
||||||
def get_display_name(self):
|
def get_display_name(self) -> str:
|
||||||
"""
|
"""Returns the display name of the user.
|
||||||
Returns the display name of the user.
|
|
||||||
A nickname if possible, otherwise, the full name
|
A nickname if possible, otherwise, the full name.
|
||||||
"""
|
"""
|
||||||
if self.nick_name:
|
if self.nick_name:
|
||||||
return "%s (%s)" % (self.get_full_name(), self.nick_name)
|
return "%s (%s)" % (self.get_full_name(), self.nick_name)
|
||||||
return self.get_full_name()
|
return self.get_full_name()
|
||||||
|
|
||||||
def get_age(self):
|
def get_age(self):
|
||||||
"""
|
"""Returns the age."""
|
||||||
Returns the age
|
|
||||||
"""
|
|
||||||
today = timezone.now()
|
today = timezone.now()
|
||||||
born = self.date_of_birth
|
born = self.date_of_birth
|
||||||
return (
|
return (
|
||||||
@ -608,18 +605,18 @@ class User(AbstractBaseUser):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def email_user(self, subject, message, from_email=None, **kwargs):
|
def email_user(self, subject, message, from_email=None, **kwargs):
|
||||||
"""
|
"""Sends an email to this User."""
|
||||||
Sends an email to this User.
|
|
||||||
"""
|
|
||||||
if from_email is None:
|
if from_email is None:
|
||||||
from_email = settings.DEFAULT_FROM_EMAIL
|
from_email = settings.DEFAULT_FROM_EMAIL
|
||||||
send_mail(subject, message, from_email, [self.email], **kwargs)
|
send_mail(subject, message, from_email, [self.email], **kwargs)
|
||||||
|
|
||||||
def generate_username(self):
|
def generate_username(self) -> str:
|
||||||
"""
|
"""Generates a unique username based on the first and last names.
|
||||||
Generates a unique username based on the first and last names.
|
|
||||||
For example: Guy Carlier gives gcarlier, and gcarlier1 if the first one exists
|
For example: Guy Carlier gives gcarlier, and gcarlier1 if the first one exists.
|
||||||
Returns the generated username
|
|
||||||
|
Returns:
|
||||||
|
The generated username.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def remove_accents(data):
|
def remove_accents(data):
|
||||||
@ -644,9 +641,7 @@ class User(AbstractBaseUser):
|
|||||||
return user_name
|
return user_name
|
||||||
|
|
||||||
def is_owner(self, obj):
|
def is_owner(self, obj):
|
||||||
"""
|
"""Determine if the object is owned by the user."""
|
||||||
Determine if the object is owned by the user
|
|
||||||
"""
|
|
||||||
if hasattr(obj, "is_owned_by") and obj.is_owned_by(self):
|
if hasattr(obj, "is_owned_by") and obj.is_owned_by(self):
|
||||||
return True
|
return True
|
||||||
if hasattr(obj, "owner_group") and self.is_in_group(pk=obj.owner_group.id):
|
if hasattr(obj, "owner_group") and self.is_in_group(pk=obj.owner_group.id):
|
||||||
@ -656,9 +651,7 @@ class User(AbstractBaseUser):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def can_edit(self, obj):
|
def can_edit(self, obj):
|
||||||
"""
|
"""Determine if the object can be edited by the user."""
|
||||||
Determine if the object can be edited by the user
|
|
||||||
"""
|
|
||||||
if hasattr(obj, "can_be_edited_by") and obj.can_be_edited_by(self):
|
if hasattr(obj, "can_be_edited_by") and obj.can_be_edited_by(self):
|
||||||
return True
|
return True
|
||||||
if hasattr(obj, "edit_groups"):
|
if hasattr(obj, "edit_groups"):
|
||||||
@ -672,9 +665,7 @@ class User(AbstractBaseUser):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def can_view(self, obj):
|
def can_view(self, obj):
|
||||||
"""
|
"""Determine if the object can be viewed by the user."""
|
||||||
Determine if the object can be viewed by the user
|
|
||||||
"""
|
|
||||||
if hasattr(obj, "can_be_viewed_by") and obj.can_be_viewed_by(self):
|
if hasattr(obj, "can_be_viewed_by") and obj.can_be_viewed_by(self):
|
||||||
return True
|
return True
|
||||||
if hasattr(obj, "view_groups"):
|
if hasattr(obj, "view_groups"):
|
||||||
@ -730,11 +721,8 @@ class User(AbstractBaseUser):
|
|||||||
return infos
|
return infos
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def clubs_with_rights(self):
|
def clubs_with_rights(self) -> list[Club]:
|
||||||
"""
|
"""The list of clubs where the user has rights"""
|
||||||
:return: the list of clubs where the user has rights
|
|
||||||
:rtype: list[club.models.Club]
|
|
||||||
"""
|
|
||||||
memberships = self.memberships.ongoing().board().select_related("club")
|
memberships = self.memberships.ongoing().board().select_related("club")
|
||||||
return [m.club for m in memberships]
|
return [m.club for m in memberships]
|
||||||
|
|
||||||
@ -796,9 +784,7 @@ class AnonymousUser(AuthAnonymousUser):
|
|||||||
raise PermissionDenied
|
raise PermissionDenied
|
||||||
|
|
||||||
def is_in_group(self, *, pk: int = None, name: str = None) -> bool:
|
def is_in_group(self, *, pk: int = None, name: str = None) -> bool:
|
||||||
"""
|
"""The anonymous user is only in the public group."""
|
||||||
The anonymous user is only in the public group
|
|
||||||
"""
|
|
||||||
allowed_id = settings.SITH_GROUP_PUBLIC_ID
|
allowed_id = settings.SITH_GROUP_PUBLIC_ID
|
||||||
if pk is not None:
|
if pk is not None:
|
||||||
return pk == allowed_id
|
return pk == allowed_id
|
||||||
@ -957,16 +943,15 @@ class SithFile(models.Model):
|
|||||||
).save()
|
).save()
|
||||||
|
|
||||||
def can_be_managed_by(self, user: User) -> bool:
|
def can_be_managed_by(self, user: User) -> bool:
|
||||||
"""
|
"""Tell if the user can manage the file (edit, delete, etc.) or not.
|
||||||
Tell if the user can manage the file (edit, delete, etc.) or not.
|
|
||||||
Apply the following rules:
|
Apply the following rules:
|
||||||
- If the file is not in the SAS nor in the profiles directory, it can be "managed" by anyone -> return True
|
- If the file is not in the SAS nor in the profiles directory, it can be "managed" by anyone -> return True
|
||||||
- If the file is in the SAS, only the SAS admins (or roots) can manage it -> return True if the user is in the SAS admin group or is a root
|
- If the file is in the SAS, only the SAS admins (or roots) can manage it -> return True if the user is in the SAS admin group or is a root
|
||||||
- If the file is in the profiles directory, only the roots can manage it -> return True if the user is a root
|
- If the file is in the profiles directory, only the roots can manage it -> return True if the user is a root.
|
||||||
|
|
||||||
:returns: True if the file is managed by the SAS or within the profiles directory, False otherwise
|
Returns:
|
||||||
|
True if the file is managed by the SAS or within the profiles directory, False otherwise
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# If the file is not in the SAS nor in the profiles directory, it can be "managed" by anyone
|
# If the file is not in the SAS nor in the profiles directory, it can be "managed" by anyone
|
||||||
profiles_dir = SithFile.objects.filter(name="profiles").first()
|
profiles_dir = SithFile.objects.filter(name="profiles").first()
|
||||||
if not self.is_in_sas and not profiles_dir in self.get_parent_list():
|
if not self.is_in_sas and not profiles_dir in self.get_parent_list():
|
||||||
@ -1017,9 +1002,7 @@ class SithFile(models.Model):
|
|||||||
return super().delete()
|
return super().delete()
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
"""
|
"""Cleans up the file."""
|
||||||
Cleans up the file
|
|
||||||
"""
|
|
||||||
super().clean()
|
super().clean()
|
||||||
if "/" in self.name:
|
if "/" in self.name:
|
||||||
raise ValidationError(_("Character '/' not authorized in name"))
|
raise ValidationError(_("Character '/' not authorized in name"))
|
||||||
@ -1070,15 +1053,14 @@ class SithFile(models.Model):
|
|||||||
c.apply_rights_recursively(only_folders=only_folders)
|
c.apply_rights_recursively(only_folders=only_folders)
|
||||||
|
|
||||||
def copy_rights(self):
|
def copy_rights(self):
|
||||||
"""Copy, if possible, the rights of the parent folder"""
|
"""Copy, if possible, the rights of the parent folder."""
|
||||||
if self.parent is not None:
|
if self.parent is not None:
|
||||||
self.edit_groups.set(self.parent.edit_groups.all())
|
self.edit_groups.set(self.parent.edit_groups.all())
|
||||||
self.view_groups.set(self.parent.view_groups.all())
|
self.view_groups.set(self.parent.view_groups.all())
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
def move_to(self, parent):
|
def move_to(self, parent):
|
||||||
"""
|
"""Move a file to a new parent.
|
||||||
Move a file to a new parent.
|
|
||||||
`parent` must be a SithFile with the `is_folder=True` property. Otherwise, this function doesn't change
|
`parent` must be a SithFile with the `is_folder=True` property. Otherwise, this function doesn't change
|
||||||
anything.
|
anything.
|
||||||
This is done only at the DB level, so that it's very fast for the user. Indeed, this function doesn't modify
|
This is done only at the DB level, so that it's very fast for the user. Indeed, this function doesn't modify
|
||||||
@ -1091,10 +1073,7 @@ class SithFile(models.Model):
|
|||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
def _repair_fs(self):
|
def _repair_fs(self):
|
||||||
"""
|
"""Rebuilds recursively the filesystem as it should be regarding the DB tree."""
|
||||||
This function rebuilds recursively the filesystem as it should be
|
|
||||||
regarding the DB tree.
|
|
||||||
"""
|
|
||||||
if self.is_folder:
|
if self.is_folder:
|
||||||
for c in self.children.all():
|
for c in self.children.all():
|
||||||
c._repair_fs()
|
c._repair_fs()
|
||||||
@ -1197,19 +1176,19 @@ class SithFile(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
class LockError(Exception):
|
class LockError(Exception):
|
||||||
"""There was a lock error on the object"""
|
"""There was a lock error on the object."""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class AlreadyLocked(LockError):
|
class AlreadyLocked(LockError):
|
||||||
"""The object is already locked"""
|
"""The object is already locked."""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class NotLocked(LockError):
|
class NotLocked(LockError):
|
||||||
"""The object is not locked"""
|
"""The object is not locked."""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -1220,12 +1199,11 @@ def get_default_owner_group():
|
|||||||
|
|
||||||
|
|
||||||
class Page(models.Model):
|
class Page(models.Model):
|
||||||
"""
|
"""The page class to build a Wiki
|
||||||
The page class to build a Wiki
|
|
||||||
Each page may have a parent and it's URL is of the form my.site/page/<grd_pa>/<parent>/<mypage>
|
Each page may have a parent and it's URL is of the form my.site/page/<grd_pa>/<parent>/<mypage>
|
||||||
It has an ID field, but don't use it, since it's only there for DB part, and because compound primary key is
|
It has an ID field, but don't use it, since it's only there for DB part, and because compound primary key is
|
||||||
awkward!
|
awkward!
|
||||||
Prefere querying pages with Page.get_page_by_full_name()
|
Prefere querying pages with Page.get_page_by_full_name().
|
||||||
|
|
||||||
Be careful with the _full_name attribute: this field may not be valid until you call save(). It's made for fast
|
Be careful with the _full_name attribute: this field may not be valid until you call save(). It's made for fast
|
||||||
query, but don't rely on it when playing with a Page object, use get_full_name() instead!
|
query, but don't rely on it when playing with a Page object, use get_full_name() instead!
|
||||||
@ -1294,9 +1272,7 @@ class Page(models.Model):
|
|||||||
return self.get_full_name()
|
return self.get_full_name()
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
"""
|
"""Performs some needed actions before and after saving a page in database."""
|
||||||
Performs some needed actions before and after saving a page in database
|
|
||||||
"""
|
|
||||||
locked = kwargs.pop("force_lock", False)
|
locked = kwargs.pop("force_lock", False)
|
||||||
if not locked:
|
if not locked:
|
||||||
locked = self.is_locked()
|
locked = self.is_locked()
|
||||||
@ -1317,22 +1293,15 @@ class Page(models.Model):
|
|||||||
self.unset_lock()
|
self.unset_lock()
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
"""
|
|
||||||
This is needed for black magic powered UpdateView's children
|
|
||||||
"""
|
|
||||||
return reverse("core:page", kwargs={"page_name": self._full_name})
|
return reverse("core:page", kwargs={"page_name": self._full_name})
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_page_by_full_name(name):
|
def get_page_by_full_name(name):
|
||||||
"""
|
"""Quicker to get a page with that method rather than building the request every time."""
|
||||||
Quicker to get a page with that method rather than building the request every time
|
|
||||||
"""
|
|
||||||
return Page.objects.filter(_full_name=name).first()
|
return Page.objects.filter(_full_name=name).first()
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
"""
|
"""Cleans up only the name for the moment, but this can be used to make any treatment before saving the object."""
|
||||||
Cleans up only the name for the moment, but this can be used to make any treatment before saving the object
|
|
||||||
"""
|
|
||||||
if "/" in self.name:
|
if "/" in self.name:
|
||||||
self.name = self.name.split("/")[-1]
|
self.name = self.name.split("/")[-1]
|
||||||
if (
|
if (
|
||||||
@ -1367,10 +1336,11 @@ class Page(models.Model):
|
|||||||
return l
|
return l
|
||||||
|
|
||||||
def is_locked(self):
|
def is_locked(self):
|
||||||
"""
|
"""Is True if the page is locked, False otherwise.
|
||||||
Is True if the page is locked, False otherwise
|
|
||||||
This is where the timeout is handled, so a locked page for which the timeout is reach will be unlocked and this
|
This is where the timeout is handled,
|
||||||
function will return False
|
so a locked page for which the timeout is reach will be unlocked and this
|
||||||
|
function will return False.
|
||||||
"""
|
"""
|
||||||
if self.lock_timeout and (
|
if self.lock_timeout and (
|
||||||
timezone.now() - self.lock_timeout > timedelta(minutes=5)
|
timezone.now() - self.lock_timeout > timedelta(minutes=5)
|
||||||
@ -1384,9 +1354,7 @@ class Page(models.Model):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def set_lock(self, user):
|
def set_lock(self, user):
|
||||||
"""
|
"""Sets a lock on the current page or raise an AlreadyLocked exception."""
|
||||||
Sets a lock on the current page or raise an AlreadyLocked exception
|
|
||||||
"""
|
|
||||||
if self.is_locked() and self.get_lock() != user:
|
if self.is_locked() and self.get_lock() != user:
|
||||||
raise AlreadyLocked("The page is already locked by someone else")
|
raise AlreadyLocked("The page is already locked by someone else")
|
||||||
self.lock_user = user
|
self.lock_user = user
|
||||||
@ -1395,41 +1363,34 @@ class Page(models.Model):
|
|||||||
# print("Locking page")
|
# print("Locking page")
|
||||||
|
|
||||||
def set_lock_recursive(self, user):
|
def set_lock_recursive(self, user):
|
||||||
"""
|
"""Locks recursively all the child pages for editing properties."""
|
||||||
Locks recursively all the child pages for editing properties
|
|
||||||
"""
|
|
||||||
for p in self.children.all():
|
for p in self.children.all():
|
||||||
p.set_lock_recursive(user)
|
p.set_lock_recursive(user)
|
||||||
self.set_lock(user)
|
self.set_lock(user)
|
||||||
|
|
||||||
def unset_lock_recursive(self):
|
def unset_lock_recursive(self):
|
||||||
"""
|
"""Unlocks recursively all the child pages."""
|
||||||
Unlocks recursively all the child pages
|
|
||||||
"""
|
|
||||||
for p in self.children.all():
|
for p in self.children.all():
|
||||||
p.unset_lock_recursive()
|
p.unset_lock_recursive()
|
||||||
self.unset_lock()
|
self.unset_lock()
|
||||||
|
|
||||||
def unset_lock(self):
|
def unset_lock(self):
|
||||||
"""Always try to unlock, even if there is no lock"""
|
"""Always try to unlock, even if there is no lock."""
|
||||||
self.lock_user = None
|
self.lock_user = None
|
||||||
self.lock_timeout = None
|
self.lock_timeout = None
|
||||||
super().save()
|
super().save()
|
||||||
# print("Unlocking page")
|
# print("Unlocking page")
|
||||||
|
|
||||||
def get_lock(self):
|
def get_lock(self):
|
||||||
"""
|
"""Returns the page's mutex containing the time and the user in a dict."""
|
||||||
Returns the page's mutex containing the time and the user in a dict
|
|
||||||
"""
|
|
||||||
if self.lock_user:
|
if self.lock_user:
|
||||||
return self.lock_user
|
return self.lock_user
|
||||||
raise NotLocked("The page is not locked and thus can not return its user")
|
raise NotLocked("The page is not locked and thus can not return its user")
|
||||||
|
|
||||||
def get_full_name(self):
|
def get_full_name(self):
|
||||||
"""
|
"""Computes the real full_name of the page based on its name and its parent's name
|
||||||
Computes the real full_name of the page based on its name and its parent's name
|
|
||||||
You can and must rely on this function when working on a page object that is not freshly fetched from the DB
|
You can and must rely on this function when working on a page object that is not freshly fetched from the DB
|
||||||
(For example when treating a Page object coming from a form)
|
(For example when treating a Page object coming from a form).
|
||||||
"""
|
"""
|
||||||
if self.parent is None:
|
if self.parent is None:
|
||||||
return self.name
|
return self.name
|
||||||
@ -1463,8 +1424,8 @@ class Page(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
class PageRev(models.Model):
|
class PageRev(models.Model):
|
||||||
"""
|
"""True content of the page.
|
||||||
This is the true content of the page.
|
|
||||||
Each page object has a revisions field that is a list of PageRev, ordered by date.
|
Each page object has a revisions field that is a list of PageRev, ordered by date.
|
||||||
my_page.revisions.last() gives the PageRev object that is the most up-to-date, and thus,
|
my_page.revisions.last() gives the PageRev object that is the most up-to-date, and thus,
|
||||||
is the real content of the page.
|
is the real content of the page.
|
||||||
@ -1492,9 +1453,6 @@ class PageRev(models.Model):
|
|||||||
self.page.unset_lock()
|
self.page.unset_lock()
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
"""
|
|
||||||
This is needed for black magic powered UpdateView's children
|
|
||||||
"""
|
|
||||||
return reverse("core:page", kwargs={"page_name": self.page._full_name})
|
return reverse("core:page", kwargs={"page_name": self.page._full_name})
|
||||||
|
|
||||||
def __getattribute__(self, attr):
|
def __getattribute__(self, attr):
|
||||||
@ -1573,9 +1531,7 @@ class Gift(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
class OperationLog(models.Model):
|
class OperationLog(models.Model):
|
||||||
"""
|
"""General purpose log object to register operations."""
|
||||||
General purpose log object to register operations
|
|
||||||
"""
|
|
||||||
|
|
||||||
date = models.DateTimeField(_("date"), auto_now_add=True)
|
date = models.DateTimeField(_("date"), auto_now_add=True)
|
||||||
label = models.CharField(_("label"), max_length=255)
|
label = models.CharField(_("label"), max_length=255)
|
||||||
|
@ -21,24 +21,26 @@
|
|||||||
#
|
#
|
||||||
#
|
#
|
||||||
|
|
||||||
"""
|
"""Collection of utils for custom migration tricks.
|
||||||
This page is useful for custom migration tricks.
|
|
||||||
Sometimes, when you need to have a migration hack and you think it can be
|
Sometimes, when you need to have a migration hack,
|
||||||
useful again, put it there, we never know if we might need the hack again.
|
and you think it can be useful again,
|
||||||
|
put it there, we never know if we might need the hack again.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from django.db import connection, migrations
|
from django.db import connection, migrations
|
||||||
|
|
||||||
|
|
||||||
class PsqlRunOnly(migrations.RunSQL):
|
class PsqlRunOnly(migrations.RunSQL):
|
||||||
"""
|
"""SQL runner for PostgreSQL-only queries.
|
||||||
This is an SQL runner that will launch the given command only if
|
|
||||||
the used DBMS is PostgreSQL.
|
|
||||||
It may be useful to run Postgres' specific SQL, or to take actions
|
It may be useful to run Postgres' specific SQL, or to take actions
|
||||||
that would be non-senses with backends other than Postgre, such
|
that would be non-senses with backends other than Postgre, such
|
||||||
as disabling particular constraints that would prevent the migration
|
as disabling particular constraints that would prevent the migration
|
||||||
to run successfully.
|
to run successfully.
|
||||||
|
|
||||||
|
If used on another DBMS than Postgres, it will be a noop.
|
||||||
|
|
||||||
See `club/migrations/0010_auto_20170912_2028.py` as an example.
|
See `club/migrations/0010_auto_20170912_2028.py` as an example.
|
||||||
Some explanations can be found here too:
|
Some explanations can be found here too:
|
||||||
https://stackoverflow.com/questions/28429933/django-migrations-using-runpython-to-commit-changes
|
https://stackoverflow.com/questions/28429933/django-migrations-using-runpython-to-commit-changes
|
||||||
|
15
core/schemas.py
Normal file
15
core/schemas.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
from ninja import ModelSchema, Schema
|
||||||
|
|
||||||
|
from core.models import User
|
||||||
|
|
||||||
|
|
||||||
|
class SimpleUserSchema(ModelSchema):
|
||||||
|
"""A schema with the minimum amount of information to represent a user."""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = User
|
||||||
|
fields = ["id", "nick_name", "first_name", "last_name"]
|
||||||
|
|
||||||
|
|
||||||
|
class MarkdownSchema(Schema):
|
||||||
|
text: str
|
@ -31,9 +31,7 @@ from django.core.files.storage import FileSystemStorage
|
|||||||
|
|
||||||
|
|
||||||
class ScssFinder(FileSystemFinder):
|
class ScssFinder(FileSystemFinder):
|
||||||
"""
|
"""Find static *.css files compiled on the fly."""
|
||||||
Find static *.css files compiled on the fly
|
|
||||||
"""
|
|
||||||
|
|
||||||
locations = []
|
locations = []
|
||||||
|
|
||||||
|
@ -35,10 +35,9 @@ from core.scss.storage import ScssFileStorage, find_file
|
|||||||
|
|
||||||
|
|
||||||
class ScssProcessor(object):
|
class ScssProcessor(object):
|
||||||
"""
|
"""If DEBUG mode enabled : compile the scss file
|
||||||
If DEBUG mode enabled : compile the scss file
|
|
||||||
Else : give the path of the corresponding css supposed to already be compiled
|
Else : give the path of the corresponding css supposed to already be compiled
|
||||||
Don't forget to use compilestatics to compile scss for production
|
Don't forget to use compilestatics to compile scss for production.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
prefix = iri_to_uri(getattr(settings, "STATIC_URL", "/static/"))
|
prefix = iri_to_uri(getattr(settings, "STATIC_URL", "/static/"))
|
||||||
|
@ -91,9 +91,9 @@ class IndexSignalProcessor(signals.BaseSignalProcessor):
|
|||||||
|
|
||||||
|
|
||||||
class BigCharFieldIndex(indexes.CharField):
|
class BigCharFieldIndex(indexes.CharField):
|
||||||
"""
|
"""Workaround to avoid xapian.InvalidArgument: Term too long (> 245).
|
||||||
Workaround to avoid xapian.InvalidArgument: Term too long (> 245)
|
|
||||||
See https://groups.google.com/forum/#!topic/django-haystack/hRJKcPNPXqw/discussion
|
See https://groups.google.com/forum/#!topic/django-haystack/hRJKcPNPXqw/discussion.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def prepare(self, term):
|
def prepare(self, term):
|
||||||
|
@ -7,9 +7,7 @@ from core.models import User
|
|||||||
|
|
||||||
@receiver(m2m_changed, sender=User.groups.through, dispatch_uid="user_groups_changed")
|
@receiver(m2m_changed, sender=User.groups.through, dispatch_uid="user_groups_changed")
|
||||||
def user_groups_changed(sender, instance: User, **kwargs):
|
def user_groups_changed(sender, instance: User, **kwargs):
|
||||||
"""
|
"""Clear the cached groups of the user."""
|
||||||
Clear the cached groups of the user
|
|
||||||
"""
|
|
||||||
# As a m2m relationship doesn't live within the model
|
# As a m2m relationship doesn't live within the model
|
||||||
# but rather on an intermediary table, there is no
|
# but rather on an intermediary table, there is no
|
||||||
# model method to override, meaning we must use
|
# model method to override, meaning we must use
|
||||||
|
BIN
core/static/core/img/promo_24.png
Normal file
BIN
core/static/core/img/promo_24.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 42 KiB |
@ -20,8 +20,12 @@
|
|||||||
code {
|
code {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
font-family: Consolas;
|
font-family: Consolas, "Andale Mono WT", "Andale Mono", "Lucida Console",
|
||||||
|
"Lucida Sans Typewriter", "DejaVu Sans Mono", "Bitstream Vera Sans Mono",
|
||||||
|
"Liberation Mono", "Nimbus Mono L", Monaco, "Courier New", Courier,
|
||||||
|
monospace;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
tab-size: 4;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border: 1px solid var(--border-lightgrey);
|
border: 1px solid var(--border-lightgrey);
|
||||||
background-color: var(--bg-lightgrey);
|
background-color: var(--bg-lightgrey);
|
||||||
@ -34,7 +38,6 @@
|
|||||||
padding: 10px;
|
padding: 10px;
|
||||||
display: block;
|
display: block;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
font-size: 1em;
|
|
||||||
|
|
||||||
@media screen and (max-width: 500px) {
|
@media screen and (max-width: 500px) {
|
||||||
margin: 10px 0;
|
margin: 10px 0;
|
||||||
@ -43,7 +46,6 @@
|
|||||||
|
|
||||||
blockquote {
|
blockquote {
|
||||||
background-color: var(--bg-lightgrey);
|
background-color: var(--bg-lightgrey);
|
||||||
border: unset;
|
|
||||||
border-left: 5px solid #ccc;
|
border-left: 5px solid #ccc;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
margin: 15px;
|
margin: 15px;
|
||||||
|
@ -24,11 +24,6 @@ $black-color: hsl(0, 0%, 17%);
|
|||||||
|
|
||||||
$faceblue: hsl(221, 44%, 41%);
|
$faceblue: hsl(221, 44%, 41%);
|
||||||
$twitblue: hsl(206, 82%, 63%);
|
$twitblue: hsl(206, 82%, 63%);
|
||||||
$pinktober: #ff5674;
|
|
||||||
$pinktober-secondary: #8a2536;
|
|
||||||
$pinktober-primary-text: white;
|
|
||||||
$pinktober-bar-closed: $pinktober-secondary;
|
|
||||||
$pinktober-bar-opened: #388e3c;
|
|
||||||
|
|
||||||
$shadow-color: rgb(223, 223, 223);
|
$shadow-color: rgb(223, 223, 223);
|
||||||
|
|
||||||
@ -48,6 +43,18 @@ body {
|
|||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button:disabled,
|
||||||
|
button:disabled:hover {
|
||||||
|
color: #fff;
|
||||||
|
background-color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.active,
|
||||||
|
button.active:hover {
|
||||||
|
color: #fff;
|
||||||
|
background-color: $secondary-color;
|
||||||
|
}
|
||||||
|
|
||||||
a.button,
|
a.button,
|
||||||
button,
|
button,
|
||||||
input[type="button"],
|
input[type="button"],
|
||||||
@ -1510,6 +1517,10 @@ $pedagogy-light-blue: #caf0ff;
|
|||||||
$pedagogy-white-text: #f0f0f0;
|
$pedagogy-white-text: #f0f0f0;
|
||||||
|
|
||||||
.pedagogy {
|
.pedagogy {
|
||||||
|
#pagination {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
&.star-not-checked {
|
&.star-not-checked {
|
||||||
color: #f7f7f7;
|
color: #f7f7f7;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
|
@ -74,7 +74,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% if bar.is_inactive() %}
|
{% if bar.is_inactive() %}
|
||||||
<i class="fa fa-question" style="color: #f39c12"></i>
|
<i class="fa fa-question" style="color: #f39c12"></i>
|
||||||
{% elif bar.is_open(): %}
|
{% elif bar.is_open %}
|
||||||
<i class="fa fa-check" style="color: #2ecc71"></i>
|
<i class="fa fa-check" style="color: #2ecc71"></i>
|
||||||
{% else %}
|
{% else %}
|
||||||
<i class="fa fa-times" style="color: #eb2f06"></i>
|
<i class="fa fa-times" style="color: #eb2f06"></i>
|
||||||
|
@ -14,26 +14,21 @@
|
|||||||
document.head.innerHTML += '<link rel="stylesheet" href="' + css + '">';
|
document.head.innerHTML += '<link rel="stylesheet" href="' + css + '">';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Custom markdown parser
|
|
||||||
function customMarkdownParser(plainText, cb) {
|
|
||||||
$.ajax({
|
|
||||||
url: "{{ markdown_api_url }}",
|
|
||||||
method: "POST",
|
|
||||||
data: { text: plainText, csrfmiddlewaretoken: getCSRFToken() },
|
|
||||||
}).done(cb);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pretty markdown input
|
// Pretty markdown input
|
||||||
const easymde = new EasyMDE({
|
const easymde = new EasyMDE({
|
||||||
element: document.getElementById("{{ widget.attrs.id }}"),
|
element: document.getElementById("{{ widget.attrs.id }}"),
|
||||||
spellChecker: false,
|
spellChecker: false,
|
||||||
autoDownloadFontAwesome: false,
|
autoDownloadFontAwesome: false,
|
||||||
previewRender: function(plainText, preview) { // Async method
|
previewRender: function (plainText, preview) {
|
||||||
clearTimeout(lastAPICall);
|
clearTimeout(lastAPICall);
|
||||||
lastAPICall = setTimeout(() => {
|
lastAPICall = setTimeout(async () => {
|
||||||
customMarkdownParser(plainText, (msg) => preview.innerHTML = msg);
|
const res = await fetch("{{ markdown_api_url }}", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ text: plainText }),
|
||||||
|
});
|
||||||
|
preview.innerHTML = await res.text();
|
||||||
}, 300);
|
}, 300);
|
||||||
return preview.innerHTML;
|
return null;
|
||||||
},
|
},
|
||||||
forceSync: true, // Avoid validation error on generic create view
|
forceSync: true, // Avoid validation error on generic create view
|
||||||
toolbar: [
|
toolbar: [
|
||||||
|
@ -84,10 +84,10 @@
|
|||||||
}
|
}
|
||||||
function download_pictures() {
|
function download_pictures() {
|
||||||
$("#download_all_pictures").prop("disabled", true);
|
$("#download_all_pictures").prop("disabled", true);
|
||||||
var xhr = new XMLHttpRequest();
|
const xhr = new XMLHttpRequest();
|
||||||
$.ajax({
|
$.ajax({
|
||||||
type: "GET",
|
type: "GET",
|
||||||
url: "{{ url('api:all_pictures_of_user', user=object.id) }}",
|
url: "{{ url('api:pictures') }}?users_identified={{ object.id }}",
|
||||||
tryCount: 0,
|
tryCount: 0,
|
||||||
xhr: function(){
|
xhr: function(){
|
||||||
return xhr;
|
return xhr;
|
||||||
|
@ -30,11 +30,11 @@ from jinja2.parser import Parser
|
|||||||
|
|
||||||
|
|
||||||
class HoneypotExtension(Extension):
|
class HoneypotExtension(Extension):
|
||||||
"""
|
"""Wrapper around the honeypot extension tag.
|
||||||
Wrapper around the honeypot extension tag
|
|
||||||
Known limitation: doesn't support arguments
|
|
||||||
|
|
||||||
Usage: {% render_honeypot_field %}
|
Known limitation: doesn't support arguments.
|
||||||
|
|
||||||
|
Usage: `{% render_honeypot_field %}`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
tags = {"render_honeypot_field"}
|
tags = {"render_honeypot_field"}
|
||||||
@ -47,12 +47,23 @@ class HoneypotExtension(Extension):
|
|||||||
|
|
||||||
def parse(self, parser: Parser) -> nodes.Output:
|
def parse(self, parser: Parser) -> nodes.Output:
|
||||||
lineno = parser.stream.expect("name:render_honeypot_field").lineno
|
lineno = parser.stream.expect("name:render_honeypot_field").lineno
|
||||||
|
key = nodes.Name("render_honeypot_field", "load", lineno=lineno)
|
||||||
|
if parser.stream.current.type != "block_end":
|
||||||
|
field_name = parser.parse_expression()
|
||||||
|
else:
|
||||||
|
field_name = nodes.Const(None)
|
||||||
call = self.call_method(
|
call = self.call_method(
|
||||||
"_render",
|
"_render",
|
||||||
[nodes.Name("render_honeypot_field", "load", lineno=lineno)],
|
[key, field_name],
|
||||||
lineno=lineno,
|
lineno=lineno,
|
||||||
)
|
)
|
||||||
return nodes.Output([nodes.MarkSafe(call)])
|
return nodes.Output([nodes.MarkSafe(call)])
|
||||||
|
|
||||||
def _render(self, render_honeypot_field: Callable[[str | None], str]):
|
def _render(
|
||||||
return render_to_string("honeypot/honeypot_field.html", render_honeypot_field())
|
self,
|
||||||
|
render_honeypot_field: Callable[[str | None], str],
|
||||||
|
field_name: str | None = None,
|
||||||
|
):
|
||||||
|
return render_to_string(
|
||||||
|
"honeypot/honeypot_field.html", render_honeypot_field(field_name=field_name)
|
||||||
|
)
|
||||||
|
@ -46,9 +46,7 @@ def markdown(text):
|
|||||||
def phonenumber(
|
def phonenumber(
|
||||||
value, country="FR", number_format=phonenumbers.PhoneNumberFormat.NATIONAL
|
value, country="FR", number_format=phonenumbers.PhoneNumberFormat.NATIONAL
|
||||||
):
|
):
|
||||||
"""
|
# collectivised from https://github.com/foundertherapy/django-phonenumber-filter.
|
||||||
This filter is kindly borrowed from https://github.com/foundertherapy/django-phonenumber-filter
|
|
||||||
"""
|
|
||||||
value = str(value)
|
value = str(value)
|
||||||
try:
|
try:
|
||||||
parsed = phonenumbers.parse(value, country)
|
parsed = phonenumbers.parse(value, country)
|
||||||
@ -59,6 +57,12 @@ def phonenumber(
|
|||||||
|
|
||||||
@register.filter(name="truncate_time")
|
@register.filter(name="truncate_time")
|
||||||
def truncate_time(value, time_unit):
|
def truncate_time(value, time_unit):
|
||||||
|
"""Remove everything in the time format lower than the specified unit.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value: the value to truncate
|
||||||
|
time_unit: the lowest unit to display
|
||||||
|
"""
|
||||||
value = str(value)
|
value = str(value)
|
||||||
return {
|
return {
|
||||||
"millis": lambda: value.split(".")[0],
|
"millis": lambda: value.split(".")[0],
|
||||||
@ -81,8 +85,6 @@ def format_timedelta(value: datetime.timedelta) -> str:
|
|||||||
|
|
||||||
@register.simple_tag()
|
@register.simple_tag()
|
||||||
def scss(path):
|
def scss(path):
|
||||||
"""
|
"""Return path of the corresponding css file after compilation."""
|
||||||
Return path of the corresponding css file after compilation
|
|
||||||
"""
|
|
||||||
processor = ScssProcessor(path)
|
processor = ScssProcessor(path)
|
||||||
return processor.get_converted_scss()
|
return processor.get_converted_scss()
|
||||||
|
144
core/tests.py
144
core/tests.py
@ -65,7 +65,7 @@ class TestUserRegistration:
|
|||||||
{"password2": "not the same as password1"},
|
{"password2": "not the same as password1"},
|
||||||
"Les deux mots de passe ne correspondent pas.",
|
"Les deux mots de passe ne correspondent pas.",
|
||||||
),
|
),
|
||||||
({"email": "not-an-email"}, "Saisissez une adresse e-mail valide."),
|
({"email": "not-an-email"}, "Saisissez une adresse de courriel valide."),
|
||||||
({"first_name": ""}, "Ce champ est obligatoire."),
|
({"first_name": ""}, "Ce champ est obligatoire."),
|
||||||
({"last_name": ""}, "Ce champ est obligatoire."),
|
({"last_name": ""}, "Ce champ est obligatoire."),
|
||||||
({"captcha_1": "WRONG_CAPTCHA"}, "CAPTCHA invalide"),
|
({"captcha_1": "WRONG_CAPTCHA"}, "CAPTCHA invalide"),
|
||||||
@ -105,7 +105,7 @@ class TestUserRegistration:
|
|||||||
def test_register_fail_with_not_existing_email(
|
def test_register_fail_with_not_existing_email(
|
||||||
self, client: Client, valid_payload, monkeypatch
|
self, client: Client, valid_payload, monkeypatch
|
||||||
):
|
):
|
||||||
"""Test that, when email is valid but doesn't actually exist, registration fails"""
|
"""Test that, when email is valid but doesn't actually exist, registration fails."""
|
||||||
|
|
||||||
def always_fail(*_args, **_kwargs):
|
def always_fail(*_args, **_kwargs):
|
||||||
raise SMTPException
|
raise SMTPException
|
||||||
@ -127,10 +127,7 @@ class TestUserLogin:
|
|||||||
return User.objects.first()
|
return User.objects.first()
|
||||||
|
|
||||||
def test_login_fail(self, client, user):
|
def test_login_fail(self, client, user):
|
||||||
"""
|
"""Should not login a user correctly."""
|
||||||
Should not login a user correctly
|
|
||||||
"""
|
|
||||||
|
|
||||||
response = client.post(
|
response = client.post(
|
||||||
reverse("core:login"),
|
reverse("core:login"),
|
||||||
{
|
{
|
||||||
@ -158,9 +155,7 @@ class TestUserLogin:
|
|||||||
assert response.wsgi_request.user.is_anonymous
|
assert response.wsgi_request.user.is_anonymous
|
||||||
|
|
||||||
def test_login_success(self, client, user):
|
def test_login_success(self, client, user):
|
||||||
"""
|
"""Should login a user correctly."""
|
||||||
Should login a user correctly
|
|
||||||
"""
|
|
||||||
response = client.post(
|
response = client.post(
|
||||||
reverse("core:login"),
|
reverse("core:login"),
|
||||||
{
|
{
|
||||||
@ -210,19 +205,19 @@ class TestUserLogin:
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_custom_markdown_syntax(md, html):
|
def test_custom_markdown_syntax(md, html):
|
||||||
"""Test the homemade markdown syntax"""
|
"""Test the homemade markdown syntax."""
|
||||||
assert markdown(md) == f"<p>{html}</p>\n"
|
assert markdown(md) == f"<p>{html}</p>\n"
|
||||||
|
|
||||||
|
|
||||||
def test_full_markdown_syntax():
|
def test_full_markdown_syntax():
|
||||||
doc_path = Path(settings.BASE_DIR) / "doc"
|
syntax_path = Path(settings.BASE_DIR) / "core" / "fixtures"
|
||||||
md = (doc_path / "SYNTAX.md").read_text()
|
md = (syntax_path / "SYNTAX.md").read_text()
|
||||||
html = (doc_path / "SYNTAX.html").read_text()
|
html = (syntax_path / "SYNTAX.html").read_text()
|
||||||
result = markdown(md)
|
result = markdown(md)
|
||||||
assert result == html
|
assert result == html
|
||||||
|
|
||||||
|
|
||||||
class PageHandlingTest(TestCase):
|
class TestPageHandling(TestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
cls.root = User.objects.get(username="root")
|
cls.root = User.objects.get(username="root")
|
||||||
@ -233,7 +228,6 @@ class PageHandlingTest(TestCase):
|
|||||||
|
|
||||||
def test_create_page_ok(self):
|
def test_create_page_ok(self):
|
||||||
"""Should create a page correctly."""
|
"""Should create a page correctly."""
|
||||||
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse("core:page_new"),
|
reverse("core:page_new"),
|
||||||
{"parent": "", "name": "guy", "owner_group": self.root_group.id},
|
{"parent": "", "name": "guy", "owner_group": self.root_group.id},
|
||||||
@ -274,9 +268,7 @@ class PageHandlingTest(TestCase):
|
|||||||
assert '<a href="/page/guy/bibou/">' in str(response.content)
|
assert '<a href="/page/guy/bibou/">' in str(response.content)
|
||||||
|
|
||||||
def test_access_child_page_ok(self):
|
def test_access_child_page_ok(self):
|
||||||
"""
|
"""Should display a page correctly."""
|
||||||
Should display a page correctly
|
|
||||||
"""
|
|
||||||
parent = Page(name="guy", owner_group=self.root_group)
|
parent = Page(name="guy", owner_group=self.root_group)
|
||||||
parent.save(force_lock=True)
|
parent.save(force_lock=True)
|
||||||
page = Page(name="bibou", owner_group=self.root_group, parent=parent)
|
page = Page(name="bibou", owner_group=self.root_group, parent=parent)
|
||||||
@ -289,18 +281,14 @@ class PageHandlingTest(TestCase):
|
|||||||
self.assertIn('<a href="/page/guy/bibou/edit/">', html)
|
self.assertIn('<a href="/page/guy/bibou/edit/">', html)
|
||||||
|
|
||||||
def test_access_page_not_found(self):
|
def test_access_page_not_found(self):
|
||||||
"""
|
"""Should not display a page correctly."""
|
||||||
Should not display a page correctly
|
|
||||||
"""
|
|
||||||
response = self.client.get(reverse("core:page", kwargs={"page_name": "swagg"}))
|
response = self.client.get(reverse("core:page", kwargs={"page_name": "swagg"}))
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
html = response.content.decode()
|
html = response.content.decode()
|
||||||
self.assertIn('<a href="/page/create/?page=swagg">', html)
|
self.assertIn('<a href="/page/create/?page=swagg">', html)
|
||||||
|
|
||||||
def test_create_page_markdown_safe(self):
|
def test_create_page_markdown_safe(self):
|
||||||
"""
|
"""Should format the markdown and escape html correctly."""
|
||||||
Should format the markdown and escape html correctly
|
|
||||||
"""
|
|
||||||
self.client.post(
|
self.client.post(
|
||||||
reverse("core:page_new"), {"parent": "", "name": "guy", "owner_group": "1"}
|
reverse("core:page_new"), {"parent": "", "name": "guy", "owner_group": "1"}
|
||||||
)
|
)
|
||||||
@ -322,7 +310,6 @@ http://git.an
|
|||||||
)
|
)
|
||||||
response = self.client.get(reverse("core:page", kwargs={"page_name": "guy"}))
|
response = self.client.get(reverse("core:page", kwargs={"page_name": "guy"}))
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
print(response.content.decode())
|
|
||||||
expected = """
|
expected = """
|
||||||
<p>Guy <em>bibou</em></p>
|
<p>Guy <em>bibou</em></p>
|
||||||
<p><a href="http://git.an">http://git.an</a></p>
|
<p><a href="http://git.an">http://git.an</a></p>
|
||||||
@ -333,28 +320,67 @@ http://git.an
|
|||||||
assertInHTML(expected, response.content.decode())
|
assertInHTML(expected, response.content.decode())
|
||||||
|
|
||||||
|
|
||||||
class UserToolsTest:
|
@pytest.mark.django_db
|
||||||
|
class TestUserTools:
|
||||||
def test_anonymous_user_unauthorized(self, client):
|
def test_anonymous_user_unauthorized(self, client):
|
||||||
"""An anonymous user shouldn't have access to the tools page"""
|
"""An anonymous user shouldn't have access to the tools page."""
|
||||||
response = client.get(reverse("core:user_tools"))
|
response = client.get(reverse("core:user_tools"))
|
||||||
assert response.status_code == 403
|
assertRedirects(
|
||||||
|
response,
|
||||||
|
expected_url=f"/login?next=%2Fuser%2Ftools%2F",
|
||||||
|
target_status_code=301,
|
||||||
|
)
|
||||||
|
|
||||||
@pytest.mark.parametrize("username", ["guy", "root", "skia", "comunity"])
|
@pytest.mark.parametrize("username", ["guy", "root", "skia", "comunity"])
|
||||||
def test_page_is_working(self, client, username):
|
def test_page_is_working(self, client, username):
|
||||||
"""All existing users should be able to see the test page"""
|
"""All existing users should be able to see the test page."""
|
||||||
# Test for simple user
|
# Test for simple user
|
||||||
client.force_login(User.objects.get(username=username))
|
client.force_login(User.objects.get(username=username))
|
||||||
response = client.get(reverse("core:user_tools"))
|
response = client.get(reverse("core:user_tools"))
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
class TestUserPicture:
|
||||||
|
def test_anonymous_user_unauthorized(self, client):
|
||||||
|
"""An anonymous user shouldn't have access to an user's photo page."""
|
||||||
|
response = client.get(
|
||||||
|
reverse(
|
||||||
|
"core:user_pictures",
|
||||||
|
kwargs={"user_id": User.objects.get(username="sli").pk},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assert response.status_code == 403
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("username", "status"),
|
||||||
|
[
|
||||||
|
("guy", 403),
|
||||||
|
("root", 200),
|
||||||
|
("skia", 200),
|
||||||
|
("sli", 200),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_page_is_working(self, client, username, status):
|
||||||
|
"""Only user that subscribed (or admins) should be able to see the page."""
|
||||||
|
# Test for simple user
|
||||||
|
client.force_login(User.objects.get(username=username))
|
||||||
|
response = client.get(
|
||||||
|
reverse(
|
||||||
|
"core:user_pictures",
|
||||||
|
kwargs={"user_id": User.objects.get(username="sli").pk},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assert response.status_code == status
|
||||||
|
|
||||||
|
|
||||||
# TODO: many tests on the pages:
|
# TODO: many tests on the pages:
|
||||||
# - renaming a page
|
# - renaming a page
|
||||||
# - changing a page's parent --> check that page's children's full_name
|
# - changing a page's parent --> check that page's children's full_name
|
||||||
# - changing the different groups of the page
|
# - changing the different groups of the page
|
||||||
|
|
||||||
|
|
||||||
class FileHandlingTest(TestCase):
|
class TestFileHandling(TestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
cls.subscriber = User.objects.get(username="subscriber")
|
cls.subscriber = User.objects.get(username="subscriber")
|
||||||
@ -390,10 +416,9 @@ class FileHandlingTest(TestCase):
|
|||||||
assert "ls</a>" in str(response.content)
|
assert "ls</a>" in str(response.content)
|
||||||
|
|
||||||
|
|
||||||
class UserIsInGroupTest(TestCase):
|
class TestUserIsInGroup(TestCase):
|
||||||
"""
|
"""Test that the User.is_in_group() and AnonymousUser.is_in_group()
|
||||||
Test that the User.is_in_group() and AnonymousUser.is_in_group()
|
work as intended.
|
||||||
work as intended
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -450,30 +475,24 @@ class UserIsInGroupTest(TestCase):
|
|||||||
assert user.is_in_group(name=meta_groups_members) is False
|
assert user.is_in_group(name=meta_groups_members) is False
|
||||||
|
|
||||||
def test_anonymous_user(self):
|
def test_anonymous_user(self):
|
||||||
"""
|
"""Test that anonymous users are only in the public group."""
|
||||||
Test that anonymous users are only in the public group
|
|
||||||
"""
|
|
||||||
user = AnonymousUser()
|
user = AnonymousUser()
|
||||||
self.assert_only_in_public_group(user)
|
self.assert_only_in_public_group(user)
|
||||||
|
|
||||||
def test_not_subscribed_user(self):
|
def test_not_subscribed_user(self):
|
||||||
"""
|
"""Test that users who never subscribed are only in the public group."""
|
||||||
Test that users who never subscribed are only in the public group
|
|
||||||
"""
|
|
||||||
self.assert_only_in_public_group(self.toto)
|
self.assert_only_in_public_group(self.toto)
|
||||||
|
|
||||||
def test_wrong_parameter_fail(self):
|
def test_wrong_parameter_fail(self):
|
||||||
"""
|
"""Test that when neither the pk nor the name argument is given,
|
||||||
Test that when neither the pk nor the name argument is given,
|
the function raises a ValueError.
|
||||||
the function raises a ValueError
|
|
||||||
"""
|
"""
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValueError):
|
||||||
self.toto.is_in_group()
|
self.toto.is_in_group()
|
||||||
|
|
||||||
def test_number_queries(self):
|
def test_number_queries(self):
|
||||||
"""
|
"""Test that the number of db queries is stable
|
||||||
Test that the number of db queries is stable
|
and that less queries are made when making a new call.
|
||||||
and that less queries are made when making a new call
|
|
||||||
"""
|
"""
|
||||||
# make sure Skia is in at least one group
|
# make sure Skia is in at least one group
|
||||||
self.skia.groups.add(Group.objects.first().pk)
|
self.skia.groups.add(Group.objects.first().pk)
|
||||||
@ -497,9 +516,8 @@ class UserIsInGroupTest(TestCase):
|
|||||||
self.skia.is_in_group(pk=group_not_in.id)
|
self.skia.is_in_group(pk=group_not_in.id)
|
||||||
|
|
||||||
def test_cache_properly_cleared_membership(self):
|
def test_cache_properly_cleared_membership(self):
|
||||||
"""
|
"""Test that when the membership of a user end,
|
||||||
Test that when the membership of a user end,
|
the cache is properly invalidated.
|
||||||
the cache is properly invalidated
|
|
||||||
"""
|
"""
|
||||||
membership = Membership.objects.create(
|
membership = Membership.objects.create(
|
||||||
club=self.club, user=self.toto, end_date=None
|
club=self.club, user=self.toto, end_date=None
|
||||||
@ -515,9 +533,8 @@ class UserIsInGroupTest(TestCase):
|
|||||||
assert self.toto.is_in_group(name=meta_groups_members) is False
|
assert self.toto.is_in_group(name=meta_groups_members) is False
|
||||||
|
|
||||||
def test_cache_properly_cleared_group(self):
|
def test_cache_properly_cleared_group(self):
|
||||||
"""
|
"""Test that when a user is removed from a group,
|
||||||
Test that when a user is removed from a group,
|
the is_in_group_method return False when calling it again.
|
||||||
the is_in_group_method return False when calling it again
|
|
||||||
"""
|
"""
|
||||||
# testing with pk
|
# testing with pk
|
||||||
self.toto.groups.add(self.com_admin.pk)
|
self.toto.groups.add(self.com_admin.pk)
|
||||||
@ -534,14 +551,13 @@ class UserIsInGroupTest(TestCase):
|
|||||||
assert self.toto.is_in_group(name="SAS admin") is False
|
assert self.toto.is_in_group(name="SAS admin") is False
|
||||||
|
|
||||||
def test_not_existing_group(self):
|
def test_not_existing_group(self):
|
||||||
"""
|
"""Test that searching for a not existing group
|
||||||
Test that searching for a not existing group
|
returns False.
|
||||||
returns False
|
|
||||||
"""
|
"""
|
||||||
assert self.skia.is_in_group(name="This doesn't exist") is False
|
assert self.skia.is_in_group(name="This doesn't exist") is False
|
||||||
|
|
||||||
|
|
||||||
class DateUtilsTest(TestCase):
|
class TestDateUtils(TestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
cls.autumn_month = settings.SITH_SEMESTER_START_AUTUMN[0]
|
cls.autumn_month = settings.SITH_SEMESTER_START_AUTUMN[0]
|
||||||
@ -557,9 +573,7 @@ class DateUtilsTest(TestCase):
|
|||||||
cls.spring_first_day = date(2023, cls.spring_month, cls.spring_day)
|
cls.spring_first_day = date(2023, cls.spring_month, cls.spring_day)
|
||||||
|
|
||||||
def test_get_semester(self):
|
def test_get_semester(self):
|
||||||
"""
|
"""Test that the get_semester function returns the correct semester string."""
|
||||||
Test that the get_semester function returns the correct semester string
|
|
||||||
"""
|
|
||||||
assert get_semester_code(self.autumn_semester_january) == "A24"
|
assert get_semester_code(self.autumn_semester_january) == "A24"
|
||||||
assert get_semester_code(self.autumn_semester_september) == "A24"
|
assert get_semester_code(self.autumn_semester_september) == "A24"
|
||||||
assert get_semester_code(self.autumn_first_day) == "A24"
|
assert get_semester_code(self.autumn_first_day) == "A24"
|
||||||
@ -568,9 +582,7 @@ class DateUtilsTest(TestCase):
|
|||||||
assert get_semester_code(self.spring_first_day) == "P23"
|
assert get_semester_code(self.spring_first_day) == "P23"
|
||||||
|
|
||||||
def test_get_start_of_semester_fixed_date(self):
|
def test_get_start_of_semester_fixed_date(self):
|
||||||
"""
|
"""Test that the get_start_of_semester correctly the starting date of the semester."""
|
||||||
Test that the get_start_of_semester correctly the starting date of the semester.
|
|
||||||
"""
|
|
||||||
automn_2024 = date(2024, self.autumn_month, self.autumn_day)
|
automn_2024 = date(2024, self.autumn_month, self.autumn_day)
|
||||||
assert get_start_of_semester(self.autumn_semester_january) == automn_2024
|
assert get_start_of_semester(self.autumn_semester_january) == automn_2024
|
||||||
assert get_start_of_semester(self.autumn_semester_september) == automn_2024
|
assert get_start_of_semester(self.autumn_semester_september) == automn_2024
|
||||||
@ -581,9 +593,8 @@ class DateUtilsTest(TestCase):
|
|||||||
assert get_start_of_semester(self.spring_first_day) == spring_2023
|
assert get_start_of_semester(self.spring_first_day) == spring_2023
|
||||||
|
|
||||||
def test_get_start_of_semester_today(self):
|
def test_get_start_of_semester_today(self):
|
||||||
"""
|
"""Test that the get_start_of_semester returns the start of the current semester
|
||||||
Test that the get_start_of_semester returns the start of the current semester
|
when no date is given.
|
||||||
when no date is given
|
|
||||||
"""
|
"""
|
||||||
with freezegun.freeze_time(self.autumn_semester_september):
|
with freezegun.freeze_time(self.autumn_semester_september):
|
||||||
assert get_start_of_semester() == self.autumn_first_day
|
assert get_start_of_semester() == self.autumn_first_day
|
||||||
@ -592,8 +603,7 @@ class DateUtilsTest(TestCase):
|
|||||||
assert get_start_of_semester() == self.spring_first_day
|
assert get_start_of_semester() == self.spring_first_day
|
||||||
|
|
||||||
def test_get_start_of_semester_changing_date(self):
|
def test_get_start_of_semester_changing_date(self):
|
||||||
"""
|
"""Test that the get_start_of_semester correctly gives the starting date of the semester,
|
||||||
Test that the get_start_of_semester correctly gives the starting date of the semester,
|
|
||||||
even when the semester changes while the server isn't restarted.
|
even when the semester changes while the server isn't restarted.
|
||||||
"""
|
"""
|
||||||
spring_2023 = date(2023, self.spring_month, self.spring_day)
|
spring_2023 = date(2023, self.spring_month, self.spring_day)
|
||||||
|
@ -25,15 +25,14 @@ from typing import Optional
|
|||||||
import PIL
|
import PIL
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.files.base import ContentFile
|
from django.core.files.base import ContentFile
|
||||||
|
from django.http import HttpRequest
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from PIL import ExifTags
|
from PIL import ExifTags
|
||||||
from PIL.Image import Resampling
|
from PIL.Image import Resampling
|
||||||
|
|
||||||
|
|
||||||
def get_git_revision_short_hash() -> str:
|
def get_git_revision_short_hash() -> str:
|
||||||
"""
|
"""Return the short hash of the current commit."""
|
||||||
Return the short hash of the current commit
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
output = subprocess.check_output(["git", "rev-parse", "--short", "HEAD"])
|
output = subprocess.check_output(["git", "rev-parse", "--short", "HEAD"])
|
||||||
if isinstance(output, bytes):
|
if isinstance(output, bytes):
|
||||||
@ -44,8 +43,7 @@ def get_git_revision_short_hash() -> str:
|
|||||||
|
|
||||||
|
|
||||||
def get_start_of_semester(today: Optional[date] = None) -> date:
|
def get_start_of_semester(today: Optional[date] = None) -> date:
|
||||||
"""
|
"""Return the date of the start of the semester of the given date.
|
||||||
Return the date of the start of the semester of the given date.
|
|
||||||
If no date is given, return the start date of the current semester.
|
If no date is given, return the start date of the current semester.
|
||||||
|
|
||||||
The current semester is computed as follows:
|
The current semester is computed as follows:
|
||||||
@ -54,8 +52,11 @@ def get_start_of_semester(today: Optional[date] = None) -> date:
|
|||||||
- If the date is between 01/01 and 15/02 => Autumn semester of the previous year.
|
- If the date is between 01/01 and 15/02 => Autumn semester of the previous year.
|
||||||
- If the date is between 15/02 and 15/08 => Spring semester
|
- If the date is between 15/02 and 15/08 => Spring semester
|
||||||
|
|
||||||
:param today: the date to use to compute the semester. If None, use today's date.
|
Args:
|
||||||
:return: the date of the start of the semester
|
today: the date to use to compute the semester. If None, use today's date.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
the date of the start of the semester
|
||||||
"""
|
"""
|
||||||
if today is None:
|
if today is None:
|
||||||
today = timezone.now().date()
|
today = timezone.now().date()
|
||||||
@ -72,16 +73,18 @@ def get_start_of_semester(today: Optional[date] = None) -> date:
|
|||||||
|
|
||||||
|
|
||||||
def get_semester_code(d: Optional[date] = None) -> str:
|
def get_semester_code(d: Optional[date] = None) -> str:
|
||||||
"""
|
"""Return the semester code of the given date.
|
||||||
Return the semester code of the given date.
|
|
||||||
If no date is given, return the semester code of the current semester.
|
If no date is given, return the semester code of the current semester.
|
||||||
|
|
||||||
The semester code is an upper letter (A for autumn, P for spring),
|
The semester code is an upper letter (A for autumn, P for spring),
|
||||||
followed by the last two digits of the year.
|
followed by the last two digits of the year.
|
||||||
For example, the autumn semester of 2018 is "A18".
|
For example, the autumn semester of 2018 is "A18".
|
||||||
|
|
||||||
:param d: the date to use to compute the semester. If None, use today's date.
|
Args:
|
||||||
:return: the semester code corresponding to the given date
|
d: the date to use to compute the semester. If None, use today's date.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
the semester code corresponding to the given date
|
||||||
"""
|
"""
|
||||||
if d is None:
|
if d is None:
|
||||||
d = timezone.now().date()
|
d = timezone.now().date()
|
||||||
@ -147,8 +150,15 @@ def exif_auto_rotate(image):
|
|||||||
return image
|
return image
|
||||||
|
|
||||||
|
|
||||||
def doku_to_markdown(text):
|
def doku_to_markdown(text: str) -> str:
|
||||||
"""This is a quite correct doku translator"""
|
"""Convert doku text to the corresponding markdown.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text: the doku text to convert
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The converted markdown text
|
||||||
|
"""
|
||||||
text = re.sub(
|
text = re.sub(
|
||||||
r"([^:]|^)\/\/(.*?)\/\/", r"*\2*", text
|
r"([^:]|^)\/\/(.*?)\/\/", r"*\2*", text
|
||||||
) # Italic (prevents protocol:// conflict)
|
) # Italic (prevents protocol:// conflict)
|
||||||
@ -235,7 +245,14 @@ def doku_to_markdown(text):
|
|||||||
|
|
||||||
|
|
||||||
def bbcode_to_markdown(text):
|
def bbcode_to_markdown(text):
|
||||||
"""This is a very basic BBcode translator"""
|
"""Convert bbcode text to the corresponding markdown.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text: the bbcode text to convert
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The converted markdown text
|
||||||
|
"""
|
||||||
text = re.sub(r"\[b\](.*?)\[\/b\]", r"**\1**", text, flags=re.DOTALL) # Bold
|
text = re.sub(r"\[b\](.*?)\[\/b\]", r"**\1**", text, flags=re.DOTALL) # Bold
|
||||||
text = re.sub(r"\[i\](.*?)\[\/i\]", r"*\1*", text, flags=re.DOTALL) # Italic
|
text = re.sub(r"\[i\](.*?)\[\/i\]", r"*\1*", text, flags=re.DOTALL) # Italic
|
||||||
text = re.sub(r"\[u\](.*?)\[\/u\]", r"__\1__", text, flags=re.DOTALL) # Underline
|
text = re.sub(r"\[u\](.*?)\[\/u\]", r"__\1__", text, flags=re.DOTALL) # Underline
|
||||||
@ -281,3 +298,16 @@ def bbcode_to_markdown(text):
|
|||||||
new_text.append(line)
|
new_text.append(line)
|
||||||
|
|
||||||
return "\n".join(new_text)
|
return "\n".join(new_text)
|
||||||
|
|
||||||
|
|
||||||
|
def get_client_ip(request: HttpRequest) -> str | None:
|
||||||
|
headers = (
|
||||||
|
"X_FORWARDED_FOR", # Common header for proixes
|
||||||
|
"FORWARDED", # Standard header defined by RFC 7239.
|
||||||
|
"REMOTE_ADDR", # Default IP Address (direct connection)
|
||||||
|
)
|
||||||
|
for header in headers:
|
||||||
|
if (ip := request.META.get(header)) is not None:
|
||||||
|
return ip
|
||||||
|
|
||||||
|
return None
|
||||||
|
@ -23,7 +23,9 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
import types
|
import types
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from django.contrib.auth.mixins import AccessMixin
|
||||||
from django.core.exceptions import (
|
from django.core.exceptions import (
|
||||||
ImproperlyConfigured,
|
ImproperlyConfigured,
|
||||||
PermissionDenied,
|
PermissionDenied,
|
||||||
@ -39,6 +41,7 @@ from django.views.generic.detail import SingleObjectMixin
|
|||||||
from django.views.generic.edit import FormView
|
from django.views.generic.edit import FormView
|
||||||
from sentry_sdk import last_event_id
|
from sentry_sdk import last_event_id
|
||||||
|
|
||||||
|
from core.models import User
|
||||||
from core.views.forms import LoginForm
|
from core.views.forms import LoginForm
|
||||||
|
|
||||||
|
|
||||||
@ -60,60 +63,63 @@ def internal_servor_error(request):
|
|||||||
return HttpResponseServerError(render(request, "core/500.jinja"))
|
return HttpResponseServerError(render(request, "core/500.jinja"))
|
||||||
|
|
||||||
|
|
||||||
def can_edit_prop(obj, user):
|
def can_edit_prop(obj: Any, user: User) -> bool:
|
||||||
"""
|
"""Can the user edit the properties of the object.
|
||||||
:param obj: Object to test for permission
|
|
||||||
:param user: core.models.User to test permissions against
|
|
||||||
:return: if user is authorized to edit object properties
|
|
||||||
:rtype: bool
|
|
||||||
|
|
||||||
:Example:
|
Args:
|
||||||
|
obj: Object to test for permission
|
||||||
|
user: core.models.User to test permissions against
|
||||||
|
|
||||||
.. code-block:: python
|
Returns:
|
||||||
|
True if user is authorized to edit object properties else False
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
```python
|
||||||
if not can_edit_prop(self.object ,request.user):
|
if not can_edit_prop(self.object ,request.user):
|
||||||
raise PermissionDenied
|
raise PermissionDenied
|
||||||
|
```
|
||||||
"""
|
"""
|
||||||
if obj is None or user.is_owner(obj):
|
if obj is None or user.is_owner(obj):
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def can_edit(obj, user):
|
def can_edit(obj: Any, user: User):
|
||||||
"""
|
"""Can the user edit the object.
|
||||||
:param obj: Object to test for permission
|
|
||||||
:param user: core.models.User to test permissions against
|
|
||||||
:return: if user is authorized to edit object
|
|
||||||
:rtype: bool
|
|
||||||
|
|
||||||
:Example:
|
Args:
|
||||||
|
obj: Object to test for permission
|
||||||
|
user: core.models.User to test permissions against
|
||||||
|
|
||||||
.. code-block:: python
|
Returns:
|
||||||
|
True if user is authorized to edit object else False
|
||||||
|
|
||||||
if not can_edit(self.object ,request.user):
|
Examples:
|
||||||
|
```python
|
||||||
|
if not can_edit(self.object, request.user):
|
||||||
raise PermissionDenied
|
raise PermissionDenied
|
||||||
|
```
|
||||||
"""
|
"""
|
||||||
if obj is None or user.can_edit(obj):
|
if obj is None or user.can_edit(obj):
|
||||||
return True
|
return True
|
||||||
return can_edit_prop(obj, user)
|
return can_edit_prop(obj, user)
|
||||||
|
|
||||||
|
|
||||||
def can_view(obj, user):
|
def can_view(obj: Any, user: User):
|
||||||
"""
|
"""Can the user see the object.
|
||||||
:param obj: Object to test for permission
|
|
||||||
:param user: core.models.User to test permissions against
|
|
||||||
:return: if user is authorized to see object
|
|
||||||
:rtype: bool
|
|
||||||
|
|
||||||
:Example:
|
Args:
|
||||||
|
obj: Object to test for permission
|
||||||
|
user: core.models.User to test permissions against
|
||||||
|
|
||||||
.. code-block:: python
|
Returns:
|
||||||
|
True if user is authorized to see object else False
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
```python
|
||||||
if not can_view(self.object ,request.user):
|
if not can_view(self.object ,request.user):
|
||||||
raise PermissionDenied
|
raise PermissionDenied
|
||||||
|
```
|
||||||
"""
|
"""
|
||||||
if obj is None or user.can_view(obj):
|
if obj is None or user.can_view(obj):
|
||||||
return True
|
return True
|
||||||
@ -121,20 +127,22 @@ def can_view(obj, user):
|
|||||||
|
|
||||||
|
|
||||||
class GenericContentPermissionMixinBuilder(View):
|
class GenericContentPermissionMixinBuilder(View):
|
||||||
"""
|
"""Used to build permission mixins.
|
||||||
Used to build permission mixins
|
|
||||||
This view protect any child view that would be showing an object that is restricted based
|
This view protect any child view that would be showing an object that is restricted based
|
||||||
on two properties
|
on two properties.
|
||||||
|
|
||||||
:prop permission_function: function to test permission with, takes an object and an user an return a bool
|
Attributes:
|
||||||
:prop raised_error: permission to be raised
|
raised_error: permission to be raised
|
||||||
|
|
||||||
:raises: raised_error
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
permission_function = lambda obj, user: False
|
|
||||||
raised_error = PermissionDenied
|
raised_error = PermissionDenied
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def permission_function(obj: Any, user: User) -> bool:
|
||||||
|
"""Function to test permission with."""
|
||||||
|
return False
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_permission_function(cls, obj, user):
|
def get_permission_function(cls, obj, user):
|
||||||
return cls.permission_function(obj, user)
|
return cls.permission_function(obj, user)
|
||||||
@ -162,11 +170,12 @@ class GenericContentPermissionMixinBuilder(View):
|
|||||||
|
|
||||||
|
|
||||||
class CanCreateMixin(View):
|
class CanCreateMixin(View):
|
||||||
"""
|
"""Protect any child view that would create an object.
|
||||||
This view is made to protect any child view that would create an object, and thus, that can not be protected by any
|
|
||||||
of the following mixin
|
|
||||||
|
|
||||||
:raises: PermissionDenied
|
Raises:
|
||||||
|
PermissionDenied:
|
||||||
|
If the user has not the necessary permission
|
||||||
|
to create the object of the view.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def dispatch(self, request, *arg, **kwargs):
|
def dispatch(self, request, *arg, **kwargs):
|
||||||
@ -183,55 +192,54 @@ class CanCreateMixin(View):
|
|||||||
|
|
||||||
|
|
||||||
class CanEditPropMixin(GenericContentPermissionMixinBuilder):
|
class CanEditPropMixin(GenericContentPermissionMixinBuilder):
|
||||||
"""
|
"""Ensure the user has owner permissions on the child view object.
|
||||||
This view is made to protect any child view that would be showing some properties of an object that are restricted
|
|
||||||
to only the owner group of the given object.
|
|
||||||
In other word, you can make a view with this view as parent, and it would be retricted to the users that are in the
|
|
||||||
object's owner_group
|
|
||||||
|
|
||||||
:raises: PermissionDenied
|
In other word, you can make a view with this view as parent,
|
||||||
|
and it will be retricted to the users that are in the
|
||||||
|
object's owner_group or that pass the `obj.can_be_viewed_by` test.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
PermissionDenied: If the user cannot see the object
|
||||||
"""
|
"""
|
||||||
|
|
||||||
permission_function = can_edit_prop
|
permission_function = can_edit_prop
|
||||||
|
|
||||||
|
|
||||||
class CanEditMixin(GenericContentPermissionMixinBuilder):
|
class CanEditMixin(GenericContentPermissionMixinBuilder):
|
||||||
"""
|
"""Ensure the user has permission to edit this view's object.
|
||||||
This view makes exactly the same thing as its direct parent, but checks the group on the edit_groups field of the
|
|
||||||
object
|
|
||||||
|
|
||||||
:raises: PermissionDenied
|
Raises:
|
||||||
|
PermissionDenied: if the user cannot edit this view's object.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
permission_function = can_edit
|
permission_function = can_edit
|
||||||
|
|
||||||
|
|
||||||
class CanViewMixin(GenericContentPermissionMixinBuilder):
|
class CanViewMixin(GenericContentPermissionMixinBuilder):
|
||||||
"""
|
"""Ensure the user has permission to view this view's object.
|
||||||
This view still makes exactly the same thing as its direct parent, but checks the group on the view_groups field of
|
|
||||||
the object
|
|
||||||
|
|
||||||
:raises: PermissionDenied
|
Raises:
|
||||||
|
PermissionDenied: if the user cannot edit this view's object.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
permission_function = can_view
|
permission_function = can_view
|
||||||
|
|
||||||
|
|
||||||
class UserIsRootMixin(GenericContentPermissionMixinBuilder):
|
class UserIsRootMixin(GenericContentPermissionMixinBuilder):
|
||||||
"""
|
"""Allow only root admins.
|
||||||
This view check if the user is root
|
|
||||||
|
|
||||||
:raises: PermissionDenied
|
Raises:
|
||||||
|
PermissionDenied: if the user isn't root
|
||||||
"""
|
"""
|
||||||
|
|
||||||
permission_function = lambda obj, user: user.is_root
|
permission_function = lambda obj, user: user.is_root
|
||||||
|
|
||||||
|
|
||||||
class FormerSubscriberMixin(View):
|
class FormerSubscriberMixin(AccessMixin):
|
||||||
"""
|
"""Check if the user was at least an old subscriber.
|
||||||
This view check if the user was at least an old subscriber
|
|
||||||
|
|
||||||
:raises: PermissionDenied
|
Raises:
|
||||||
|
PermissionDenied: if the user never subscribed.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
@ -240,23 +248,15 @@ class FormerSubscriberMixin(View):
|
|||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class UserIsLoggedMixin(View):
|
class SubscriberMixin(AccessMixin):
|
||||||
"""
|
|
||||||
This view check if the user is logged
|
|
||||||
|
|
||||||
:raises: PermissionDenied
|
|
||||||
"""
|
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
if request.user.is_anonymous:
|
if not request.user.is_subscribed:
|
||||||
raise PermissionDenied
|
return self.handle_no_permission()
|
||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class TabedViewMixin(View):
|
class TabedViewMixin(View):
|
||||||
"""
|
"""Basic functions for displaying tabs in the template."""
|
||||||
This view provide the basic functions for displaying tabs in the template
|
|
||||||
"""
|
|
||||||
|
|
||||||
def get_tabs_title(self):
|
def get_tabs_title(self):
|
||||||
if hasattr(self, "tabs_title"):
|
if hasattr(self, "tabs_title"):
|
||||||
@ -299,7 +299,7 @@ class QuickNotifMixin:
|
|||||||
return ret
|
return ret
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
"""Add quick notifications to context"""
|
"""Add quick notifications to context."""
|
||||||
kwargs = super().get_context_data(**kwargs)
|
kwargs = super().get_context_data(**kwargs)
|
||||||
kwargs["quick_notifs"] = []
|
kwargs["quick_notifs"] = []
|
||||||
for n in self.quick_notif_list:
|
for n in self.quick_notif_list:
|
||||||
@ -312,21 +312,15 @@ class QuickNotifMixin:
|
|||||||
|
|
||||||
|
|
||||||
class DetailFormView(SingleObjectMixin, FormView):
|
class DetailFormView(SingleObjectMixin, FormView):
|
||||||
"""
|
"""Class that allow both a detail view and a form view."""
|
||||||
Class that allow both a detail view and a form view
|
|
||||||
"""
|
|
||||||
|
|
||||||
def get_object(self):
|
def get_object(self):
|
||||||
"""
|
"""Get current group from id in url."""
|
||||||
Get current group from id in url
|
|
||||||
"""
|
|
||||||
return self.cached_object
|
return self.cached_object
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def cached_object(self):
|
def cached_object(self):
|
||||||
"""
|
"""Optimisation on group retrieval."""
|
||||||
Optimisation on group retrieval
|
|
||||||
"""
|
|
||||||
return super().get_object()
|
return super().get_object()
|
||||||
|
|
||||||
|
|
||||||
|
@ -42,8 +42,7 @@ from counter.models import Counter
|
|||||||
|
|
||||||
|
|
||||||
def send_file(request, file_id, file_class=SithFile, file_attr="file"):
|
def send_file(request, file_id, file_class=SithFile, file_attr="file"):
|
||||||
"""
|
"""Send a file through Django without loading the whole file into
|
||||||
Send a file through Django without loading the whole file into
|
|
||||||
memory at once. The FileWrapper will turn the file object into an
|
memory at once. The FileWrapper will turn the file object into an
|
||||||
iterator for chunks of 8KB.
|
iterator for chunks of 8KB.
|
||||||
"""
|
"""
|
||||||
@ -268,7 +267,7 @@ class FileEditPropView(CanEditPropMixin, UpdateView):
|
|||||||
|
|
||||||
|
|
||||||
class FileView(CanViewMixin, DetailView, FormMixin):
|
class FileView(CanViewMixin, DetailView, FormMixin):
|
||||||
"""This class handle the upload of new files into a folder"""
|
"""Handle the upload of new files into a folder."""
|
||||||
|
|
||||||
model = SithFile
|
model = SithFile
|
||||||
pk_url_kwarg = "file_id"
|
pk_url_kwarg = "file_id"
|
||||||
@ -278,8 +277,8 @@ class FileView(CanViewMixin, DetailView, FormMixin):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def handle_clipboard(request, obj):
|
def handle_clipboard(request, obj):
|
||||||
"""
|
"""Handle the clipboard in the view.
|
||||||
This method handles the clipboard in the view.
|
|
||||||
This method can fail, since it does not catch the exceptions coming from
|
This method can fail, since it does not catch the exceptions coming from
|
||||||
below, allowing proper handling in the calling view.
|
below, allowing proper handling in the calling view.
|
||||||
Use this method like this:
|
Use this method like this:
|
||||||
|
@ -104,7 +104,7 @@ class MarkdownInput(Textarea):
|
|||||||
"fullscreen": _("Toggle fullscreen"),
|
"fullscreen": _("Toggle fullscreen"),
|
||||||
"guide": _("Markdown guide"),
|
"guide": _("Markdown guide"),
|
||||||
}
|
}
|
||||||
context["markdown_api_url"] = reverse("api:api_markdown")
|
context["markdown_api_url"] = reverse("api:markdown")
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
@ -196,10 +196,9 @@ class RegisteringForm(UserCreationForm):
|
|||||||
|
|
||||||
|
|
||||||
class UserProfileForm(forms.ModelForm):
|
class UserProfileForm(forms.ModelForm):
|
||||||
"""
|
"""Form handling the user profile, managing the files
|
||||||
Form handling the user profile, managing the files
|
|
||||||
This form is actually pretty bad and was made in the rush before the migration. It should be refactored.
|
This form is actually pretty bad and was made in the rush before the migration. It should be refactored.
|
||||||
TODO: refactor this form
|
TODO: refactor this form.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -13,9 +13,7 @@
|
|||||||
#
|
#
|
||||||
#
|
#
|
||||||
|
|
||||||
"""
|
"""Views to manage Groups."""
|
||||||
This module contains views to manage Groups
|
|
||||||
"""
|
|
||||||
|
|
||||||
from ajax_select.fields import AutoCompleteSelectMultipleField
|
from ajax_select.fields import AutoCompleteSelectMultipleField
|
||||||
from django import forms
|
from django import forms
|
||||||
@ -31,9 +29,7 @@ from core.views import CanCreateMixin, CanEditMixin, DetailFormView
|
|||||||
|
|
||||||
|
|
||||||
class EditMembersForm(forms.Form):
|
class EditMembersForm(forms.Form):
|
||||||
"""
|
"""Add and remove members from a Group."""
|
||||||
Add and remove members from a Group
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.current_users = kwargs.pop("users", [])
|
self.current_users = kwargs.pop("users", [])
|
||||||
@ -53,9 +49,7 @@ class EditMembersForm(forms.Form):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def clean_users_added(self):
|
def clean_users_added(self):
|
||||||
"""
|
"""Check that the user is not trying to add an user already in the group."""
|
||||||
Check that the user is not trying to add an user already in the group
|
|
||||||
"""
|
|
||||||
cleaned_data = super().clean()
|
cleaned_data = super().clean()
|
||||||
users_added = cleaned_data.get("users_added", None)
|
users_added = cleaned_data.get("users_added", None)
|
||||||
if not users_added:
|
if not users_added:
|
||||||
@ -77,9 +71,7 @@ class EditMembersForm(forms.Form):
|
|||||||
|
|
||||||
|
|
||||||
class GroupListView(CanEditMixin, ListView):
|
class GroupListView(CanEditMixin, ListView):
|
||||||
"""
|
"""Displays the Group list."""
|
||||||
Displays the Group list
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = RealGroup
|
model = RealGroup
|
||||||
ordering = ["name"]
|
ordering = ["name"]
|
||||||
@ -87,9 +79,7 @@ class GroupListView(CanEditMixin, ListView):
|
|||||||
|
|
||||||
|
|
||||||
class GroupEditView(CanEditMixin, UpdateView):
|
class GroupEditView(CanEditMixin, UpdateView):
|
||||||
"""
|
"""Edit infos of a Group."""
|
||||||
Edit infos of a Group
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = RealGroup
|
model = RealGroup
|
||||||
pk_url_kwarg = "group_id"
|
pk_url_kwarg = "group_id"
|
||||||
@ -98,9 +88,7 @@ class GroupEditView(CanEditMixin, UpdateView):
|
|||||||
|
|
||||||
|
|
||||||
class GroupCreateView(CanCreateMixin, CreateView):
|
class GroupCreateView(CanCreateMixin, CreateView):
|
||||||
"""
|
"""Add a new Group."""
|
||||||
Add a new Group
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = RealGroup
|
model = RealGroup
|
||||||
template_name = "core/create.jinja"
|
template_name = "core/create.jinja"
|
||||||
@ -108,9 +96,8 @@ class GroupCreateView(CanCreateMixin, CreateView):
|
|||||||
|
|
||||||
|
|
||||||
class GroupTemplateView(CanEditMixin, DetailFormView):
|
class GroupTemplateView(CanEditMixin, DetailFormView):
|
||||||
"""
|
"""Display all users in a given Group
|
||||||
Display all users in a given Group
|
Allow adding and removing users from it.
|
||||||
Allow adding and removing users from it
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
model = RealGroup
|
model = RealGroup
|
||||||
@ -143,9 +130,7 @@ class GroupTemplateView(CanEditMixin, DetailFormView):
|
|||||||
|
|
||||||
|
|
||||||
class GroupDeleteView(CanEditMixin, DeleteView):
|
class GroupDeleteView(CanEditMixin, DeleteView):
|
||||||
"""
|
"""Delete a Group."""
|
||||||
Delete a Group
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = RealGroup
|
model = RealGroup
|
||||||
pk_url_kwarg = "group_id"
|
pk_url_kwarg = "group_id"
|
||||||
|
@ -29,6 +29,7 @@ from smtplib import SMTPException
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth import login, views
|
from django.contrib.auth import login, views
|
||||||
from django.contrib.auth.forms import PasswordChangeForm
|
from django.contrib.auth.forms import PasswordChangeForm
|
||||||
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
from django.core.exceptions import PermissionDenied, ValidationError
|
from django.core.exceptions import PermissionDenied, ValidationError
|
||||||
from django.forms import CheckboxSelectMultiple
|
from django.forms import CheckboxSelectMultiple
|
||||||
from django.forms.models import modelform_factory
|
from django.forms.models import modelform_factory
|
||||||
@ -50,7 +51,6 @@ from django.views.generic.dates import MonthMixin, YearMixin
|
|||||||
from django.views.generic.edit import FormView, UpdateView
|
from django.views.generic.edit import FormView, UpdateView
|
||||||
from honeypot.decorators import check_honeypot
|
from honeypot.decorators import check_honeypot
|
||||||
|
|
||||||
from api.views.sas import all_pictures_of_user
|
|
||||||
from core.models import Gift, Preferences, SithFile, User
|
from core.models import Gift, Preferences, SithFile, User
|
||||||
from core.views import (
|
from core.views import (
|
||||||
CanEditMixin,
|
CanEditMixin,
|
||||||
@ -58,7 +58,6 @@ from core.views import (
|
|||||||
CanViewMixin,
|
CanViewMixin,
|
||||||
QuickNotifMixin,
|
QuickNotifMixin,
|
||||||
TabedViewMixin,
|
TabedViewMixin,
|
||||||
UserIsLoggedMixin,
|
|
||||||
)
|
)
|
||||||
from core.views.forms import (
|
from core.views.forms import (
|
||||||
GiftForm,
|
GiftForm,
|
||||||
@ -68,15 +67,14 @@ from core.views.forms import (
|
|||||||
UserProfileForm,
|
UserProfileForm,
|
||||||
)
|
)
|
||||||
from counter.forms import StudentCardForm
|
from counter.forms import StudentCardForm
|
||||||
|
from sas.models import Picture
|
||||||
from subscription.models import Subscription
|
from subscription.models import Subscription
|
||||||
from trombi.views import UserTrombiForm
|
from trombi.views import UserTrombiForm
|
||||||
|
|
||||||
|
|
||||||
@method_decorator(check_honeypot, name="post")
|
@method_decorator(check_honeypot, name="post")
|
||||||
class SithLoginView(views.LoginView):
|
class SithLoginView(views.LoginView):
|
||||||
"""
|
"""The login View."""
|
||||||
The login View
|
|
||||||
"""
|
|
||||||
|
|
||||||
template_name = "core/login.jinja"
|
template_name = "core/login.jinja"
|
||||||
authentication_form = LoginForm
|
authentication_form = LoginForm
|
||||||
@ -85,33 +83,25 @@ class SithLoginView(views.LoginView):
|
|||||||
|
|
||||||
|
|
||||||
class SithPasswordChangeView(views.PasswordChangeView):
|
class SithPasswordChangeView(views.PasswordChangeView):
|
||||||
"""
|
"""Allows a user to change its password."""
|
||||||
Allows a user to change its password
|
|
||||||
"""
|
|
||||||
|
|
||||||
template_name = "core/password_change.jinja"
|
template_name = "core/password_change.jinja"
|
||||||
success_url = reverse_lazy("core:password_change_done")
|
success_url = reverse_lazy("core:password_change_done")
|
||||||
|
|
||||||
|
|
||||||
class SithPasswordChangeDoneView(views.PasswordChangeDoneView):
|
class SithPasswordChangeDoneView(views.PasswordChangeDoneView):
|
||||||
"""
|
"""Allows a user to change its password."""
|
||||||
Allows a user to change its password
|
|
||||||
"""
|
|
||||||
|
|
||||||
template_name = "core/password_change_done.jinja"
|
template_name = "core/password_change_done.jinja"
|
||||||
|
|
||||||
|
|
||||||
def logout(request):
|
def logout(request):
|
||||||
"""
|
"""The logout view."""
|
||||||
The logout view
|
|
||||||
"""
|
|
||||||
return views.logout_then_login(request)
|
return views.logout_then_login(request)
|
||||||
|
|
||||||
|
|
||||||
def password_root_change(request, user_id):
|
def password_root_change(request, user_id):
|
||||||
"""
|
"""Allows a root user to change someone's password."""
|
||||||
Allows a root user to change someone's password
|
|
||||||
"""
|
|
||||||
if not request.user.is_root:
|
if not request.user.is_root:
|
||||||
raise PermissionDenied
|
raise PermissionDenied
|
||||||
user = User.objects.filter(id=user_id).first()
|
user = User.objects.filter(id=user_id).first()
|
||||||
@ -131,9 +121,7 @@ def password_root_change(request, user_id):
|
|||||||
|
|
||||||
@method_decorator(check_honeypot, name="post")
|
@method_decorator(check_honeypot, name="post")
|
||||||
class SithPasswordResetView(views.PasswordResetView):
|
class SithPasswordResetView(views.PasswordResetView):
|
||||||
"""
|
"""Allows someone to enter an email address for resetting password."""
|
||||||
Allows someone to enter an email address for resetting password
|
|
||||||
"""
|
|
||||||
|
|
||||||
template_name = "core/password_reset.jinja"
|
template_name = "core/password_reset.jinja"
|
||||||
email_template_name = "core/password_reset_email.jinja"
|
email_template_name = "core/password_reset_email.jinja"
|
||||||
@ -141,26 +129,20 @@ class SithPasswordResetView(views.PasswordResetView):
|
|||||||
|
|
||||||
|
|
||||||
class SithPasswordResetDoneView(views.PasswordResetDoneView):
|
class SithPasswordResetDoneView(views.PasswordResetDoneView):
|
||||||
"""
|
"""Confirm that the reset email has been sent."""
|
||||||
Confirm that the reset email has been sent
|
|
||||||
"""
|
|
||||||
|
|
||||||
template_name = "core/password_reset_done.jinja"
|
template_name = "core/password_reset_done.jinja"
|
||||||
|
|
||||||
|
|
||||||
class SithPasswordResetConfirmView(views.PasswordResetConfirmView):
|
class SithPasswordResetConfirmView(views.PasswordResetConfirmView):
|
||||||
"""
|
"""Provide a reset password form."""
|
||||||
Provide a reset password form
|
|
||||||
"""
|
|
||||||
|
|
||||||
template_name = "core/password_reset_confirm.jinja"
|
template_name = "core/password_reset_confirm.jinja"
|
||||||
success_url = reverse_lazy("core:password_reset_complete")
|
success_url = reverse_lazy("core:password_reset_complete")
|
||||||
|
|
||||||
|
|
||||||
class SithPasswordResetCompleteView(views.PasswordResetCompleteView):
|
class SithPasswordResetCompleteView(views.PasswordResetCompleteView):
|
||||||
"""
|
"""Confirm the password has successfully been reset."""
|
||||||
Confirm the password has successfully been reset
|
|
||||||
"""
|
|
||||||
|
|
||||||
template_name = "core/password_reset_complete.jinja"
|
template_name = "core/password_reset_complete.jinja"
|
||||||
|
|
||||||
@ -302,9 +284,7 @@ class UserTabsMixin(TabedViewMixin):
|
|||||||
|
|
||||||
|
|
||||||
class UserView(UserTabsMixin, CanViewMixin, DetailView):
|
class UserView(UserTabsMixin, CanViewMixin, DetailView):
|
||||||
"""
|
"""Display a user's profile."""
|
||||||
Display a user's profile
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = User
|
model = User
|
||||||
pk_url_kwarg = "user_id"
|
pk_url_kwarg = "user_id"
|
||||||
@ -321,9 +301,7 @@ class UserView(UserTabsMixin, CanViewMixin, DetailView):
|
|||||||
|
|
||||||
|
|
||||||
class UserPicturesView(UserTabsMixin, CanViewMixin, DetailView):
|
class UserPicturesView(UserTabsMixin, CanViewMixin, DetailView):
|
||||||
"""
|
"""Display a user's pictures."""
|
||||||
Display a user's pictures
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = User
|
model = User
|
||||||
pk_url_kwarg = "user_id"
|
pk_url_kwarg = "user_id"
|
||||||
@ -335,7 +313,11 @@ class UserPicturesView(UserTabsMixin, CanViewMixin, DetailView):
|
|||||||
kwargs = super().get_context_data(**kwargs)
|
kwargs = super().get_context_data(**kwargs)
|
||||||
kwargs["albums"] = []
|
kwargs["albums"] = []
|
||||||
kwargs["pictures"] = {}
|
kwargs["pictures"] = {}
|
||||||
picture_qs = all_pictures_of_user(self.object)
|
picture_qs = (
|
||||||
|
Picture.objects.filter(people__user_id=self.object.id)
|
||||||
|
.order_by("parent__date", "id")
|
||||||
|
.all()
|
||||||
|
)
|
||||||
last_album = None
|
last_album = None
|
||||||
for picture in picture_qs:
|
for picture in picture_qs:
|
||||||
album = picture.parent
|
album = picture.parent
|
||||||
@ -361,9 +343,7 @@ def delete_user_godfather(request, user_id, godfather_id, is_father):
|
|||||||
|
|
||||||
|
|
||||||
class UserGodfathersView(UserTabsMixin, CanViewMixin, DetailView):
|
class UserGodfathersView(UserTabsMixin, CanViewMixin, DetailView):
|
||||||
"""
|
"""Display a user's godfathers."""
|
||||||
Display a user's godfathers
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = User
|
model = User
|
||||||
pk_url_kwarg = "user_id"
|
pk_url_kwarg = "user_id"
|
||||||
@ -394,9 +374,7 @@ class UserGodfathersView(UserTabsMixin, CanViewMixin, DetailView):
|
|||||||
|
|
||||||
|
|
||||||
class UserGodfathersTreeView(UserTabsMixin, CanViewMixin, DetailView):
|
class UserGodfathersTreeView(UserTabsMixin, CanViewMixin, DetailView):
|
||||||
"""
|
"""Display a user's family tree."""
|
||||||
Display a user's family tree
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = User
|
model = User
|
||||||
pk_url_kwarg = "user_id"
|
pk_url_kwarg = "user_id"
|
||||||
@ -415,9 +393,7 @@ class UserGodfathersTreeView(UserTabsMixin, CanViewMixin, DetailView):
|
|||||||
|
|
||||||
|
|
||||||
class UserGodfathersTreePictureView(CanViewMixin, DetailView):
|
class UserGodfathersTreePictureView(CanViewMixin, DetailView):
|
||||||
"""
|
"""Display a user's tree as a picture."""
|
||||||
Display a user's tree as a picture
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = User
|
model = User
|
||||||
pk_url_kwarg = "user_id"
|
pk_url_kwarg = "user_id"
|
||||||
@ -489,9 +465,7 @@ class UserGodfathersTreePictureView(CanViewMixin, DetailView):
|
|||||||
|
|
||||||
|
|
||||||
class UserStatsView(UserTabsMixin, CanViewMixin, DetailView):
|
class UserStatsView(UserTabsMixin, CanViewMixin, DetailView):
|
||||||
"""
|
"""Display a user's stats."""
|
||||||
Display a user's stats
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = User
|
model = User
|
||||||
pk_url_kwarg = "user_id"
|
pk_url_kwarg = "user_id"
|
||||||
@ -591,9 +565,7 @@ class UserStatsView(UserTabsMixin, CanViewMixin, DetailView):
|
|||||||
|
|
||||||
|
|
||||||
class UserMiniView(CanViewMixin, DetailView):
|
class UserMiniView(CanViewMixin, DetailView):
|
||||||
"""
|
"""Display a user's profile."""
|
||||||
Display a user's profile
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = User
|
model = User
|
||||||
pk_url_kwarg = "user_id"
|
pk_url_kwarg = "user_id"
|
||||||
@ -602,18 +574,14 @@ class UserMiniView(CanViewMixin, DetailView):
|
|||||||
|
|
||||||
|
|
||||||
class UserListView(ListView, CanEditPropMixin):
|
class UserListView(ListView, CanEditPropMixin):
|
||||||
"""
|
"""Displays the user list."""
|
||||||
Displays the user list
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = User
|
model = User
|
||||||
template_name = "core/user_list.jinja"
|
template_name = "core/user_list.jinja"
|
||||||
|
|
||||||
|
|
||||||
class UserUploadProfilePictView(CanEditMixin, DetailView):
|
class UserUploadProfilePictView(CanEditMixin, DetailView):
|
||||||
"""
|
"""Handle the upload of the profile picture taken with webcam in navigator."""
|
||||||
Handle the upload of the profile picture taken with webcam in navigator
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = User
|
model = User
|
||||||
pk_url_kwarg = "user_id"
|
pk_url_kwarg = "user_id"
|
||||||
@ -650,9 +618,7 @@ class UserUploadProfilePictView(CanEditMixin, DetailView):
|
|||||||
|
|
||||||
|
|
||||||
class UserUpdateProfileView(UserTabsMixin, CanEditMixin, UpdateView):
|
class UserUpdateProfileView(UserTabsMixin, CanEditMixin, UpdateView):
|
||||||
"""
|
"""Edit a user's profile."""
|
||||||
Edit a user's profile
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = User
|
model = User
|
||||||
pk_url_kwarg = "user_id"
|
pk_url_kwarg = "user_id"
|
||||||
@ -663,9 +629,7 @@ class UserUpdateProfileView(UserTabsMixin, CanEditMixin, UpdateView):
|
|||||||
board_only = []
|
board_only = []
|
||||||
|
|
||||||
def remove_restricted_fields(self, request):
|
def remove_restricted_fields(self, request):
|
||||||
"""
|
"""Removes edit_once and board_only fields."""
|
||||||
Removes edit_once and board_only fields
|
|
||||||
"""
|
|
||||||
for i in self.edit_once:
|
for i in self.edit_once:
|
||||||
if getattr(self.form.instance, i) and not (
|
if getattr(self.form.instance, i) and not (
|
||||||
request.user.is_board_member or request.user.is_root
|
request.user.is_board_member or request.user.is_root
|
||||||
@ -703,9 +667,7 @@ class UserUpdateProfileView(UserTabsMixin, CanEditMixin, UpdateView):
|
|||||||
|
|
||||||
|
|
||||||
class UserClubView(UserTabsMixin, CanViewMixin, DetailView):
|
class UserClubView(UserTabsMixin, CanViewMixin, DetailView):
|
||||||
"""
|
"""Display the user's club(s)."""
|
||||||
Display the user's club(s)
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = User
|
model = User
|
||||||
context_object_name = "profile"
|
context_object_name = "profile"
|
||||||
@ -715,9 +677,7 @@ class UserClubView(UserTabsMixin, CanViewMixin, DetailView):
|
|||||||
|
|
||||||
|
|
||||||
class UserPreferencesView(UserTabsMixin, CanEditMixin, UpdateView):
|
class UserPreferencesView(UserTabsMixin, CanEditMixin, UpdateView):
|
||||||
"""
|
"""Edit a user's preferences."""
|
||||||
Edit a user's preferences
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = User
|
model = User
|
||||||
pk_url_kwarg = "user_id"
|
pk_url_kwarg = "user_id"
|
||||||
@ -752,9 +712,7 @@ class UserPreferencesView(UserTabsMixin, CanEditMixin, UpdateView):
|
|||||||
|
|
||||||
|
|
||||||
class UserUpdateGroupView(UserTabsMixin, CanEditPropMixin, UpdateView):
|
class UserUpdateGroupView(UserTabsMixin, CanEditPropMixin, UpdateView):
|
||||||
"""
|
"""Edit a user's groups."""
|
||||||
Edit a user's groups
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = User
|
model = User
|
||||||
pk_url_kwarg = "user_id"
|
pk_url_kwarg = "user_id"
|
||||||
@ -766,10 +724,8 @@ class UserUpdateGroupView(UserTabsMixin, CanEditPropMixin, UpdateView):
|
|||||||
current_tab = "groups"
|
current_tab = "groups"
|
||||||
|
|
||||||
|
|
||||||
class UserToolsView(QuickNotifMixin, UserTabsMixin, UserIsLoggedMixin, TemplateView):
|
class UserToolsView(LoginRequiredMixin, QuickNotifMixin, UserTabsMixin, TemplateView):
|
||||||
"""
|
"""Displays the logged user's tools."""
|
||||||
Displays the logged user's tools
|
|
||||||
"""
|
|
||||||
|
|
||||||
template_name = "core/user_tools.jinja"
|
template_name = "core/user_tools.jinja"
|
||||||
current_tab = "tools"
|
current_tab = "tools"
|
||||||
@ -786,9 +742,7 @@ class UserToolsView(QuickNotifMixin, UserTabsMixin, UserIsLoggedMixin, TemplateV
|
|||||||
|
|
||||||
|
|
||||||
class UserAccountBase(UserTabsMixin, DetailView):
|
class UserAccountBase(UserTabsMixin, DetailView):
|
||||||
"""
|
"""Base class for UserAccount."""
|
||||||
Base class for UserAccount
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = User
|
model = User
|
||||||
pk_url_kwarg = "user_id"
|
pk_url_kwarg = "user_id"
|
||||||
@ -809,9 +763,7 @@ class UserAccountBase(UserTabsMixin, DetailView):
|
|||||||
|
|
||||||
|
|
||||||
class UserAccountView(UserAccountBase):
|
class UserAccountView(UserAccountBase):
|
||||||
"""
|
"""Display a user's account."""
|
||||||
Display a user's account
|
|
||||||
"""
|
|
||||||
|
|
||||||
template_name = "core/user_account.jinja"
|
template_name = "core/user_account.jinja"
|
||||||
|
|
||||||
@ -858,9 +810,7 @@ class UserAccountView(UserAccountBase):
|
|||||||
|
|
||||||
|
|
||||||
class UserAccountDetailView(UserAccountBase, YearMixin, MonthMixin):
|
class UserAccountDetailView(UserAccountBase, YearMixin, MonthMixin):
|
||||||
"""
|
"""Display a user's account for month."""
|
||||||
Display a user's account for month
|
|
||||||
"""
|
|
||||||
|
|
||||||
template_name = "core/user_account_detail.jinja"
|
template_name = "core/user_account_detail.jinja"
|
||||||
|
|
||||||
|
37
counter/api.py
Normal file
37
counter/api.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
#
|
||||||
|
# Copyright 2024 AE UTBM
|
||||||
|
# ae@utbm.fr / ae.info@utbm.fr
|
||||||
|
#
|
||||||
|
# This file is part of the website of the UTBM Student Association (AE UTBM),
|
||||||
|
# https://ae.utbm.fr.
|
||||||
|
#
|
||||||
|
# You can find the source code of the website at https://github.com/ae-utbm/sith3
|
||||||
|
#
|
||||||
|
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
|
||||||
|
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
|
||||||
|
# OR WITHIN THE LOCAL FILE "LICENSE"
|
||||||
|
#
|
||||||
|
#
|
||||||
|
from ninja_extra import ControllerBase, api_controller, route
|
||||||
|
|
||||||
|
from core.api_permissions import CanView, IsRoot
|
||||||
|
from counter.models import Counter
|
||||||
|
from counter.schemas import CounterSchema
|
||||||
|
|
||||||
|
|
||||||
|
@api_controller("/counter")
|
||||||
|
class CounterController(ControllerBase):
|
||||||
|
@route.get("", response=list[CounterSchema], permissions=[IsRoot])
|
||||||
|
def fetch_all(self):
|
||||||
|
return Counter.objects.all()
|
||||||
|
|
||||||
|
@route.get("{counter_id}/", response=CounterSchema, permissions=[CanView])
|
||||||
|
def fetch_one(self, counter_id: int):
|
||||||
|
return self.get_object_or_exception(Counter, pk=counter_id)
|
||||||
|
|
||||||
|
@route.get("bar/", response=list[CounterSchema], permissions=[CanView])
|
||||||
|
def fetch_bars(self):
|
||||||
|
counters = list(Counter.objects.filter(type="BAR"))
|
||||||
|
for c in counters:
|
||||||
|
self.check_object_permissions(c)
|
||||||
|
return counters
|
@ -30,9 +30,8 @@ class BillingInfoForm(forms.ModelForm):
|
|||||||
|
|
||||||
|
|
||||||
class StudentCardForm(forms.ModelForm):
|
class StudentCardForm(forms.ModelForm):
|
||||||
"""
|
"""Form for adding student cards
|
||||||
Form for adding student cards
|
Only used for user profile since CounterClick is to complicated.
|
||||||
Only used for user profile since CounterClick is to complicated
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -48,8 +47,7 @@ class StudentCardForm(forms.ModelForm):
|
|||||||
|
|
||||||
|
|
||||||
class GetUserForm(forms.Form):
|
class GetUserForm(forms.Form):
|
||||||
"""
|
"""The Form class aims at providing a valid user_id field in its cleaned data, in order to pass it to some view,
|
||||||
The Form class aims at providing a valid user_id field in its cleaned data, in order to pass it to some view,
|
|
||||||
reverse function, or any other use.
|
reverse function, or any other use.
|
||||||
|
|
||||||
The Form implements a nice JS widget allowing the user to type a customer account id, or search the database with
|
The Form implements a nice JS widget allowing the user to type a customer account id, or search the database with
|
||||||
|
@ -44,9 +44,10 @@ from subscription.models import Subscription
|
|||||||
|
|
||||||
|
|
||||||
class Customer(models.Model):
|
class Customer(models.Model):
|
||||||
"""
|
"""Customer data of a User.
|
||||||
This class extends a user to make a customer. It adds some basic customers' information, such as the account ID, and
|
|
||||||
is used by other accounting classes as reference to the customer, rather than using User
|
It adds some basic customers' information, such as the account ID, and
|
||||||
|
is used by other accounting classes as reference to the customer, rather than using User.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
user = models.OneToOneField(User, primary_key=True, on_delete=models.CASCADE)
|
user = models.OneToOneField(User, primary_key=True, on_delete=models.CASCADE)
|
||||||
@ -63,10 +64,9 @@ class Customer(models.Model):
|
|||||||
return "%s - %s" % (self.user.username, self.account_id)
|
return "%s - %s" % (self.user.username, self.account_id)
|
||||||
|
|
||||||
def save(self, *args, allow_negative=False, is_selling=False, **kwargs):
|
def save(self, *args, allow_negative=False, is_selling=False, **kwargs):
|
||||||
"""
|
"""is_selling : tell if the current action is a selling
|
||||||
is_selling : tell if the current action is a selling
|
|
||||||
allow_negative : ignored if not a selling. Allow a selling to put the account in negative
|
allow_negative : ignored if not a selling. Allow a selling to put the account in negative
|
||||||
Those two parameters avoid blocking the save method of a customer if his account is negative
|
Those two parameters avoid blocking the save method of a customer if his account is negative.
|
||||||
"""
|
"""
|
||||||
if self.amount < 0 and (is_selling and not allow_negative):
|
if self.amount < 0 and (is_selling and not allow_negative):
|
||||||
raise ValidationError(_("Not enough money"))
|
raise ValidationError(_("Not enough money"))
|
||||||
@ -84,9 +84,8 @@ class Customer(models.Model):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def can_buy(self) -> bool:
|
def can_buy(self) -> bool:
|
||||||
"""
|
"""Check if whether this customer has the right to purchase any item.
|
||||||
Check if whether this customer has the right to
|
|
||||||
purchase any item.
|
|
||||||
This must be not confused with the Product.can_be_sold_to(user)
|
This must be not confused with the Product.can_be_sold_to(user)
|
||||||
method as the present method returns an information
|
method as the present method returns an information
|
||||||
about a customer whereas the other tells something
|
about a customer whereas the other tells something
|
||||||
@ -100,8 +99,7 @@ class Customer(models.Model):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_or_create(cls, user: User) -> Tuple[Customer, bool]:
|
def get_or_create(cls, user: User) -> Tuple[Customer, bool]:
|
||||||
"""
|
"""Work in pretty much the same way as the usual get_or_create method,
|
||||||
Work in pretty much the same way as the usual get_or_create method,
|
|
||||||
but with the default field replaced by some under the hood.
|
but with the default field replaced by some under the hood.
|
||||||
|
|
||||||
If the user has an account, return it as is.
|
If the user has an account, return it as is.
|
||||||
@ -158,9 +156,8 @@ class Customer(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
class BillingInfo(models.Model):
|
class BillingInfo(models.Model):
|
||||||
"""
|
"""Represent the billing information of a user, which are required
|
||||||
Represent the billing information of a user, which are required
|
by the 3D-Secure v2 system used by the etransaction module.
|
||||||
by the 3D-Secure v2 system used by the etransaction module
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
customer = models.OneToOneField(
|
customer = models.OneToOneField(
|
||||||
@ -182,10 +179,9 @@ class BillingInfo(models.Model):
|
|||||||
return f"{self.first_name} {self.last_name}"
|
return f"{self.first_name} {self.last_name}"
|
||||||
|
|
||||||
def to_3dsv2_xml(self) -> str:
|
def to_3dsv2_xml(self) -> str:
|
||||||
"""
|
"""Convert the data from this model into a xml usable
|
||||||
Convert the data from this model into a xml usable
|
|
||||||
by the online paying service of the Crédit Agricole bank.
|
by the online paying service of the Crédit Agricole bank.
|
||||||
see : `https://www.ca-moncommerce.com/espace-client-mon-commerce/up2pay-e-transactions/ma-documentation/manuel-dintegration-focus-3ds-v2/principes-generaux/#integration-3dsv2-developpeur-webmaster`
|
see : `https://www.ca-moncommerce.com/espace-client-mon-commerce/up2pay-e-transactions/ma-documentation/manuel-dintegration-focus-3ds-v2/principes-generaux/#integration-3dsv2-developpeur-webmaster`.
|
||||||
"""
|
"""
|
||||||
data = {
|
data = {
|
||||||
"Address": {
|
"Address": {
|
||||||
@ -204,9 +200,9 @@ class BillingInfo(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
class ProductType(models.Model):
|
class ProductType(models.Model):
|
||||||
"""
|
"""A product type.
|
||||||
This describes a product type
|
|
||||||
Useful only for categorizing, changes are made at the product level for now
|
Useful only for categorizing.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = models.CharField(_("name"), max_length=30)
|
name = models.CharField(_("name"), max_length=30)
|
||||||
@ -229,9 +225,7 @@ class ProductType(models.Model):
|
|||||||
return reverse("counter:producttype_list")
|
return reverse("counter:producttype_list")
|
||||||
|
|
||||||
def is_owned_by(self, user):
|
def is_owned_by(self, user):
|
||||||
"""
|
"""Method to see if that object can be edited by the given user."""
|
||||||
Method to see if that object can be edited by the given user
|
|
||||||
"""
|
|
||||||
if user.is_anonymous:
|
if user.is_anonymous:
|
||||||
return False
|
return False
|
||||||
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
|
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
|
||||||
@ -240,9 +234,7 @@ class ProductType(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
class Product(models.Model):
|
class Product(models.Model):
|
||||||
"""
|
"""A product, with all its related information."""
|
||||||
This describes a product, with all its related informations
|
|
||||||
"""
|
|
||||||
|
|
||||||
name = models.CharField(_("name"), max_length=64)
|
name = models.CharField(_("name"), max_length=64)
|
||||||
description = models.TextField(_("description"), blank=True)
|
description = models.TextField(_("description"), blank=True)
|
||||||
@ -297,9 +289,7 @@ class Product(models.Model):
|
|||||||
return settings.SITH_ECOCUP_DECO == self.id
|
return settings.SITH_ECOCUP_DECO == self.id
|
||||||
|
|
||||||
def is_owned_by(self, user):
|
def is_owned_by(self, user):
|
||||||
"""
|
"""Method to see if that object can be edited by the given user."""
|
||||||
Method to see if that object can be edited by the given user
|
|
||||||
"""
|
|
||||||
if user.is_anonymous:
|
if user.is_anonymous:
|
||||||
return False
|
return False
|
||||||
if user.is_in_group(
|
if user.is_in_group(
|
||||||
@ -309,8 +299,7 @@ class Product(models.Model):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def can_be_sold_to(self, user: User) -> bool:
|
def can_be_sold_to(self, user: User) -> bool:
|
||||||
"""
|
"""Check if whether the user given in parameter has the right to buy
|
||||||
Check if whether the user given in parameter has the right to buy
|
|
||||||
this product or not.
|
this product or not.
|
||||||
|
|
||||||
This must be not confused with the Customer.can_buy()
|
This must be not confused with the Customer.can_buy()
|
||||||
@ -319,7 +308,8 @@ class Product(models.Model):
|
|||||||
whereas the other tells something about a Customer
|
whereas the other tells something about a Customer
|
||||||
(and not a user, they are not the same model).
|
(and not a user, they are not the same model).
|
||||||
|
|
||||||
:return: True if the user can buy this product else False
|
Returns:
|
||||||
|
True if the user can buy this product else False
|
||||||
"""
|
"""
|
||||||
if not self.buying_groups.exists():
|
if not self.buying_groups.exists():
|
||||||
return True
|
return True
|
||||||
@ -335,15 +325,16 @@ class Product(models.Model):
|
|||||||
|
|
||||||
class CounterQuerySet(models.QuerySet):
|
class CounterQuerySet(models.QuerySet):
|
||||||
def annotate_has_barman(self, user: User) -> CounterQuerySet:
|
def annotate_has_barman(self, user: User) -> CounterQuerySet:
|
||||||
"""
|
"""Annotate the queryset with the `user_is_barman` field.
|
||||||
Annotate the queryset with the `user_is_barman` field.
|
|
||||||
For each counter, this field has value True if the user
|
For each counter, this field has value True if the user
|
||||||
is a barman of this counter, else False.
|
is a barman of this counter, else False.
|
||||||
|
|
||||||
:param user: the user we want to check if he is a barman
|
Args:
|
||||||
|
user: the user we want to check if he is a barman
|
||||||
Example::
|
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
```python
|
||||||
sli = User.objects.get(username="sli")
|
sli = User.objects.get(username="sli")
|
||||||
counters = (
|
counters = (
|
||||||
Counter.objects
|
Counter.objects
|
||||||
@ -353,6 +344,7 @@ class CounterQuerySet(models.QuerySet):
|
|||||||
print("Sli est barman dans les comptoirs suivants :")
|
print("Sli est barman dans les comptoirs suivants :")
|
||||||
for counter in counters:
|
for counter in counters:
|
||||||
print(f"- {counter.name}")
|
print(f"- {counter.name}")
|
||||||
|
```
|
||||||
"""
|
"""
|
||||||
subquery = user.counters.filter(pk=OuterRef("pk"))
|
subquery = user.counters.filter(pk=OuterRef("pk"))
|
||||||
# noinspection PyTypeChecker
|
# noinspection PyTypeChecker
|
||||||
@ -391,19 +383,19 @@ class Counter(models.Model):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self) -> str:
|
||||||
if self.type == "EBOUTIC":
|
if self.type == "EBOUTIC":
|
||||||
return reverse("eboutic:main")
|
return reverse("eboutic:main")
|
||||||
return reverse("counter:details", kwargs={"counter_id": self.id})
|
return reverse("counter:details", kwargs={"counter_id": self.id})
|
||||||
|
|
||||||
def __getattribute__(self, name):
|
def __getattribute__(self, name: str):
|
||||||
if name == "edit_groups":
|
if name == "edit_groups":
|
||||||
return Group.objects.filter(
|
return Group.objects.filter(
|
||||||
name=self.club.unix_name + settings.SITH_BOARD_SUFFIX
|
name=self.club.unix_name + settings.SITH_BOARD_SUFFIX
|
||||||
).all()
|
).all()
|
||||||
return object.__getattribute__(self, name)
|
return object.__getattribute__(self, name)
|
||||||
|
|
||||||
def is_owned_by(self, user):
|
def is_owned_by(self, user: User) -> bool:
|
||||||
if user.is_anonymous:
|
if user.is_anonymous:
|
||||||
return False
|
return False
|
||||||
mem = self.club.get_membership_for(user)
|
mem = self.club.get_membership_for(user)
|
||||||
@ -411,108 +403,71 @@ class Counter(models.Model):
|
|||||||
return True
|
return True
|
||||||
return user.is_in_group(pk=settings.SITH_GROUP_COUNTER_ADMIN_ID)
|
return user.is_in_group(pk=settings.SITH_GROUP_COUNTER_ADMIN_ID)
|
||||||
|
|
||||||
def can_be_viewed_by(self, user):
|
def can_be_viewed_by(self, user: User) -> bool:
|
||||||
if self.type == "BAR":
|
if self.type == "BAR":
|
||||||
return True
|
return True
|
||||||
return user.is_board_member or user in self.sellers.all()
|
return user.is_board_member or user in self.sellers.all()
|
||||||
|
|
||||||
def gen_token(self):
|
def gen_token(self) -> None:
|
||||||
"""Generate a new token for this counter"""
|
"""Generate a new token for this counter."""
|
||||||
self.token = "".join(
|
self.token = "".join(
|
||||||
random.choice(string.ascii_letters + string.digits) for x in range(30)
|
random.choice(string.ascii_letters + string.digits) for _ in range(30)
|
||||||
)
|
)
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
def add_barman(self, user):
|
|
||||||
"""
|
|
||||||
Logs a barman in to the given counter
|
|
||||||
A user is stored as a tuple with its login time
|
|
||||||
"""
|
|
||||||
Permanency(user=user, counter=self, start=timezone.now(), end=None).save()
|
|
||||||
|
|
||||||
def del_barman(self, user):
|
|
||||||
"""
|
|
||||||
Logs a barman out and store its permanency
|
|
||||||
"""
|
|
||||||
perm = Permanency.objects.filter(counter=self, user=user, end=None).all()
|
|
||||||
for p in perm:
|
|
||||||
p.end = p.activity
|
|
||||||
p.save()
|
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def barmen_list(self):
|
def barmen_list(self) -> list[User]:
|
||||||
return self.get_barmen_list()
|
return self.get_barmen_list()
|
||||||
|
|
||||||
def get_barmen_list(self):
|
def get_barmen_list(self) -> list[User]:
|
||||||
"""
|
"""Returns the barman list as list of User.
|
||||||
Returns the barman list as list of User
|
|
||||||
|
|
||||||
Also handle the timeout of the barmen
|
Also handle the timeout of the barmen
|
||||||
"""
|
"""
|
||||||
pl = Permanency.objects.filter(counter=self, end=None).all()
|
perms = self.permanencies.filter(end=None)
|
||||||
bl = []
|
|
||||||
for p in pl:
|
|
||||||
if timezone.now() - p.activity < timedelta(
|
|
||||||
minutes=settings.SITH_BARMAN_TIMEOUT
|
|
||||||
):
|
|
||||||
bl.append(p.user)
|
|
||||||
else:
|
|
||||||
p.end = p.activity
|
|
||||||
p.save()
|
|
||||||
return bl
|
|
||||||
|
|
||||||
def get_random_barman(self):
|
# disconnect barmen who are inactive
|
||||||
"""
|
timeout = timezone.now() - timedelta(minutes=settings.SITH_BARMAN_TIMEOUT)
|
||||||
Return a random user being currently a barman
|
perms.filter(activity__lte=timeout).update(end=F("activity"))
|
||||||
"""
|
|
||||||
bl = self.get_barmen_list()
|
|
||||||
return bl[random.randrange(0, len(bl))]
|
|
||||||
|
|
||||||
def update_activity(self):
|
return [p.user for p in perms.select_related("user")]
|
||||||
"""
|
|
||||||
Update the barman activity to prevent timeout
|
|
||||||
"""
|
|
||||||
for p in Permanency.objects.filter(counter=self, end=None).all():
|
|
||||||
p.save() # Update activity
|
|
||||||
|
|
||||||
def is_open(self):
|
def get_random_barman(self) -> User:
|
||||||
|
"""Return a random user being currently a barman."""
|
||||||
|
return random.choice(self.barmen_list)
|
||||||
|
|
||||||
|
def update_activity(self) -> None:
|
||||||
|
"""Update the barman activity to prevent timeout."""
|
||||||
|
self.permanencies.filter(end=None).update(activity=timezone.now())
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_open(self) -> bool:
|
||||||
return len(self.barmen_list) > 0
|
return len(self.barmen_list) > 0
|
||||||
|
|
||||||
def is_inactive(self):
|
def is_inactive(self) -> bool:
|
||||||
"""
|
"""Returns True if the counter self is inactive from SITH_COUNTER_MINUTE_INACTIVE's value minutes, else False."""
|
||||||
Returns True if the counter self is inactive from SITH_COUNTER_MINUTE_INACTIVE's value minutes, else False
|
return self.is_open and (
|
||||||
"""
|
|
||||||
return self.is_open() and (
|
|
||||||
(timezone.now() - self.permanencies.order_by("-activity").first().activity)
|
(timezone.now() - self.permanencies.order_by("-activity").first().activity)
|
||||||
> timedelta(minutes=settings.SITH_COUNTER_MINUTE_INACTIVE)
|
> timedelta(minutes=settings.SITH_COUNTER_MINUTE_INACTIVE)
|
||||||
)
|
)
|
||||||
|
|
||||||
def barman_list(self):
|
def barman_list(self) -> list[int]:
|
||||||
"""
|
"""Returns the barman id list."""
|
||||||
Returns the barman id list
|
return [b.id for b in self.barmen_list]
|
||||||
"""
|
|
||||||
return [b.id for b in self.get_barmen_list()]
|
|
||||||
|
|
||||||
def can_refill(self):
|
|
||||||
"""
|
|
||||||
Show if the counter authorize the refilling with physic money
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
def can_refill(self) -> bool:
|
||||||
|
"""Show if the counter authorize the refilling with physic money."""
|
||||||
if self.type != "BAR":
|
if self.type != "BAR":
|
||||||
return False
|
return False
|
||||||
if self.id in SITH_COUNTER_OFFICES:
|
if self.id in SITH_COUNTER_OFFICES:
|
||||||
# If the counter is either 'AE' or 'BdF', refills are authorized
|
# If the counter is either 'AE' or 'BdF', refills are authorized
|
||||||
return True
|
return True
|
||||||
is_ae_member = False
|
# at least one of the barmen is in the AE board
|
||||||
ae = Club.objects.get(unix_name=SITH_MAIN_CLUB["unix_name"])
|
ae = Club.objects.get(unix_name=SITH_MAIN_CLUB["unix_name"])
|
||||||
for barman in self.get_barmen_list():
|
return any(ae.get_membership_for(barman) for barman in self.barmen_list)
|
||||||
if ae.get_membership_for(barman):
|
|
||||||
is_ae_member = True
|
|
||||||
return is_ae_member
|
|
||||||
|
|
||||||
def get_top_barmen(self) -> QuerySet:
|
def get_top_barmen(self) -> QuerySet:
|
||||||
"""
|
"""Return a QuerySet querying the office hours stats of all the barmen of all time
|
||||||
Return a QuerySet querying the office hours stats of all the barmen of all time
|
|
||||||
of this counter, ordered by descending number of hours.
|
of this counter, ordered by descending number of hours.
|
||||||
|
|
||||||
Each element of the QuerySet corresponds to a barman and has the following data :
|
Each element of the QuerySet corresponds to a barman and has the following data :
|
||||||
@ -535,16 +490,17 @@ class Counter(models.Model):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def get_top_customers(self, since: datetime | date | None = None) -> QuerySet:
|
def get_top_customers(self, since: datetime | date | None = None) -> QuerySet:
|
||||||
"""
|
"""Return a QuerySet querying the money spent by customers of this counter
|
||||||
Return a QuerySet querying the money spent by customers of this counter
|
|
||||||
since the specified date, ordered by descending amount of money spent.
|
since the specified date, ordered by descending amount of money spent.
|
||||||
|
|
||||||
Each element of the QuerySet corresponds to a customer and has the following data :
|
Each element of the QuerySet corresponds to a customer and has the following data :
|
||||||
|
|
||||||
- the full name (first name + last name) of the customer
|
- the full name (first name + last name) of the customer
|
||||||
- the nickname of the customer
|
- the nickname of the customer
|
||||||
- the amount of money spent by the customer
|
- the amount of money spent by the customer
|
||||||
|
|
||||||
:param since: timestamp from which to perform the calculation
|
Args:
|
||||||
|
since: timestamp from which to perform the calculation
|
||||||
"""
|
"""
|
||||||
if since is None:
|
if since is None:
|
||||||
since = get_start_of_semester()
|
since = get_start_of_semester()
|
||||||
@ -573,27 +529,31 @@ class Counter(models.Model):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def get_total_sales(self, since: datetime | date | None = None) -> CurrencyField:
|
def get_total_sales(self, since: datetime | date | None = None) -> CurrencyField:
|
||||||
"""
|
"""Compute and return the total turnover of this counter since the given date.
|
||||||
Compute and return the total turnover of this counter
|
|
||||||
since the date specified in parameter (by default, since the start of the current
|
By default, the date is the start of the current semester.
|
||||||
semester)
|
|
||||||
:param since: timestamp from which to perform the calculation
|
Args:
|
||||||
:return: Total revenue earned at this counter
|
since: timestamp from which to perform the calculation
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Total revenue earned at this counter.
|
||||||
"""
|
"""
|
||||||
if since is None:
|
if since is None:
|
||||||
since = get_start_of_semester()
|
since = get_start_of_semester()
|
||||||
if isinstance(since, date):
|
if isinstance(since, date):
|
||||||
since = datetime(since.year, since.month, since.day, tzinfo=tz.utc)
|
since = datetime(since.year, since.month, since.day, tzinfo=tz.utc)
|
||||||
total = self.sellings.filter(date__gte=since).aggregate(
|
return self.sellings.filter(date__gte=since).aggregate(
|
||||||
total=Sum(F("quantity") * F("unit_price"), output_field=CurrencyField())
|
total=Sum(
|
||||||
|
F("quantity") * F("unit_price"),
|
||||||
|
default=0,
|
||||||
|
output_field=CurrencyField(),
|
||||||
|
)
|
||||||
)["total"]
|
)["total"]
|
||||||
return total if total is not None else CurrencyField(0)
|
|
||||||
|
|
||||||
|
|
||||||
class Refilling(models.Model):
|
class Refilling(models.Model):
|
||||||
"""
|
"""Handle the refilling."""
|
||||||
Handle the refilling
|
|
||||||
"""
|
|
||||||
|
|
||||||
counter = models.ForeignKey(
|
counter = models.ForeignKey(
|
||||||
Counter, related_name="refillings", blank=False, on_delete=models.CASCADE
|
Counter, related_name="refillings", blank=False, on_delete=models.CASCADE
|
||||||
@ -665,9 +625,7 @@ class Refilling(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
class Selling(models.Model):
|
class Selling(models.Model):
|
||||||
"""
|
"""Handle the sellings."""
|
||||||
Handle the sellings
|
|
||||||
"""
|
|
||||||
|
|
||||||
label = models.CharField(_("label"), max_length=64)
|
label = models.CharField(_("label"), max_length=64)
|
||||||
product = models.ForeignKey(
|
product = models.ForeignKey(
|
||||||
@ -724,9 +682,7 @@ class Selling(models.Model):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def save(self, *args, allow_negative=False, **kwargs):
|
def save(self, *args, allow_negative=False, **kwargs):
|
||||||
"""
|
"""allow_negative : Allow this selling to use more money than available for this user."""
|
||||||
allow_negative : Allow this selling to use more money than available for this user
|
|
||||||
"""
|
|
||||||
if not self.date:
|
if not self.date:
|
||||||
self.date = timezone.now()
|
self.date = timezone.now()
|
||||||
self.full_clean()
|
self.full_clean()
|
||||||
@ -864,8 +820,10 @@ class Selling(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
class Permanency(models.Model):
|
class Permanency(models.Model):
|
||||||
"""
|
"""A permanency of a barman, on a counter.
|
||||||
This class aims at storing a traceability of who was barman where and when
|
|
||||||
|
This aims at storing a traceability of who was barman where and when.
|
||||||
|
Mainly for ~~dick size contest~~ establishing the top 10 barmen of the semester.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
user = models.ForeignKey(
|
user = models.ForeignKey(
|
||||||
@ -971,9 +929,7 @@ class CashRegisterSummary(models.Model):
|
|||||||
return object.__getattribute__(self, name)
|
return object.__getattribute__(self, name)
|
||||||
|
|
||||||
def is_owned_by(self, user):
|
def is_owned_by(self, user):
|
||||||
"""
|
"""Method to see if that object can be edited by the given user."""
|
||||||
Method to see if that object can be edited by the given user
|
|
||||||
"""
|
|
||||||
if user.is_anonymous:
|
if user.is_anonymous:
|
||||||
return False
|
return False
|
||||||
if user.is_in_group(pk=settings.SITH_GROUP_COUNTER_ADMIN_ID):
|
if user.is_in_group(pk=settings.SITH_GROUP_COUNTER_ADMIN_ID):
|
||||||
@ -1010,9 +966,7 @@ class CashRegisterSummaryItem(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
class Eticket(models.Model):
|
class Eticket(models.Model):
|
||||||
"""
|
"""Eticket can be linked to a product an allows PDF generation."""
|
||||||
Eticket can be linked to a product an allows PDF generation
|
|
||||||
"""
|
|
||||||
|
|
||||||
product = models.OneToOneField(
|
product = models.OneToOneField(
|
||||||
Product,
|
Product,
|
||||||
@ -1041,9 +995,7 @@ class Eticket(models.Model):
|
|||||||
return reverse("counter:eticket_list")
|
return reverse("counter:eticket_list")
|
||||||
|
|
||||||
def is_owned_by(self, user):
|
def is_owned_by(self, user):
|
||||||
"""
|
"""Method to see if that object can be edited by the given user."""
|
||||||
Method to see if that object can be edited by the given user
|
|
||||||
"""
|
|
||||||
if user.is_anonymous:
|
if user.is_anonymous:
|
||||||
return False
|
return False
|
||||||
return user.is_in_group(pk=settings.SITH_GROUP_COUNTER_ADMIN_ID)
|
return user.is_in_group(pk=settings.SITH_GROUP_COUNTER_ADMIN_ID)
|
||||||
@ -1058,11 +1010,11 @@ class Eticket(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
class StudentCard(models.Model):
|
class StudentCard(models.Model):
|
||||||
"""
|
"""Alternative way to connect a customer into a counter.
|
||||||
Alternative way to connect a customer into a counter
|
|
||||||
We are using Mifare DESFire EV1 specs since it's used for izly cards
|
We are using Mifare DESFire EV1 specs since it's used for izly cards
|
||||||
https://www.nxp.com/docs/en/application-note/AN10927.pdf
|
https://www.nxp.com/docs/en/application-note/AN10927.pdf
|
||||||
UID is 7 byte long that means 14 hexa characters
|
UID is 7 byte long that means 14 hexa characters.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
UID_SIZE = 14
|
UID_SIZE = 14
|
||||||
|
13
counter/schemas.py
Normal file
13
counter/schemas.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
from ninja import ModelSchema
|
||||||
|
|
||||||
|
from core.schemas import SimpleUserSchema
|
||||||
|
from counter.models import Counter
|
||||||
|
|
||||||
|
|
||||||
|
class CounterSchema(ModelSchema):
|
||||||
|
barmen_list: list[SimpleUserSchema]
|
||||||
|
is_open: bool
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Counter
|
||||||
|
fields = ["id", "name", "type", "club", "products"]
|
@ -41,7 +41,7 @@ def write_log(instance, operation_type):
|
|||||||
session_token = session.get("counter_token", None)
|
session_token = session.get("counter_token", None)
|
||||||
if session_token:
|
if session_token:
|
||||||
counter = Counter.objects.filter(token=session_token).first()
|
counter = Counter.objects.filter(token=session_token).first()
|
||||||
if counter and len(counter.get_barmen_list()) > 0:
|
if counter and len(counter.barmen_list) > 0:
|
||||||
return counter.get_random_barman()
|
return counter.get_random_barman()
|
||||||
|
|
||||||
# Get the current logged user if not from a counter
|
# Get the current logged user if not from a counter
|
||||||
|
@ -14,9 +14,8 @@
|
|||||||
{% if counter.type == 'BAR' %}
|
{% if counter.type == 'BAR' %}
|
||||||
<h4>{% trans %}Barmen list{% endtrans %}</h4>
|
<h4>{% trans %}Barmen list{% endtrans %}</h4>
|
||||||
<ul>
|
<ul>
|
||||||
{% set barmans_list = counter.get_barmen_list() %}
|
{% if counter.barmen_list | length > 0 %}
|
||||||
{% if barmans_list | length > 0 %}
|
{% for b in counter.barmen_list %}
|
||||||
{% for b in barmans_list %}
|
|
||||||
<li>{{ user_profile_link(b) }}</li>
|
<li>{{ user_profile_link(b) }}</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% else %}
|
{% else %}
|
||||||
|
@ -41,9 +41,10 @@
|
|||||||
<input type="submit" value="{% trans %}Go{% endtrans %}"/>
|
<input type="submit" value="{% trans %}Go{% endtrans %}"/>
|
||||||
</form>
|
</form>
|
||||||
<h6>{% trans %}Registered cards{% endtrans %}</h6>
|
<h6>{% trans %}Registered cards{% endtrans %}</h6>
|
||||||
{% if customer.student_cards.exists() %}
|
{% if student_cards %}
|
||||||
|
<p>{{ student_cards }}</p>
|
||||||
<ul>
|
<ul>
|
||||||
{% for card in customer.student_cards.all() %}
|
{% for card in student_cards %}
|
||||||
<li>{{ card.uid }}</li>
|
<li>{{ card.uid }}</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
@ -55,7 +56,7 @@
|
|||||||
<div id="click_form">
|
<div id="click_form">
|
||||||
<h5>{% trans %}Selling{% endtrans %}</h5>
|
<h5>{% trans %}Selling{% endtrans %}</h5>
|
||||||
<div>
|
<div>
|
||||||
{% set counter_click_url = url('counter:click', counter_id=counter.id, user_id=customer.user.id) %}
|
{% set counter_click_url = url('counter:click', counter_id=counter.id, user_id=customer.user_id) %}
|
||||||
|
|
||||||
{# Formulaire pour rechercher un produit en tapant son code dans une barre de recherche #}
|
{# Formulaire pour rechercher un produit en tapant son code dans une barre de recherche #}
|
||||||
<form method="post" action=""
|
<form method="post" action=""
|
||||||
@ -166,7 +167,7 @@
|
|||||||
{%- endfor %}
|
{%- endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock content %}
|
||||||
|
|
||||||
{% block script %}
|
{% block script %}
|
||||||
{{ super() }}
|
{{ super() }}
|
||||||
@ -193,4 +194,4 @@
|
|||||||
{%- endfor %}
|
{%- endfor %}
|
||||||
];
|
];
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock script %}
|
||||||
|
@ -27,7 +27,7 @@ from counter.models import BillingInfo, Counter, Customer, Permanency, Product,
|
|||||||
from sith.settings import SITH_MAIN_CLUB
|
from sith.settings import SITH_MAIN_CLUB
|
||||||
|
|
||||||
|
|
||||||
class CounterTest(TestCase):
|
class TestCounter(TestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
cls.skia = User.objects.filter(username="skia").first()
|
cls.skia = User.objects.filter(username="skia").first()
|
||||||
@ -140,10 +140,7 @@ class CounterTest(TestCase):
|
|||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
def test_annotate_has_barman_queryset(self):
|
def test_annotate_has_barman_queryset(self):
|
||||||
"""
|
"""Test if the custom queryset method `annotate_has_barman` works as intended."""
|
||||||
Test if the custom queryset method ``annotate_has_barman``
|
|
||||||
works as intended
|
|
||||||
"""
|
|
||||||
self.sli.counters.set([self.foyer, self.mde])
|
self.sli.counters.set([self.foyer, self.mde])
|
||||||
counters = Counter.objects.annotate_has_barman(self.sli)
|
counters = Counter.objects.annotate_has_barman(self.sli)
|
||||||
for counter in counters:
|
for counter in counters:
|
||||||
@ -153,7 +150,7 @@ class CounterTest(TestCase):
|
|||||||
assert not counter.has_annotated_barman
|
assert not counter.has_annotated_barman
|
||||||
|
|
||||||
|
|
||||||
class CounterStatsTest(TestCase):
|
class TestCounterStats(TestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
cls.counter = Counter.objects.get(id=2)
|
cls.counter = Counter.objects.get(id=2)
|
||||||
@ -265,15 +262,11 @@ class CounterStatsTest(TestCase):
|
|||||||
assert response.status_code == 403
|
assert response.status_code == 403
|
||||||
|
|
||||||
def test_get_total_sales(self):
|
def test_get_total_sales(self):
|
||||||
"""
|
"""Test the result of the Counter.get_total_sales() method."""
|
||||||
Test the result of the Counter.get_total_sales() method
|
|
||||||
"""
|
|
||||||
assert self.counter.get_total_sales() == 3102
|
assert self.counter.get_total_sales() == 3102
|
||||||
|
|
||||||
def test_top_barmen(self):
|
def test_top_barmen(self):
|
||||||
"""
|
"""Test the result of Counter.get_top_barmen() is correct."""
|
||||||
Test the result of Counter.get_top_barmen() is correct
|
|
||||||
"""
|
|
||||||
users = [self.skia, self.root, self.sli]
|
users = [self.skia, self.root, self.sli]
|
||||||
perm_times = [
|
perm_times = [
|
||||||
timedelta(days=16, hours=2, minutes=35, seconds=54),
|
timedelta(days=16, hours=2, minutes=35, seconds=54),
|
||||||
@ -292,9 +285,7 @@ class CounterStatsTest(TestCase):
|
|||||||
]
|
]
|
||||||
|
|
||||||
def test_top_customer(self):
|
def test_top_customer(self):
|
||||||
"""
|
"""Test the result of Counter.get_top_customers() is correct."""
|
||||||
Test the result of Counter.get_top_customers() is correct
|
|
||||||
"""
|
|
||||||
users = [self.sli, self.skia, self.krophil, self.root]
|
users = [self.sli, self.skia, self.krophil, self.root]
|
||||||
sale_amounts = [2000, 1000, 100, 2]
|
sale_amounts = [2000, 1000, 100, 2]
|
||||||
assert list(self.counter.get_top_customers()) == [
|
assert list(self.counter.get_top_customers()) == [
|
||||||
@ -309,7 +300,7 @@ class CounterStatsTest(TestCase):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class BillingInfoTest(TestCase):
|
class TestBillingInfo(TestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
cls.payload_1 = {
|
cls.payload_1 = {
|
||||||
@ -537,7 +528,7 @@ class BillingInfoTest(TestCase):
|
|||||||
assert infos.country == "FR"
|
assert infos.country == "FR"
|
||||||
|
|
||||||
|
|
||||||
class BarmanConnectionTest(TestCase):
|
class TestBarmanConnection(TestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
cls.krophil = User.objects.get(username="krophil")
|
cls.krophil = User.objects.get(username="krophil")
|
||||||
@ -587,10 +578,9 @@ class BarmanConnectionTest(TestCase):
|
|||||||
assert not '<li><a href="/user/1/">S' Kia</a></li>' in str(response.content)
|
assert not '<li><a href="/user/1/">S' Kia</a></li>' in str(response.content)
|
||||||
|
|
||||||
|
|
||||||
class StudentCardTest(TestCase):
|
class TestStudentCard(TestCase):
|
||||||
"""
|
"""Tests for adding and deleting Stundent Cards
|
||||||
Tests for adding and deleting Stundent Cards
|
Test that an user can be found with it's student card.
|
||||||
Test that an user can be found with it's student card
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -893,7 +883,7 @@ class StudentCardTest(TestCase):
|
|||||||
assert response.status_code == 403
|
assert response.status_code == 403
|
||||||
|
|
||||||
|
|
||||||
class CustomerAccountIdTest(TestCase):
|
class TestCustomerAccountId(TestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
cls.user_a = User.objects.create(
|
cls.user_a = User.objects.create(
|
||||||
|
@ -40,8 +40,8 @@ urlpatterns = [
|
|||||||
name="activity",
|
name="activity",
|
||||||
),
|
),
|
||||||
path("<int:counter_id>/stats/", CounterStatView.as_view(), name="stats"),
|
path("<int:counter_id>/stats/", CounterStatView.as_view(), name="stats"),
|
||||||
path("<int:counter_id>/login/", CounterLogin.as_view(), name="login"),
|
path("<int:counter_id>/login/", counter_login, name="login"),
|
||||||
path("<int:counter_id>/logout/", CounterLogout.as_view(), name="logout"),
|
path("<int:counter_id>/logout/", counter_logout, name="logout"),
|
||||||
path(
|
path(
|
||||||
"eticket/<int:selling_id>/pdf/",
|
"eticket/<int:selling_id>/pdf/",
|
||||||
EticketPDFView.as_view(),
|
EticketPDFView.as_view(),
|
||||||
|
327
counter/views.py
327
counter/views.py
@ -15,10 +15,10 @@
|
|||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
from datetime import timezone as tz
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from urllib.parse import parse_qs
|
from urllib.parse import parse_qs
|
||||||
|
|
||||||
import pytz
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
@ -27,13 +27,19 @@ from django.db import DataError, transaction
|
|||||||
from django.db.models import F
|
from django.db.models import F
|
||||||
from django.forms import CheckboxSelectMultiple
|
from django.forms import CheckboxSelectMultiple
|
||||||
from django.forms.models import modelform_factory
|
from django.forms.models import modelform_factory
|
||||||
from django.http import Http404, HttpResponse, HttpResponseRedirect, JsonResponse
|
from django.http import (
|
||||||
from django.shortcuts import get_object_or_404
|
Http404,
|
||||||
|
HttpRequest,
|
||||||
|
HttpResponse,
|
||||||
|
HttpResponseRedirect,
|
||||||
|
JsonResponse,
|
||||||
|
)
|
||||||
|
from django.shortcuts import get_object_or_404, redirect
|
||||||
from django.urls import reverse, reverse_lazy
|
from django.urls import reverse, reverse_lazy
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.views.decorators.http import require_POST
|
from django.views.decorators.http import require_POST
|
||||||
from django.views.generic import DetailView, ListView, RedirectView, TemplateView
|
from django.views.generic import DetailView, ListView, TemplateView
|
||||||
from django.views.generic.base import View
|
from django.views.generic.base import View
|
||||||
from django.views.generic.edit import (
|
from django.views.generic.edit import (
|
||||||
CreateView,
|
CreateView,
|
||||||
@ -66,6 +72,7 @@ from counter.models import (
|
|||||||
Counter,
|
Counter,
|
||||||
Customer,
|
Customer,
|
||||||
Eticket,
|
Eticket,
|
||||||
|
Permanency,
|
||||||
Product,
|
Product,
|
||||||
ProductType,
|
ProductType,
|
||||||
Refilling,
|
Refilling,
|
||||||
@ -75,9 +82,7 @@ from counter.models import (
|
|||||||
|
|
||||||
|
|
||||||
class CounterAdminMixin(View):
|
class CounterAdminMixin(View):
|
||||||
"""
|
"""Protect counter admin section."""
|
||||||
This view is made to protect counter admin section
|
|
||||||
"""
|
|
||||||
|
|
||||||
edit_group = [settings.SITH_GROUP_COUNTER_ADMIN_ID]
|
edit_group = [settings.SITH_GROUP_COUNTER_ADMIN_ID]
|
||||||
edit_club = []
|
edit_club = []
|
||||||
@ -105,9 +110,7 @@ class CounterAdminMixin(View):
|
|||||||
|
|
||||||
|
|
||||||
class StudentCardDeleteView(DeleteView, CanEditMixin):
|
class StudentCardDeleteView(DeleteView, CanEditMixin):
|
||||||
"""
|
"""View used to delete a card from a user."""
|
||||||
View used to delete a card from a user
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = StudentCard
|
model = StudentCard
|
||||||
template_name = "core/delete_confirm.jinja"
|
template_name = "core/delete_confirm.jinja"
|
||||||
@ -210,9 +213,7 @@ class CounterTabsMixin(TabedViewMixin):
|
|||||||
class CounterMain(
|
class CounterMain(
|
||||||
CounterTabsMixin, CanViewMixin, DetailView, ProcessFormView, FormMixin
|
CounterTabsMixin, CanViewMixin, DetailView, ProcessFormView, FormMixin
|
||||||
):
|
):
|
||||||
"""
|
"""The public (barman) view."""
|
||||||
The public (barman) view
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = Counter
|
model = Counter
|
||||||
template_name = "counter/counter_main.jinja"
|
template_name = "counter/counter_main.jinja"
|
||||||
@ -239,9 +240,7 @@ class CounterMain(
|
|||||||
return super().post(request, *args, **kwargs)
|
return super().post(request, *args, **kwargs)
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
"""
|
"""We handle here the login form for the barman."""
|
||||||
We handle here the login form for the barman
|
|
||||||
"""
|
|
||||||
if self.request.method == "POST":
|
if self.request.method == "POST":
|
||||||
self.object = self.get_object()
|
self.object = self.get_object()
|
||||||
self.object.update_activity()
|
self.object.update_activity()
|
||||||
@ -262,7 +261,7 @@ class CounterMain(
|
|||||||
None, _("Bad location, someone is already logged in somewhere else")
|
None, _("Bad location, someone is already logged in somewhere else")
|
||||||
)
|
)
|
||||||
if self.object.type == "BAR":
|
if self.object.type == "BAR":
|
||||||
kwargs["barmen"] = self.object.get_barmen_list()
|
kwargs["barmen"] = self.object.barmen_list
|
||||||
elif self.request.user.is_authenticated:
|
elif self.request.user.is_authenticated:
|
||||||
kwargs["barmen"] = [self.request.user]
|
kwargs["barmen"] = [self.request.user]
|
||||||
if "last_basket" in self.request.session.keys():
|
if "last_basket" in self.request.session.keys():
|
||||||
@ -275,9 +274,7 @@ class CounterMain(
|
|||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
"""
|
"""We handle here the redirection, passing the user id of the asked customer."""
|
||||||
We handle here the redirection, passing the user id of the asked customer
|
|
||||||
"""
|
|
||||||
self.kwargs["user_id"] = form.cleaned_data["user_id"]
|
self.kwargs["user_id"] = form.cleaned_data["user_id"]
|
||||||
return super().form_valid(form)
|
return super().form_valid(form)
|
||||||
|
|
||||||
@ -286,10 +283,9 @@ class CounterMain(
|
|||||||
|
|
||||||
|
|
||||||
class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
|
class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
|
||||||
"""
|
"""The click view
|
||||||
The click view
|
|
||||||
This is a detail view not to have to worry about loading the counter
|
This is a detail view not to have to worry about loading the counter
|
||||||
Everything is made by hand in the post method
|
Everything is made by hand in the post method.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
model = Counter
|
model = Counter
|
||||||
@ -327,27 +323,21 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
|
|||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
self.customer = get_object_or_404(Customer, user__id=self.kwargs["user_id"])
|
self.customer = get_object_or_404(Customer, user__id=self.kwargs["user_id"])
|
||||||
obj = self.get_object()
|
obj: Counter = self.get_object()
|
||||||
if not self.customer.can_buy:
|
if not self.customer.can_buy:
|
||||||
raise Http404
|
raise Http404
|
||||||
if obj.type == "BAR":
|
if obj.type != "BAR" and not request.user.is_authenticated:
|
||||||
if (
|
|
||||||
not (
|
|
||||||
"counter_token" in request.session.keys()
|
|
||||||
and request.session["counter_token"] == obj.token
|
|
||||||
)
|
|
||||||
or len(obj.get_barmen_list()) < 1
|
|
||||||
):
|
|
||||||
return HttpResponseRedirect(
|
|
||||||
reverse_lazy("counter:details", kwargs={"counter_id": obj.id})
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
if not request.user.is_authenticated:
|
|
||||||
raise PermissionDenied
|
raise PermissionDenied
|
||||||
|
if (
|
||||||
|
"counter_token" not in request.session
|
||||||
|
or request.session["counter_token"] != obj.token
|
||||||
|
or len(obj.barmen_list) == 0
|
||||||
|
):
|
||||||
|
return redirect(obj)
|
||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
"""Simple get view"""
|
"""Simple get view."""
|
||||||
if "basket" not in request.session.keys(): # Init the basket session entry
|
if "basket" not in request.session.keys(): # Init the basket session entry
|
||||||
request.session["basket"] = {}
|
request.session["basket"] = {}
|
||||||
request.session["basket_total"] = 0
|
request.session["basket_total"] = 0
|
||||||
@ -358,17 +348,17 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
|
|||||||
self.refill_form = None
|
self.refill_form = None
|
||||||
ret = super().get(request, *args, **kwargs)
|
ret = super().get(request, *args, **kwargs)
|
||||||
if (self.object.type != "BAR" and not request.user.is_authenticated) or (
|
if (self.object.type != "BAR" and not request.user.is_authenticated) or (
|
||||||
self.object.type == "BAR" and len(self.object.get_barmen_list()) < 1
|
self.object.type == "BAR" and len(self.object.barmen_list) == 0
|
||||||
): # Check that at least one barman is logged in
|
): # Check that at least one barman is logged in
|
||||||
ret = self.cancel(request) # Otherwise, go to main view
|
ret = self.cancel(request) # Otherwise, go to main view
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
"""Handle the many possibilities of the post request"""
|
"""Handle the many possibilities of the post request."""
|
||||||
self.object = self.get_object()
|
self.object = self.get_object()
|
||||||
self.refill_form = None
|
self.refill_form = None
|
||||||
if (self.object.type != "BAR" and not request.user.is_authenticated) or (
|
if (self.object.type != "BAR" and not request.user.is_authenticated) or (
|
||||||
self.object.type == "BAR" and len(self.object.get_barmen_list()) < 1
|
self.object.type == "BAR" and len(self.object.barmen_list) < 1
|
||||||
): # Check that at least one barman is logged in
|
): # Check that at least one barman is logged in
|
||||||
return self.cancel(request)
|
return self.cancel(request)
|
||||||
if self.object.type == "BAR" and not (
|
if self.object.type == "BAR" and not (
|
||||||
@ -481,10 +471,9 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
|
|||||||
return len(request.POST) == 0 and len(request.body) != 0
|
return len(request.POST) == 0 and len(request.body) != 0
|
||||||
|
|
||||||
def add_product(self, request, q=1, p=None):
|
def add_product(self, request, q=1, p=None):
|
||||||
"""
|
"""Add a product to the basket
|
||||||
Add a product to the basket
|
|
||||||
q is the quantity passed as integer
|
q is the quantity passed as integer
|
||||||
p is the product id, passed as an integer
|
p is the product id, passed as an integer.
|
||||||
"""
|
"""
|
||||||
pid = p or parse_qs(request.body.decode())["product_id"][0]
|
pid = p or parse_qs(request.body.decode())["product_id"][0]
|
||||||
pid = str(pid)
|
pid = str(pid)
|
||||||
@ -543,28 +532,24 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def add_student_card(self, request):
|
def add_student_card(self, request):
|
||||||
"""
|
"""Add a new student card on the customer account."""
|
||||||
Add a new student card on the customer account
|
uid = str(request.POST["student_card_uid"])
|
||||||
"""
|
|
||||||
uid = request.POST["student_card_uid"]
|
|
||||||
uid = str(uid)
|
|
||||||
if not StudentCard.is_valid(uid):
|
if not StudentCard.is_valid(uid):
|
||||||
request.session["not_valid_student_card_uid"] = True
|
request.session["not_valid_student_card_uid"] = True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if not (
|
if not (
|
||||||
self.object.type == "BAR"
|
self.object.type == "BAR"
|
||||||
and "counter_token" in request.session.keys()
|
and "counter_token" in request.session
|
||||||
and request.session["counter_token"] == self.object.token
|
and request.session["counter_token"] == self.object.token
|
||||||
and len(self.object.get_barmen_list()) > 0
|
and self.object.is_open
|
||||||
):
|
):
|
||||||
raise PermissionDenied
|
raise PermissionDenied
|
||||||
|
|
||||||
StudentCard(customer=self.customer, uid=uid).save()
|
StudentCard(customer=self.customer, uid=uid).save()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def del_product(self, request):
|
def del_product(self, request):
|
||||||
"""Delete a product from the basket"""
|
"""Delete a product from the basket."""
|
||||||
pid = parse_qs(request.body.decode())["product_id"][0]
|
pid = parse_qs(request.body.decode())["product_id"][0]
|
||||||
product = self.get_product(pid)
|
product = self.get_product(pid)
|
||||||
if pid in request.session["basket"]:
|
if pid in request.session["basket"]:
|
||||||
@ -581,11 +566,11 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
|
|||||||
request.session.modified = True
|
request.session.modified = True
|
||||||
|
|
||||||
def parse_code(self, request):
|
def parse_code(self, request):
|
||||||
"""
|
"""Parse the string entered by the barman.
|
||||||
Parse the string entered by the barman
|
|
||||||
This can be of two forms :
|
This can be of two forms :
|
||||||
- <str>, where the string is the code of the product
|
- `<str>`, where the string is the code of the product
|
||||||
- <int>X<str>, where the integer is the quantity and str the code
|
- `<int>X<str>`, where the integer is the quantity and str the code.
|
||||||
"""
|
"""
|
||||||
string = parse_qs(request.body.decode()).get("code", [""])[0].upper()
|
string = parse_qs(request.body.decode()).get("code", [""])[0].upper()
|
||||||
if string == "FIN":
|
if string == "FIN":
|
||||||
@ -605,7 +590,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
|
|||||||
return self.render_to_response(context)
|
return self.render_to_response(context)
|
||||||
|
|
||||||
def finish(self, request):
|
def finish(self, request):
|
||||||
"""Finish the click session, and validate the basket"""
|
"""Finish the click session, and validate the basket."""
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
request.session["last_basket"] = []
|
request.session["last_basket"] = []
|
||||||
if self.sum_basket(request) > self.customer.amount:
|
if self.sum_basket(request) > self.customer.amount:
|
||||||
@ -657,7 +642,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def cancel(self, request):
|
def cancel(self, request):
|
||||||
"""Cancel the click session"""
|
"""Cancel the click session."""
|
||||||
kwargs = {"counter_id": self.object.id}
|
kwargs = {"counter_id": self.object.id}
|
||||||
request.session.pop("basket", None)
|
request.session.pop("basket", None)
|
||||||
return HttpResponseRedirect(
|
return HttpResponseRedirect(
|
||||||
@ -665,7 +650,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def refill(self, request):
|
def refill(self, request):
|
||||||
"""Refill the customer's account"""
|
"""Refill the customer's account."""
|
||||||
if not self.object.can_refill():
|
if not self.object.can_refill():
|
||||||
raise PermissionDenied
|
raise PermissionDenied
|
||||||
form = RefillForm(request.POST)
|
form = RefillForm(request.POST)
|
||||||
@ -678,7 +663,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
|
|||||||
self.refill_form = form
|
self.refill_form = form
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
"""Add customer to the context"""
|
"""Add customer to the context."""
|
||||||
kwargs = super().get_context_data(**kwargs)
|
kwargs = super().get_context_data(**kwargs)
|
||||||
products = self.object.products.select_related("product_type")
|
products = self.object.products.select_related("product_type")
|
||||||
if self.customer_is_barman():
|
if self.customer_is_barman():
|
||||||
@ -693,6 +678,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
|
|||||||
product
|
product
|
||||||
)
|
)
|
||||||
kwargs["customer"] = self.customer
|
kwargs["customer"] = self.customer
|
||||||
|
kwargs["student_cards"] = self.customer.student_cards.all()
|
||||||
kwargs["basket_total"] = self.sum_basket(self.request)
|
kwargs["basket_total"] = self.sum_basket(self.request)
|
||||||
kwargs["refill_form"] = self.refill_form or RefillForm()
|
kwargs["refill_form"] = self.refill_form or RefillForm()
|
||||||
kwargs["student_card_max_uid_size"] = StudentCard.UID_SIZE
|
kwargs["student_card_max_uid_size"] = StudentCard.UID_SIZE
|
||||||
@ -700,61 +686,34 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
|
|||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
|
|
||||||
class CounterLogin(RedirectView):
|
@require_POST
|
||||||
"""
|
def counter_login(request: HttpRequest, counter_id: int) -> HttpResponseRedirect:
|
||||||
Handle the login of a barman
|
"""Log a user in a counter.
|
||||||
|
|
||||||
Logged barmen are stored in the Permanency model
|
A successful login will result in the beginning of a counter duty
|
||||||
|
for the user.
|
||||||
"""
|
"""
|
||||||
|
counter = get_object_or_404(Counter, pk=counter_id)
|
||||||
permanent = False
|
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
|
||||||
"""
|
|
||||||
Register the logged user as barman for this counter
|
|
||||||
"""
|
|
||||||
self.counter_id = kwargs["counter_id"]
|
|
||||||
self.counter = Counter.objects.filter(id=kwargs["counter_id"]).first()
|
|
||||||
form = LoginForm(request, data=request.POST)
|
form = LoginForm(request, data=request.POST)
|
||||||
self.errors = []
|
if not form.is_valid():
|
||||||
if form.is_valid():
|
return redirect(counter.get_absolute_url() + "?credentials")
|
||||||
user = User.objects.filter(username=form.cleaned_data["username"]).first()
|
user = form.get_user()
|
||||||
if (
|
if not counter.sellers.contains(user) or user in counter.barmen_list:
|
||||||
user in self.counter.sellers.all()
|
return redirect(counter.get_absolute_url() + "?sellers")
|
||||||
and not user in self.counter.get_barmen_list()
|
if len(counter.barmen_list) == 0:
|
||||||
):
|
counter.gen_token()
|
||||||
if len(self.counter.get_barmen_list()) <= 0:
|
request.session["counter_token"] = counter.token
|
||||||
self.counter.gen_token()
|
counter.permanencies.create(user=user, start=timezone.now())
|
||||||
request.session["counter_token"] = self.counter.token
|
return redirect(counter)
|
||||||
self.counter.add_barman(user)
|
|
||||||
else:
|
|
||||||
self.errors += ["sellers"]
|
|
||||||
else:
|
|
||||||
self.errors += ["credentials"]
|
|
||||||
return super().post(request, *args, **kwargs)
|
|
||||||
|
|
||||||
def get_redirect_url(self, *args, **kwargs):
|
|
||||||
return (
|
@require_POST
|
||||||
reverse_lazy("counter:details", args=args, kwargs=kwargs)
|
def counter_logout(request: HttpRequest, counter_id: int) -> HttpResponseRedirect:
|
||||||
+ "?"
|
"""End the permanency of a user in this counter."""
|
||||||
+ "&".join(self.errors)
|
Permanency.objects.filter(counter=counter_id, user=request.POST["user_id"]).update(
|
||||||
|
end=F("activity")
|
||||||
)
|
)
|
||||||
|
return redirect("counter:details", counter_id=counter_id)
|
||||||
|
|
||||||
class CounterLogout(RedirectView):
|
|
||||||
permanent = False
|
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
|
||||||
"""
|
|
||||||
Unregister the user from the barman
|
|
||||||
"""
|
|
||||||
self.counter = Counter.objects.filter(id=kwargs["counter_id"]).first()
|
|
||||||
user = User.objects.filter(id=request.POST["user_id"]).first()
|
|
||||||
self.counter.del_barman(user)
|
|
||||||
return super().post(request, *args, **kwargs)
|
|
||||||
|
|
||||||
def get_redirect_url(self, *args, **kwargs):
|
|
||||||
return reverse_lazy("counter:details", args=args, kwargs=kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
# Counter admin views
|
# Counter admin views
|
||||||
@ -803,9 +762,7 @@ class CounterAdminTabsMixin(TabedViewMixin):
|
|||||||
|
|
||||||
|
|
||||||
class CounterListView(CounterAdminTabsMixin, CanViewMixin, ListView):
|
class CounterListView(CounterAdminTabsMixin, CanViewMixin, ListView):
|
||||||
"""
|
"""A list view for the admins."""
|
||||||
A list view for the admins
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = Counter
|
model = Counter
|
||||||
template_name = "counter/counter_list.jinja"
|
template_name = "counter/counter_list.jinja"
|
||||||
@ -813,9 +770,7 @@ class CounterListView(CounterAdminTabsMixin, CanViewMixin, ListView):
|
|||||||
|
|
||||||
|
|
||||||
class CounterEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
|
class CounterEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
|
||||||
"""
|
"""Edit a counter's main informations (for the counter's manager)."""
|
||||||
Edit a counter's main informations (for the counter's manager)
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = Counter
|
model = Counter
|
||||||
form_class = CounterEditForm
|
form_class = CounterEditForm
|
||||||
@ -833,9 +788,7 @@ class CounterEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
|
|||||||
|
|
||||||
|
|
||||||
class CounterEditPropView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
|
class CounterEditPropView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
|
||||||
"""
|
"""Edit a counter's main informations (for the counter's admin)."""
|
||||||
Edit a counter's main informations (for the counter's admin)
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = Counter
|
model = Counter
|
||||||
form_class = modelform_factory(Counter, fields=["name", "club", "type"])
|
form_class = modelform_factory(Counter, fields=["name", "club", "type"])
|
||||||
@ -845,9 +798,7 @@ class CounterEditPropView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
|
|||||||
|
|
||||||
|
|
||||||
class CounterCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView):
|
class CounterCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView):
|
||||||
"""
|
"""Create a counter (for the admins)."""
|
||||||
Create a counter (for the admins)
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = Counter
|
model = Counter
|
||||||
form_class = modelform_factory(
|
form_class = modelform_factory(
|
||||||
@ -860,9 +811,7 @@ class CounterCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView):
|
|||||||
|
|
||||||
|
|
||||||
class CounterDeleteView(CounterAdminTabsMixin, CounterAdminMixin, DeleteView):
|
class CounterDeleteView(CounterAdminTabsMixin, CounterAdminMixin, DeleteView):
|
||||||
"""
|
"""Delete a counter (for the admins)."""
|
||||||
Delete a counter (for the admins)
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = Counter
|
model = Counter
|
||||||
pk_url_kwarg = "counter_id"
|
pk_url_kwarg = "counter_id"
|
||||||
@ -875,9 +824,7 @@ class CounterDeleteView(CounterAdminTabsMixin, CounterAdminMixin, DeleteView):
|
|||||||
|
|
||||||
|
|
||||||
class ProductTypeListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
|
class ProductTypeListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
|
||||||
"""
|
"""A list view for the admins."""
|
||||||
A list view for the admins
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = ProductType
|
model = ProductType
|
||||||
template_name = "counter/producttype_list.jinja"
|
template_name = "counter/producttype_list.jinja"
|
||||||
@ -885,9 +832,7 @@ class ProductTypeListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
|
|||||||
|
|
||||||
|
|
||||||
class ProductTypeCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView):
|
class ProductTypeCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView):
|
||||||
"""
|
"""A create view for the admins."""
|
||||||
A create view for the admins
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = ProductType
|
model = ProductType
|
||||||
fields = ["name", "description", "comment", "icon", "priority"]
|
fields = ["name", "description", "comment", "icon", "priority"]
|
||||||
@ -896,9 +841,7 @@ class ProductTypeCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView
|
|||||||
|
|
||||||
|
|
||||||
class ProductTypeEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
|
class ProductTypeEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
|
||||||
"""
|
"""An edit view for the admins."""
|
||||||
An edit view for the admins
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = ProductType
|
model = ProductType
|
||||||
template_name = "core/edit.jinja"
|
template_name = "core/edit.jinja"
|
||||||
@ -908,9 +851,7 @@ class ProductTypeEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
|
|||||||
|
|
||||||
|
|
||||||
class ProductArchivedListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
|
class ProductArchivedListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
|
||||||
"""
|
"""A list view for the admins."""
|
||||||
A list view for the admins
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = Product
|
model = Product
|
||||||
template_name = "counter/product_list.jinja"
|
template_name = "counter/product_list.jinja"
|
||||||
@ -920,9 +861,7 @@ class ProductArchivedListView(CounterAdminTabsMixin, CounterAdminMixin, ListView
|
|||||||
|
|
||||||
|
|
||||||
class ProductListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
|
class ProductListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
|
||||||
"""
|
"""A list view for the admins."""
|
||||||
A list view for the admins
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = Product
|
model = Product
|
||||||
template_name = "counter/product_list.jinja"
|
template_name = "counter/product_list.jinja"
|
||||||
@ -932,9 +871,7 @@ class ProductListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
|
|||||||
|
|
||||||
|
|
||||||
class ProductCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView):
|
class ProductCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView):
|
||||||
"""
|
"""A create view for the admins."""
|
||||||
A create view for the admins
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = Product
|
model = Product
|
||||||
form_class = ProductEditForm
|
form_class = ProductEditForm
|
||||||
@ -943,9 +880,7 @@ class ProductCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView):
|
|||||||
|
|
||||||
|
|
||||||
class ProductEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
|
class ProductEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
|
||||||
"""
|
"""An edit view for the admins."""
|
||||||
An edit view for the admins
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = Product
|
model = Product
|
||||||
form_class = ProductEditForm
|
form_class = ProductEditForm
|
||||||
@ -955,18 +890,14 @@ class ProductEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
|
|||||||
|
|
||||||
|
|
||||||
class RefillingDeleteView(DeleteView):
|
class RefillingDeleteView(DeleteView):
|
||||||
"""
|
"""Delete a refilling (for the admins)."""
|
||||||
Delete a refilling (for the admins)
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = Refilling
|
model = Refilling
|
||||||
pk_url_kwarg = "refilling_id"
|
pk_url_kwarg = "refilling_id"
|
||||||
template_name = "core/delete_confirm.jinja"
|
template_name = "core/delete_confirm.jinja"
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
"""
|
"""We have here a very particular right handling, we can't inherit from CanEditPropMixin."""
|
||||||
We have here a very particular right handling, we can't inherit from CanEditPropMixin
|
|
||||||
"""
|
|
||||||
self.object = self.get_object()
|
self.object = self.get_object()
|
||||||
if (
|
if (
|
||||||
timezone.now() - self.object.date
|
timezone.now() - self.object.date
|
||||||
@ -990,18 +921,14 @@ class RefillingDeleteView(DeleteView):
|
|||||||
|
|
||||||
|
|
||||||
class SellingDeleteView(DeleteView):
|
class SellingDeleteView(DeleteView):
|
||||||
"""
|
"""Delete a selling (for the admins)."""
|
||||||
Delete a selling (for the admins)
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = Selling
|
model = Selling
|
||||||
pk_url_kwarg = "selling_id"
|
pk_url_kwarg = "selling_id"
|
||||||
template_name = "core/delete_confirm.jinja"
|
template_name = "core/delete_confirm.jinja"
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
"""
|
"""We have here a very particular right handling, we can't inherit from CanEditPropMixin."""
|
||||||
We have here a very particular right handling, we can't inherit from CanEditPropMixin
|
|
||||||
"""
|
|
||||||
self.object = self.get_object()
|
self.object = self.get_object()
|
||||||
if (
|
if (
|
||||||
timezone.now() - self.object.date
|
timezone.now() - self.object.date
|
||||||
@ -1028,9 +955,7 @@ class SellingDeleteView(DeleteView):
|
|||||||
|
|
||||||
|
|
||||||
class CashRegisterSummaryForm(forms.Form):
|
class CashRegisterSummaryForm(forms.Form):
|
||||||
"""
|
"""Provide the cash summary form."""
|
||||||
Provide the cash summary form
|
|
||||||
"""
|
|
||||||
|
|
||||||
ten_cents = forms.IntegerField(label=_("10 cents"), required=False, min_value=0)
|
ten_cents = forms.IntegerField(label=_("10 cents"), required=False, min_value=0)
|
||||||
twenty_cents = forms.IntegerField(label=_("20 cents"), required=False, min_value=0)
|
twenty_cents = forms.IntegerField(label=_("20 cents"), required=False, min_value=0)
|
||||||
@ -1238,9 +1163,7 @@ class CashRegisterSummaryForm(forms.Form):
|
|||||||
|
|
||||||
|
|
||||||
class CounterLastOperationsView(CounterTabsMixin, CanViewMixin, DetailView):
|
class CounterLastOperationsView(CounterTabsMixin, CanViewMixin, DetailView):
|
||||||
"""
|
"""Provide the last operations to allow barmen to delete them."""
|
||||||
Provide the last operations to allow barmen to delete them
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = Counter
|
model = Counter
|
||||||
pk_url_kwarg = "counter_id"
|
pk_url_kwarg = "counter_id"
|
||||||
@ -1248,12 +1171,10 @@ class CounterLastOperationsView(CounterTabsMixin, CanViewMixin, DetailView):
|
|||||||
current_tab = "last_ops"
|
current_tab = "last_ops"
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
"""
|
"""We have here again a very particular right handling."""
|
||||||
We have here again a very particular right handling
|
|
||||||
"""
|
|
||||||
self.object = self.get_object()
|
self.object = self.get_object()
|
||||||
if (
|
if (
|
||||||
self.object.get_barmen_list()
|
self.object.barmen_list
|
||||||
and "counter_token" in request.session.keys()
|
and "counter_token" in request.session.keys()
|
||||||
and request.session["counter_token"]
|
and request.session["counter_token"]
|
||||||
and Counter.objects.filter( # check if not null for counters that have no token set
|
and Counter.objects.filter( # check if not null for counters that have no token set
|
||||||
@ -1267,7 +1188,7 @@ class CounterLastOperationsView(CounterTabsMixin, CanViewMixin, DetailView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
"""Add form to the context"""
|
"""Add form to the context."""
|
||||||
kwargs = super().get_context_data(**kwargs)
|
kwargs = super().get_context_data(**kwargs)
|
||||||
threshold = timezone.now() - timedelta(
|
threshold = timezone.now() - timedelta(
|
||||||
minutes=settings.SITH_LAST_OPERATIONS_LIMIT
|
minutes=settings.SITH_LAST_OPERATIONS_LIMIT
|
||||||
@ -1282,9 +1203,7 @@ class CounterLastOperationsView(CounterTabsMixin, CanViewMixin, DetailView):
|
|||||||
|
|
||||||
|
|
||||||
class CounterCashSummaryView(CounterTabsMixin, CanViewMixin, DetailView):
|
class CounterCashSummaryView(CounterTabsMixin, CanViewMixin, DetailView):
|
||||||
"""
|
"""Provide the cash summary form."""
|
||||||
Provide the cash summary form
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = Counter
|
model = Counter
|
||||||
pk_url_kwarg = "counter_id"
|
pk_url_kwarg = "counter_id"
|
||||||
@ -1292,12 +1211,10 @@ class CounterCashSummaryView(CounterTabsMixin, CanViewMixin, DetailView):
|
|||||||
current_tab = "cash_summary"
|
current_tab = "cash_summary"
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
"""
|
"""We have here again a very particular right handling."""
|
||||||
We have here again a very particular right handling
|
|
||||||
"""
|
|
||||||
self.object = self.get_object()
|
self.object = self.get_object()
|
||||||
if (
|
if (
|
||||||
self.object.get_barmen_list()
|
self.object.barmen_list
|
||||||
and "counter_token" in request.session.keys()
|
and "counter_token" in request.session.keys()
|
||||||
and request.session["counter_token"]
|
and request.session["counter_token"]
|
||||||
and Counter.objects.filter( # check if not null for counters that have no token set
|
and Counter.objects.filter( # check if not null for counters that have no token set
|
||||||
@ -1327,16 +1244,14 @@ class CounterCashSummaryView(CounterTabsMixin, CanViewMixin, DetailView):
|
|||||||
return reverse_lazy("counter:details", kwargs={"counter_id": self.object.id})
|
return reverse_lazy("counter:details", kwargs={"counter_id": self.object.id})
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
"""Add form to the context"""
|
"""Add form to the context."""
|
||||||
kwargs = super().get_context_data(**kwargs)
|
kwargs = super().get_context_data(**kwargs)
|
||||||
kwargs["form"] = self.form
|
kwargs["form"] = self.form
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
|
|
||||||
class CounterActivityView(DetailView):
|
class CounterActivityView(DetailView):
|
||||||
"""
|
"""Show the bar activity."""
|
||||||
Show the bar activity
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = Counter
|
model = Counter
|
||||||
pk_url_kwarg = "counter_id"
|
pk_url_kwarg = "counter_id"
|
||||||
@ -1344,16 +1259,14 @@ class CounterActivityView(DetailView):
|
|||||||
|
|
||||||
|
|
||||||
class CounterStatView(DetailView, CounterAdminMixin):
|
class CounterStatView(DetailView, CounterAdminMixin):
|
||||||
"""
|
"""Show the bar stats."""
|
||||||
Show the bar stats
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = Counter
|
model = Counter
|
||||||
pk_url_kwarg = "counter_id"
|
pk_url_kwarg = "counter_id"
|
||||||
template_name = "counter/stats.jinja"
|
template_name = "counter/stats.jinja"
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
"""Add stats to the context"""
|
"""Add stats to the context."""
|
||||||
counter: Counter = self.object
|
counter: Counter = self.object
|
||||||
semester_start = get_start_of_semester()
|
semester_start = get_start_of_semester()
|
||||||
office_hours = counter.get_top_barmen()
|
office_hours = counter.get_top_barmen()
|
||||||
@ -1386,7 +1299,7 @@ class CounterStatView(DetailView, CounterAdminMixin):
|
|||||||
|
|
||||||
|
|
||||||
class CashSummaryEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
|
class CashSummaryEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
|
||||||
"""Edit cash summaries"""
|
"""Edit cash summaries."""
|
||||||
|
|
||||||
model = CashRegisterSummary
|
model = CashRegisterSummary
|
||||||
template_name = "counter/cash_register_summary.jinja"
|
template_name = "counter/cash_register_summary.jinja"
|
||||||
@ -1400,7 +1313,7 @@ class CashSummaryEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
|
|||||||
|
|
||||||
|
|
||||||
class CashSummaryListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
|
class CashSummaryListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
|
||||||
"""Display a list of cash summaries"""
|
"""Display a list of cash summaries."""
|
||||||
|
|
||||||
model = CashRegisterSummary
|
model = CashRegisterSummary
|
||||||
template_name = "counter/cash_summary_list.jinja"
|
template_name = "counter/cash_summary_list.jinja"
|
||||||
@ -1410,7 +1323,7 @@ class CashSummaryListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
|
|||||||
paginate_by = settings.SITH_COUNTER_CASH_SUMMARY_LENGTH
|
paginate_by = settings.SITH_COUNTER_CASH_SUMMARY_LENGTH
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
"""Add sums to the context"""
|
"""Add sums to the context."""
|
||||||
kwargs = super().get_context_data(**kwargs)
|
kwargs = super().get_context_data(**kwargs)
|
||||||
form = CashSummaryFormBase(self.request.GET)
|
form = CashSummaryFormBase(self.request.GET)
|
||||||
kwargs["form"] = form
|
kwargs["form"] = form
|
||||||
@ -1439,10 +1352,10 @@ class CashSummaryListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
refillings = refillings.filter(
|
refillings = refillings.filter(
|
||||||
date__gte=datetime(year=1994, month=5, day=17, tzinfo=pytz.UTC)
|
date__gte=datetime(year=1994, month=5, day=17, tzinfo=tz.utc)
|
||||||
) # My birth date should be old enough
|
) # My birth date should be old enough
|
||||||
cashredistersummaries = cashredistersummaries.filter(
|
cashredistersummaries = cashredistersummaries.filter(
|
||||||
date__gte=datetime(year=1994, month=5, day=17, tzinfo=pytz.UTC)
|
date__gte=datetime(year=1994, month=5, day=17, tzinfo=tz.utc)
|
||||||
)
|
)
|
||||||
if form.is_valid() and form.cleaned_data["end_date"]:
|
if form.is_valid() and form.cleaned_data["end_date"]:
|
||||||
refillings = refillings.filter(date__lte=form.cleaned_data["end_date"])
|
refillings = refillings.filter(date__lte=form.cleaned_data["end_date"])
|
||||||
@ -1461,20 +1374,18 @@ class InvoiceCallView(CounterAdminTabsMixin, CounterAdminMixin, TemplateView):
|
|||||||
current_tab = "invoices_call"
|
current_tab = "invoices_call"
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
"""Add sums to the context"""
|
"""Add sums to the context."""
|
||||||
kwargs = super().get_context_data(**kwargs)
|
kwargs = super().get_context_data(**kwargs)
|
||||||
kwargs["months"] = Selling.objects.datetimes("date", "month", order="DESC")
|
kwargs["months"] = Selling.objects.datetimes("date", "month", order="DESC")
|
||||||
start_date = None
|
if "month" in self.request.GET:
|
||||||
end_date = None
|
|
||||||
try:
|
|
||||||
start_date = datetime.strptime(self.request.GET["month"], "%Y-%m")
|
start_date = datetime.strptime(self.request.GET["month"], "%Y-%m")
|
||||||
except:
|
else:
|
||||||
start_date = datetime(
|
start_date = datetime(
|
||||||
year=timezone.now().year,
|
year=timezone.now().year,
|
||||||
month=(timezone.now().month + 10) % 12 + 1,
|
month=(timezone.now().month + 10) % 12 + 1,
|
||||||
day=1,
|
day=1,
|
||||||
)
|
)
|
||||||
start_date = start_date.replace(tzinfo=pytz.UTC)
|
start_date = start_date.replace(tzinfo=tz.utc)
|
||||||
end_date = (start_date + timedelta(days=32)).replace(
|
end_date = (start_date + timedelta(days=32)).replace(
|
||||||
day=1, hour=0, minute=0, microsecond=0
|
day=1, hour=0, minute=0, microsecond=0
|
||||||
)
|
)
|
||||||
@ -1524,9 +1435,7 @@ class InvoiceCallView(CounterAdminTabsMixin, CounterAdminMixin, TemplateView):
|
|||||||
|
|
||||||
|
|
||||||
class EticketListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
|
class EticketListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
|
||||||
"""
|
"""A list view for the admins."""
|
||||||
A list view for the admins
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = Eticket
|
model = Eticket
|
||||||
template_name = "counter/eticket_list.jinja"
|
template_name = "counter/eticket_list.jinja"
|
||||||
@ -1535,9 +1444,7 @@ class EticketListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
|
|||||||
|
|
||||||
|
|
||||||
class EticketCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView):
|
class EticketCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView):
|
||||||
"""
|
"""Create an eticket."""
|
||||||
Create an eticket
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = Eticket
|
model = Eticket
|
||||||
template_name = "core/create.jinja"
|
template_name = "core/create.jinja"
|
||||||
@ -1546,9 +1453,7 @@ class EticketCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView):
|
|||||||
|
|
||||||
|
|
||||||
class EticketEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
|
class EticketEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
|
||||||
"""
|
"""Edit an eticket."""
|
||||||
Edit an eticket
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = Eticket
|
model = Eticket
|
||||||
template_name = "core/edit.jinja"
|
template_name = "core/edit.jinja"
|
||||||
@ -1558,9 +1463,7 @@ class EticketEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
|
|||||||
|
|
||||||
|
|
||||||
class EticketPDFView(CanViewMixin, DetailView):
|
class EticketPDFView(CanViewMixin, DetailView):
|
||||||
"""
|
"""Display the PDF of an eticket."""
|
||||||
Display the PDF of an eticket
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = Selling
|
model = Selling
|
||||||
pk_url_kwarg = "selling_id"
|
pk_url_kwarg = "selling_id"
|
||||||
@ -1649,9 +1552,7 @@ class EticketPDFView(CanViewMixin, DetailView):
|
|||||||
|
|
||||||
|
|
||||||
class CounterRefillingListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
|
class CounterRefillingListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
|
||||||
"""
|
"""List of refillings on a counter."""
|
||||||
List of refillings on a counter
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = Refilling
|
model = Refilling
|
||||||
template_name = "counter/refilling_list.jinja"
|
template_name = "counter/refilling_list.jinja"
|
||||||
@ -1670,9 +1571,7 @@ class CounterRefillingListView(CounterAdminTabsMixin, CounterAdminMixin, ListVie
|
|||||||
|
|
||||||
|
|
||||||
class StudentCardFormView(FormView):
|
class StudentCardFormView(FormView):
|
||||||
"""
|
"""Add a new student card."""
|
||||||
Add a new student card
|
|
||||||
"""
|
|
||||||
|
|
||||||
form_class = StudentCardForm
|
form_class = StudentCardForm
|
||||||
template_name = "core/create.jinja"
|
template_name = "core/create.jinja"
|
||||||
|
Binary file not shown.
@ -1,3 +0,0 @@
|
|||||||
A Pen created at CodePen.io. You can find this one at https://codepen.io/anon/pen/PKVVXY.
|
|
||||||
|
|
||||||
This page was used to generate the color palette of the sith.
|
|
@ -1,74 +0,0 @@
|
|||||||
html, body {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: #424242;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
font-size: .875rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form {
|
|
||||||
margin: 5rem 0;
|
|
||||||
width: 30rem;
|
|
||||||
background: #FAFAFA;
|
|
||||||
padding: 2rem;
|
|
||||||
border-radius: .25rem;
|
|
||||||
box-shadow: 0 4px 5px hsla(0, 0%, 0%, .5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group {
|
|
||||||
display: flex;
|
|
||||||
flex-flow: column nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group:not(:first-child) {
|
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.color-display {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.color-group {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.color-group > .color-code {
|
|
||||||
flex: 1 0 auto;
|
|
||||||
margin-left: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.color-hue {
|
|
||||||
height: 2rem;
|
|
||||||
align-self: stretch;
|
|
||||||
background-image: linear-gradient(to right, red, yellow, lime, cyan, blue, magenta, red)
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-input {
|
|
||||||
width: 100%;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.color-preview {
|
|
||||||
height: 5rem;
|
|
||||||
width: 5rem;
|
|
||||||
background-color: lightgrey;
|
|
||||||
border: 1px solid black;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.color-text {
|
|
||||||
font-size: 3rem;
|
|
||||||
}
|
|
@ -1,422 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html >
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>Color Theory Palette creator</title>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<style>
|
|
||||||
/* NOTE: The styles were added inline because Prefixfree needs access to your styles and they must be inlined if they are on local disk! */
|
|
||||||
html, body {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: #424242;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
font-size: .875rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form {
|
|
||||||
margin: 5rem 0;
|
|
||||||
width: 30rem;
|
|
||||||
background: #FAFAFA;
|
|
||||||
padding: 2rem;
|
|
||||||
border-radius: .25rem;
|
|
||||||
box-shadow: 0 4px 5px hsla(0, 0%, 0%, .5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group {
|
|
||||||
display: flex;
|
|
||||||
flex-flow: column nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group:not(:first-child) {
|
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.color-display {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.color-group {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.color-group > .color-code {
|
|
||||||
flex: 1 0 auto;
|
|
||||||
margin-left: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.color-hue {
|
|
||||||
height: 2rem;
|
|
||||||
align-self: stretch;
|
|
||||||
background-image: linear-gradient(to right, red, yellow, lime, cyan, blue, magenta, red)
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-input {
|
|
||||||
width: 100%;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.color-preview {
|
|
||||||
height: 5rem;
|
|
||||||
width: 5rem;
|
|
||||||
background-color: lightgrey;
|
|
||||||
border: 1px solid black;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.color-text {
|
|
||||||
font-size: 3rem;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/prefixfree/1.0.7/prefixfree.min.js"></script>
|
|
||||||
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<form name="color-theory" class="form">
|
|
||||||
<div class="form-group">
|
|
||||||
<div class="color-hue"></div>
|
|
||||||
<input type="range" name="first-color" class="form-input" min="0" max="359" value="220">
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<fieldset class="first-color">
|
|
||||||
<legend>First color</legend>
|
|
||||||
<div class="color-group">
|
|
||||||
<div class="color-preview"></div>
|
|
||||||
<input type="text" class="color-code" readonly>
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
|
||||||
<fieldset class="second-color">
|
|
||||||
<legend>Second color</legend>
|
|
||||||
<div class="color-group">
|
|
||||||
<div class="color-preview"></div>
|
|
||||||
<input type="text" class="color-code" readonly>
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<fieldset class="primary-color">
|
|
||||||
<legend>Primary color</legend>
|
|
||||||
<div class="color-group">
|
|
||||||
<div class="color-preview"></div>
|
|
||||||
<input type="text" class="color-code" readonly>
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
|
||||||
<fieldset class="complementary-color">
|
|
||||||
<legend>Secondary color</legend>
|
|
||||||
<div class="color-group">
|
|
||||||
<div class="color-preview"></div>
|
|
||||||
<input type="text" class="color-code" readonly>
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<fieldset class="text-on-primary">
|
|
||||||
<legend>Text on primary color</legend>
|
|
||||||
<div class="color-group">
|
|
||||||
<div class="color-preview">
|
|
||||||
<span class="color-text">Ab</span>
|
|
||||||
</div>
|
|
||||||
<input type="text" class="color-code" readonly>
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
|
||||||
<fieldset class="text-on-complementary">
|
|
||||||
<legend>Text on complementary color</legend>
|
|
||||||
<div class="color-group">
|
|
||||||
<div class="color-preview">
|
|
||||||
<span class="color-text">Ab</span>
|
|
||||||
</div>
|
|
||||||
<input type="text" class="color-code" readonly>
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<fieldset class="complementary-neutral-light">
|
|
||||||
<legend>Complementary neutral light color</legend>
|
|
||||||
<div class="color-group">
|
|
||||||
<div class="color-preview">
|
|
||||||
<span class="color-text">Ab</span>
|
|
||||||
</div>
|
|
||||||
<input type="text" class="color-code" readonly>
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
|
||||||
<fieldset class="complementary-neutral">
|
|
||||||
<legend>Complementary neutral color</legend>
|
|
||||||
<div class="color-group">
|
|
||||||
<div class="color-preview">
|
|
||||||
<span class="color-text">Ab</span>
|
|
||||||
</div>
|
|
||||||
<input type="text" class="color-code" readonly>
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
|
||||||
<fieldset class="complementary-neutral-dark">
|
|
||||||
<legend>Complementary neutral dark color</legend>
|
|
||||||
<div class="color-group">
|
|
||||||
<div class="color-preview">
|
|
||||||
<span class="color-text">Ab</span>
|
|
||||||
</div>
|
|
||||||
<input type="text" class="color-code" readonly>
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
|
||||||
<fieldset class="primary-neutral-light">
|
|
||||||
<legend>Primary neutral light color</legend>
|
|
||||||
<div class="color-group">
|
|
||||||
<div class="color-preview">
|
|
||||||
<span class="color-text">Ab</span>
|
|
||||||
</div>
|
|
||||||
<input type="text" class="color-code" readonly>
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
|
||||||
<fieldset class="primary-neutral">
|
|
||||||
<legend>Primary neutral color</legend>
|
|
||||||
<div class="color-group">
|
|
||||||
<div class="color-preview">
|
|
||||||
<span class="color-text">Ab</span>
|
|
||||||
</div>
|
|
||||||
<input type="text" class="color-code" readonly>
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
|
||||||
<fieldset class="primary-neutral-dark">
|
|
||||||
<legend>Primary neutral dark color</legend>
|
|
||||||
<div class="color-group">
|
|
||||||
<div class="color-preview">
|
|
||||||
<span class="color-text">Ab</span>
|
|
||||||
</div>
|
|
||||||
<input type="text" class="color-code" readonly>
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<fieldset class="white">
|
|
||||||
<legend>"White" color</legend>
|
|
||||||
<div class="color-group">
|
|
||||||
<div class="color-preview">
|
|
||||||
<span class="color-text">Ab</span>
|
|
||||||
</div>
|
|
||||||
<input type="text" class="color-code" readonly>
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
|
||||||
<fieldset class="black">
|
|
||||||
<legend>"Black" color</legend>
|
|
||||||
<div class="color-group">
|
|
||||||
<div class="color-preview">
|
|
||||||
<span class="color-text">Ab</span>
|
|
||||||
</div>
|
|
||||||
<input type="text" class="color-code" readonly>
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<fieldset class="primary-light">
|
|
||||||
<legend>Primary light color</legend>
|
|
||||||
<div class="color-group">
|
|
||||||
<div class="color-preview">
|
|
||||||
<span class="color-text">Ab</span>
|
|
||||||
</div>
|
|
||||||
<input type="text" class="color-code" readonly>
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
|
||||||
<fieldset class="primary-color">
|
|
||||||
<legend>Primary color</legend>
|
|
||||||
<div class="color-group">
|
|
||||||
<div class="color-preview">
|
|
||||||
<span class="color-text">Ab</span>
|
|
||||||
</div>
|
|
||||||
<input type="text" class="color-code" readonly>
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
|
||||||
<fieldset class="primary-dark">
|
|
||||||
<legend>Primary dark color</legend>
|
|
||||||
<div class="color-group">
|
|
||||||
<div class="color-preview">
|
|
||||||
<span class="color-text">Ab</span>
|
|
||||||
</div>
|
|
||||||
<input type="text" class="color-code" readonly>
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
<template id="scss-template">
|
|
||||||
<style type="text/scss">
|
|
||||||
//From https://github.com/tallys/color-theory
|
|
||||||
|
|
||||||
//Pick a color
|
|
||||||
|
|
||||||
// THIS IS SET IN THE THEME FILE AT THE TOP LEVEL
|
|
||||||
// $first-color: hsl(17, 100%, 50%);
|
|
||||||
|
|
||||||
// Find the complement
|
|
||||||
|
|
||||||
$second-color: complement($first-color);
|
|
||||||
|
|
||||||
//Check if you have a cool color on your hands. Cool colors will overpower warm colors when mixing.
|
|
||||||
|
|
||||||
@function is-cool-color($color) {
|
|
||||||
@return hue($color) < 310 and hue($color) > 140;
|
|
||||||
}
|
|
||||||
|
|
||||||
@function is-high-key-value($color) {
|
|
||||||
@return hue($color) > 20 and hue($color) < 190;
|
|
||||||
}
|
|
||||||
|
|
||||||
@function is-highest-key-value($color) {
|
|
||||||
@return hue($color) > 30 and hue($color) <90;
|
|
||||||
}
|
|
||||||
|
|
||||||
//Establish a relationship (similar lighting conditions) between colors.
|
|
||||||
|
|
||||||
@function harmonious-mix($mix, $base) {
|
|
||||||
@if (is-cool-color($mix)){
|
|
||||||
@if is-high-key-value($base) {
|
|
||||||
@return mix($mix, $base, 11%);
|
|
||||||
}
|
|
||||||
@else {
|
|
||||||
@return mix($mix, $base, 16%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@else {
|
|
||||||
@if is-high-key-value($base) {
|
|
||||||
@return mix($mix, $base, 13%);
|
|
||||||
}
|
|
||||||
@else {
|
|
||||||
@return mix($mix, $base, 23%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@function mix-neutral($color) {
|
|
||||||
@if (is-highest-key-value($color)) {
|
|
||||||
@if is-high-key-value(complement($color)) {
|
|
||||||
@return mix(complement($color), $color, 19%);
|
|
||||||
}
|
|
||||||
@else {
|
|
||||||
@return mix(complement($color), $color, 13%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@else if (is-high-key-value($color)) {
|
|
||||||
@if is-high-key-value(complement($color)) {
|
|
||||||
@return mix(complement($color), $color, 31%);
|
|
||||||
}
|
|
||||||
@else {
|
|
||||||
@return mix(complement($color), $color, 23%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@else {
|
|
||||||
@if is-highest-key-value(complement($color)) {
|
|
||||||
@return mix(complement($color), $color, 31%);
|
|
||||||
}
|
|
||||||
@if is-high-key-value(complement($color)) {
|
|
||||||
@return mix(complement($color), $color, 26%);
|
|
||||||
}
|
|
||||||
@else {
|
|
||||||
@return mix(complement($color), $color, 23%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@function pick-contrast-of($color) {
|
|
||||||
@if is-high-key-value($color){
|
|
||||||
@if lightness($color) < 30% {
|
|
||||||
@return lighten(complement($color), 86);
|
|
||||||
}
|
|
||||||
@else if lightness($color) > 70% {
|
|
||||||
@return darken(complement($color), 68);
|
|
||||||
}
|
|
||||||
@else {
|
|
||||||
@return darken(complement($color), 53);
|
|
||||||
}
|
|
||||||
} @else {
|
|
||||||
@if lightness($color) < 30% {
|
|
||||||
@return lighten(complement($color), 86);
|
|
||||||
}
|
|
||||||
@else if lightness($color) > 70% {
|
|
||||||
@return darken(complement($color), 68);
|
|
||||||
}
|
|
||||||
@else {
|
|
||||||
@return lighten(complement($color), 53);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$primary-color: harmonious-mix($second-color, $first-color);
|
|
||||||
$complementary-color: harmonious-mix($first-color, $second-color);
|
|
||||||
|
|
||||||
// Complementary Neutrals, highlight, midtone, shadow
|
|
||||||
|
|
||||||
$complementary-neutral: mix-neutral($complementary-color);
|
|
||||||
$complementary-neutral-light: lighten($complementary-neutral, 33);
|
|
||||||
$complementary-neutral-dark: darken($complementary-neutral, 33);
|
|
||||||
|
|
||||||
// Primary neutrals, highlight, midtone, shadow`
|
|
||||||
|
|
||||||
$primary-neutral: mix-neutral($primary-color);
|
|
||||||
$primary-neutral-light: lighten($primary-neutral, 33);
|
|
||||||
$primary-neutral-dark: darken($primary-neutral, 33);
|
|
||||||
|
|
||||||
// Primary tint and shade
|
|
||||||
|
|
||||||
$primary-light: mix($primary-neutral-light, $primary-color, 45%);
|
|
||||||
$primary-dark: mix($primary-neutral-dark, $primary-color, 45%);
|
|
||||||
|
|
||||||
$complementary-light: mix($complementary-neutral-light, $complementary-color, 45%);
|
|
||||||
|
|
||||||
// Pure neutrals, highlight, midtone, shadow
|
|
||||||
|
|
||||||
$white: lighten($primary-neutral-light, 15);
|
|
||||||
$neutral-gray: grayscale($primary-neutral);
|
|
||||||
$primary-gray: mix($primary-color, $complementary-color, 30);
|
|
||||||
$complementary-gray: mix($complementary-color, $primary-color, 63);
|
|
||||||
$black: grayscale($complementary-neutral-dark);
|
|
||||||
|
|
||||||
// Analogous Colors
|
|
||||||
|
|
||||||
$analogous-color: adjust-hue($complementary-color, -40);
|
|
||||||
$complementary-analogous: mix($analogous-color, $complementary-color, 66);
|
|
||||||
|
|
||||||
.first-color {color: unquote( 'hsl(') hue($first-color), saturation($first-color), lightness($first-color) unquote(')')}
|
|
||||||
.second-color {color: unquote( 'hsl(') hue($second-color), saturation($second-color), lightness($second-color) unquote(')')}
|
|
||||||
|
|
||||||
.primary-color {color: unquote( 'hsl(') hue($primary-color), saturation($primary-color), lightness($primary-color) unquote(')')}
|
|
||||||
.complementary-color {color: unquote( 'hsl(') hue($complementary-color), saturation($complementary-color), lightness($complementary-color) unquote(')')}
|
|
||||||
|
|
||||||
.primary-neutral-light {color: unquote( 'hsl(') hue($primary-neutral-light), saturation($primary-neutral-light), lightness($primary-neutral-light) unquote(')')}
|
|
||||||
.primary-neutral {color: unquote( 'hsl(') hue($primary-neutral), saturation($primary-neutral), lightness($primary-neutral) unquote(')')}
|
|
||||||
.primary-neutral-dark {color: unquote( 'hsl(') hue($primary-neutral-dark), saturation($primary-neutral-dark), lightness($primary-neutral-dark) unquote(')')}
|
|
||||||
.complementary-neutral-light {color: unquote( 'hsl(') hue($complementary-neutral-light), saturation($complementary-neutral-light), lightness($complementary-neutral-light) unquote(')')}
|
|
||||||
.complementary-neutral {color: unquote( 'hsl(') hue($complementary-neutral), saturation($complementary-neutral), lightness($complementary-neutral) unquote(')')}
|
|
||||||
.complementary-neutral-dark {color: unquote( 'hsl(') hue($complementary-neutral-dark), saturation($complementary-neutral-dark), lightness($complementary-neutral-dark) unquote(')')}
|
|
||||||
|
|
||||||
.white {color: unquote( 'hsl(') hue($white), saturation($white), lightness($white) unquote(')')}
|
|
||||||
.black {color: unquote( 'hsl(') hue($black), saturation($black), lightness($black) unquote(')')}
|
|
||||||
|
|
||||||
.primary-light {color: unquote( 'hsl(') hue($primary-light), saturation($primary-light), lightness($primary-light) unquote(')')}
|
|
||||||
.primary-dark {color: unquote( 'hsl(') hue($primary-dark), saturation($primary-dark), lightness($primary-dark) unquote(')')}
|
|
||||||
|
|
||||||
.complementary-light {color: unquote( 'hsl(') hue($complementary-light), saturation($complementary-light), lightness($complementary-light) unquote(')')}
|
|
||||||
.primary-gray {color: unquote( 'hsl(') hue($primary-gray), saturation($primary-gray), lightness($primary-gray) unquote(')')}
|
|
||||||
.neutral-gray {color: unquote( 'hsl(') hue($neutral-gray), saturation($neutral-gray), lightness($neutral-gray) unquote(')')}
|
|
||||||
.complementary-gray {color: unquote( 'hsl(') hue($complementary-gray), saturation($complementary-gray), lightness($complementary-gray) unquote(')')}
|
|
||||||
</style>
|
|
||||||
</template>
|
|
||||||
<script src='https://cdnjs.cloudflare.com/ajax/libs/sass.js/0.10.3/sass.sync.js'></script>
|
|
||||||
|
|
||||||
<script src="js/index.js"></script>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,103 +0,0 @@
|
|||||||
/* jshint esversion: 6 */
|
|
||||||
|
|
||||||
// This is using Sass.js to use Sass built-in color mixing functions
|
|
||||||
|
|
||||||
const firstColorChooser = document.forms['color-theory']['first-color'];
|
|
||||||
const firstColor = document.querySelector('.first-color');
|
|
||||||
const secondColor = document.querySelector('.second-color');
|
|
||||||
|
|
||||||
let hue = undefined;
|
|
||||||
let comp = undefined;
|
|
||||||
|
|
||||||
updateColors();
|
|
||||||
Sass.options({
|
|
||||||
precision: 1
|
|
||||||
});
|
|
||||||
computeScss();
|
|
||||||
|
|
||||||
document.forms['color-theory']['first-color'].addEventListener('input', updateColors);
|
|
||||||
|
|
||||||
document.forms['color-theory']['first-color'].addEventListener('change', computeScss);
|
|
||||||
|
|
||||||
function updateColors(ev) {
|
|
||||||
hue = parseInt(firstColorChooser.value);
|
|
||||||
comp = (hue + 180) % 360;
|
|
||||||
setColorPreview([firstColor], `hsl(${hue}, 100%, 50%)`);
|
|
||||||
setColorPreview([secondColor], `hsl(${comp}, 100%, 50%)`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function setColorPreview(fieldsets, color) {
|
|
||||||
Array.from(fieldsets).forEach(fieldset => {
|
|
||||||
const preview = fieldset.querySelector('.color-preview');
|
|
||||||
preview.style.backgroundColor = fieldset.querySelector('.color-code').value = color;
|
|
||||||
const text = fieldset.querySelector('.color-text')
|
|
||||||
if(text)
|
|
||||||
text.style.color = computeTextColor(window.getComputedStyle(preview).backgroundColor);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function setColorText(fieldsets, bg, text) {
|
|
||||||
Array.from(fieldsets).forEach(fieldset => {
|
|
||||||
fieldset.querySelector('.color-preview').style.backgroundColor = bg;
|
|
||||||
fieldset.querySelector('.color-text').style.color = fieldset.querySelector('.color-code').value = text;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function computeScss() {
|
|
||||||
Sass.compile(
|
|
||||||
`$first-color: hsl(${hue}, 100%, 50%);` +
|
|
||||||
document.querySelector('#scss-template').content.firstElementChild.textContent, computedScssHandler);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function computedScssHandler(result) {
|
|
||||||
let colors = {};
|
|
||||||
result.text.split('\n\n').forEach(rule => {
|
|
||||||
const color = /\.([\w\-]+) {\s*color: (hsl\() (\d{1,3}(?:\.\d+)?)deg(.*) (\));\s*}/.exec(rule).splice(1, 5).join('').split('hsl');
|
|
||||||
colors[color[0]] = `hsl${color[1]}`;
|
|
||||||
});
|
|
||||||
|
|
||||||
for (let colorName in colors)
|
|
||||||
if (document.querySelector(`.${colorName}`)) setColorPreview(document.querySelectorAll(`.${colorName}`), colors[colorName]);
|
|
||||||
|
|
||||||
const primaryTextColor = computeTextColor(window.getComputedStyle(document.querySelector('.primary-color .color-preview')).backgroundColor);
|
|
||||||
const complementaryTextColor = computeTextColor(window.getComputedStyle(document.querySelector('.complementary-color .color-preview')).backgroundColor);
|
|
||||||
setColorText([document.querySelector('.text-on-primary')], document.querySelector('.primary-color .color-preview').style.backgroundColor, primaryTextColor);
|
|
||||||
setColorText([document.querySelector('.text-on-complementary')], document.querySelector('.complementary-color .color-preview').style.backgroundColor, complementaryTextColor)
|
|
||||||
}
|
|
||||||
|
|
||||||
function computeTextColor(colorStr) {
|
|
||||||
const black = [0, 0, 0, .87];
|
|
||||||
const white = [255, 255, 255, 1];
|
|
||||||
|
|
||||||
[, , r, g, b, a] = /(rgba?)\((\d{1,3}), (\d{1,3}), (\d{1,3})(?:, (\d(?:\.\d+)))?\)/.exec(colorStr);
|
|
||||||
const color = [parseInt(r), parseInt(g), parseInt(b), parseFloat(a == undefined ? 1 : a)]
|
|
||||||
const blackContrast = computeConstrastRatio(black, color);
|
|
||||||
const whiteContrast = computeConstrastRatio(white, color);
|
|
||||||
return blackContrast < whiteContrast ? `hsl(0, 0%, 100%)` : `hsla(0, 0%, 0%, 0.87)`
|
|
||||||
}
|
|
||||||
|
|
||||||
function computeConstrastRatio([fr, fg, fb, fa], [br, bg, bb, ba]) {
|
|
||||||
if (fa < 1) {
|
|
||||||
fr = fr * fa + br * (1 - fa);
|
|
||||||
fg = fg * fa + bg * (1 - fa);
|
|
||||||
fb = fb * fa + bb * (1 - fa);
|
|
||||||
fa = 1;
|
|
||||||
}
|
|
||||||
const fl = luminance([fr, fg, fb]);
|
|
||||||
const bl = luminance([br, bg, bb]);
|
|
||||||
|
|
||||||
if (fl < bl)
|
|
||||||
return (bl + .05) / (fl + .05);
|
|
||||||
else
|
|
||||||
return (fl + .05) / (bl + .05);
|
|
||||||
}
|
|
||||||
|
|
||||||
function luminance([r, g, b]) {
|
|
||||||
return .2126 * colorComponent(r) + .7152 * colorComponent(g) + .0722 * colorComponent(b);
|
|
||||||
}
|
|
||||||
|
|
||||||
function colorComponent(color) {
|
|
||||||
const c = color / 255;
|
|
||||||
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
|
||||||
}
|
|
@ -1,16 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Copyright (c) 2017 by Captain Anonymous (https://codepen.io/anon/pen/PKVVXY)
|
|
||||||
|
|
||||||
|
|
||||||
Fork of an original work by Jean-Baptiste Lenglet (https://codepen.io/Magador/pen/YZjWxe)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
-->
|
|
Binary file not shown.
Binary file not shown.
@ -1,124 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
// --------------- VARIABLES A MODIFIER ---------------
|
|
||||||
|
|
||||||
// Ennonciation de variables
|
|
||||||
$pbx_site = '1520411'; //variable de test 1999888
|
|
||||||
$pbx_rang = '001'; //variable de test 32
|
|
||||||
$pbx_identifiant = '650995411'; //variable de test 3
|
|
||||||
$pbx_cmd = 'CMD_1'; //variable de test cmd_test1
|
|
||||||
$pbx_porteur = 'skia@git.an'; //variable de test test@test.fr
|
|
||||||
$pbx_total = '510'; //variable de test 100
|
|
||||||
// Suppression des points ou virgules dans le montant
|
|
||||||
$pbx_total = str_replace(",", "", $pbx_total);
|
|
||||||
$pbx_total = str_replace(".", "", $pbx_total);
|
|
||||||
|
|
||||||
// Paramétrage des urls de redirection après paiement
|
|
||||||
$pbx_effectue = 'http://www.votre-site.extention/page-de-confirmation';
|
|
||||||
$pbx_annule = 'http://www.votre-site.extention/page-d-annulation';
|
|
||||||
$pbx_refuse = 'http://www.votre-site.extention/page-de-refus';
|
|
||||||
// Paramétrage de l'url de retour back office site
|
|
||||||
$pbx_repondre_a = 'http://www.votre-site.extention/page-de-back-office-site';
|
|
||||||
// Paramétrage du retour back office site
|
|
||||||
$pbx_retour = 'Amount:M;BasketID:R;Auto:A;Error:E;Sig:K';
|
|
||||||
|
|
||||||
// Connection à la base de données
|
|
||||||
// mysql_connect...
|
|
||||||
// On récupère la clé secrète HMAC (stockée dans une base de données par exemple) et que l’on renseigne dans la variable $keyTest;
|
|
||||||
//$keyTest = '0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF';
|
|
||||||
$keyTest = '2d21b1f0d5b64bce056b342b5259db312dfc0176dcafb33eb804b6aaaa3acc07320742954ef3b052f36942b09f86ccb9d24c8814586c1a0d24319fd8985c19e5';
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// --------------- TESTS DE DISPONIBILITE DES SERVEURS ---------------
|
|
||||||
|
|
||||||
/*
|
|
||||||
$serveurs = array('tpeweb.paybox.com', //serveur primaire
|
|
||||||
'tpeweb1.paybox.com'); //serveur secondaire
|
|
||||||
$serveurOK = "";
|
|
||||||
//phpinfo(); <== voir paybox
|
|
||||||
foreach($serveurs as $serveur){
|
|
||||||
$doc = new DOMDocument();
|
|
||||||
$doc->loadHTMLFile('https://'.$serveur.'/load.html');
|
|
||||||
$server_status = "";
|
|
||||||
$element = $doc->getElementById('server_status');
|
|
||||||
if($element){
|
|
||||||
$server_status = $element->textContent;}
|
|
||||||
if($server_status == "OK"){
|
|
||||||
// Le serveur est prêt et les services opérationnels
|
|
||||||
$serveurOK = $serveur;
|
|
||||||
break;}
|
|
||||||
// else : La machine est disponible mais les services ne le sont pas.
|
|
||||||
}
|
|
||||||
//curl_close($ch); <== voir paybox
|
|
||||||
if(!$serveurOK){
|
|
||||||
die("Erreur : Aucun serveur n'a été trouvé");}
|
|
||||||
// Activation de l'univers de préproduction
|
|
||||||
//$serveurOK = 'preprod-tpeweb.paybox.com';
|
|
||||||
|
|
||||||
//Création de l'url cgi paybox
|
|
||||||
$serveurOK = 'https://'.$serveurOK.'/cgi/MYchoix_pagepaiement.cgi';
|
|
||||||
// echo $serveurOK;
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// --------------- TRAITEMENT DES VARIABLES ---------------
|
|
||||||
|
|
||||||
// On récupère la date au format ISO-8601
|
|
||||||
$dateTime = date("c");
|
|
||||||
$dateTime = "2016-07-26T15:38:11+02:00";
|
|
||||||
|
|
||||||
// On crée la chaîne à hacher sans URLencodage
|
|
||||||
$msg = "PBX_SITE=".$pbx_site.
|
|
||||||
"&PBX_RANG=".$pbx_rang.
|
|
||||||
"&PBX_IDENTIFIANT=".$pbx_identifiant.
|
|
||||||
"&PBX_TOTAL=".$pbx_total.
|
|
||||||
"&PBX_DEVISE=978".
|
|
||||||
"&PBX_CMD=".$pbx_cmd.
|
|
||||||
"&PBX_PORTEUR=".$pbx_porteur.
|
|
||||||
// "&PBX_REPONDRE_A=".$pbx_repondre_a.
|
|
||||||
"&PBX_RETOUR=".$pbx_retour.
|
|
||||||
// "&PBX_EFFECTUE=".$pbx_effectue.
|
|
||||||
// "&PBX_ANNULE=".$pbx_annule.
|
|
||||||
// "&PBX_REFUSE=".$pbx_refuse.
|
|
||||||
"&PBX_HASH=SHA512".
|
|
||||||
"&PBX_TIME=".$dateTime;
|
|
||||||
// echo $msg;
|
|
||||||
|
|
||||||
// Si la clé est en ASCII, On la transforme en binaire
|
|
||||||
$binKey = pack("H*", $keyTest);
|
|
||||||
|
|
||||||
// On calcule l’empreinte (à renseigner dans le paramètre PBX_HMAC) grâce à la fonction hash_hmac et //
|
|
||||||
// la clé binaire
|
|
||||||
// On envoi via la variable PBX_HASH l'algorithme de hachage qui a été utilisé (SHA512 dans ce cas)
|
|
||||||
// Pour afficher la liste des algorithmes disponibles sur votre environnement, décommentez la ligne //
|
|
||||||
// suivante
|
|
||||||
// print_r(hash_algos());
|
|
||||||
echo $msg, "\n\n";
|
|
||||||
var_dump($binKey);
|
|
||||||
$hmac = strtoupper(hash_hmac('sha512', $msg, $binKey));
|
|
||||||
|
|
||||||
// La chaîne sera envoyée en majuscule, d'où l'utilisation de strtoupper()
|
|
||||||
// On crée le formulaire à envoyer
|
|
||||||
// ATTENTION : l'ordre des champs est extrêmement important, il doit
|
|
||||||
// correspondre exactement à l'ordre des champs dans la chaîne hachée
|
|
||||||
?>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!------------------ ENVOI DES INFORMATIONS A PAYBOX (Formulaire) ------------------>
|
|
||||||
<form method="POST" action="<?php echo $serveurOK; ?>">
|
|
||||||
<input type="hidden" name="PBX_SITE" value="<?php echo $pbx_site; ?>">
|
|
||||||
<input type="hidden" name="PBX_RANG" value="<?php echo $pbx_rang; ?>">
|
|
||||||
<input type="hidden" name="PBX_IDENTIFIANT" value="<?php echo $pbx_identifiant; ?>">
|
|
||||||
<input type="hidden" name="PBX_TOTAL" value="<?php echo $pbx_total; ?>">
|
|
||||||
<input type="hidden" name="PBX_DEVISE" value="978">
|
|
||||||
<input type="hidden" name="PBX_CMD" value="<?php echo $pbx_cmd; ?>">
|
|
||||||
<input type="hidden" name="PBX_PORTEUR" value="<?php echo $pbx_porteur; ?>">
|
|
||||||
<input type="hidden" name="PBX_RETOUR" value="<?php echo $pbx_retour; ?>">
|
|
||||||
<input type="hidden" name="PBX_HASH" value="SHA512">
|
|
||||||
<input type="hidden" name="PBX_TIME" value="<?php echo $dateTime; ?>">
|
|
||||||
<input type="hidden" name="PBX_HMAC" value="<?php echo $hmac; ?>">
|
|
||||||
<input type="submit" value="Envoyer">
|
|
||||||
</form>
|
|
@ -1,63 +0,0 @@
|
|||||||
package signver;
|
|
||||||
|
|
||||||
import java.security.interfaces.RSAPublicKey;
|
|
||||||
import java.security.Signature;
|
|
||||||
import java.security.KeyFactory;
|
|
||||||
import java.security.spec.X509EncodedKeySpec;
|
|
||||||
import java.io.FileInputStream;
|
|
||||||
import java.io.DataInputStream;
|
|
||||||
|
|
||||||
import org.apache.commons.codec.binary.Base64;
|
|
||||||
import org.apache.commons.codec.net.URLCodec;
|
|
||||||
|
|
||||||
public class SignVer {
|
|
||||||
|
|
||||||
// verification signature RSA des donnees avec cle publique
|
|
||||||
|
|
||||||
private static boolean verify( byte[] dataBytes, byte[] sigBytes, String sigAlg, RSAPublicKey pubKey) throws Exception
|
|
||||||
{
|
|
||||||
Signature sig = Signature.getInstance(sigAlg);
|
|
||||||
sig.initVerify(pubKey);
|
|
||||||
sig.update(dataBytes);
|
|
||||||
return sig.verify(sigBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
// chargement de la cle AU FORMAT der :
|
|
||||||
// openssl rsa -inform PEM -in pbx_pubkey.pem -outform DER -pubin -out /tmp/pubkey.der
|
|
||||||
|
|
||||||
private static RSAPublicKey getPubKey(String pubKeyFile) throws Exception
|
|
||||||
{
|
|
||||||
FileInputStream fis = new FileInputStream(pubKeyFile);
|
|
||||||
DataInputStream dis = new DataInputStream(fis);
|
|
||||||
byte[] pubKeyBytes = new byte[fis.available()];
|
|
||||||
dis.readFully(pubKeyBytes);
|
|
||||||
fis.close();
|
|
||||||
dis.close();
|
|
||||||
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
|
|
||||||
// extraction cle
|
|
||||||
X509EncodedKeySpec pubSpec = new X509EncodedKeySpec(pubKeyBytes);
|
|
||||||
RSAPublicKey pubKey = (RSAPublicKey) keyFactory.generatePublic(pubSpec);
|
|
||||||
return pubKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
// exemple de verification de la signature
|
|
||||||
|
|
||||||
public static void main(String[] unused) throws Exception {
|
|
||||||
|
|
||||||
String sData = ""; // donnees signees URL encodees
|
|
||||||
String sSig = ""; // signature Base64 et URL encodee
|
|
||||||
|
|
||||||
// decodage ...
|
|
||||||
byte[] dataBytes = URLCodec.decodeUrl(sData.getBytes());
|
|
||||||
byte[] sigBytes = Base64.decodeBase64( URLCodec.decodeUrl(sSig.getBytes()));
|
|
||||||
|
|
||||||
// lecture de la cle publique
|
|
||||||
RSAPublicKey pubK = getPubKey("/tmp/pubkey.der");
|
|
||||||
|
|
||||||
// verification signature
|
|
||||||
boolean result = verify(dataBytes, sigBytes, "SHA1withRSA", pubK);
|
|
||||||
|
|
||||||
// affichage resultat
|
|
||||||
System.out.println("Resultat de la verification de signature : " + result);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,15 +0,0 @@
|
|||||||
-----BEGIN RSA PRIVATE KEY-----
|
|
||||||
MIICXAIBAAKBgQCvDYKaLH2xz4goZYXZWoHo6wyMb24A1iF7s70tB/g3XthEVS+/
|
|
||||||
Wov+ZGqNTMLc0L+HZAJjcEc9h8Br5jPLR4VhaoKi+rezDxTQweaC24ydJWFKRhyX
|
|
||||||
Bhm2Wfnhppgzv9EqZKOrFaTlLQHu0F+KWEd7LngP4xcW9qjt19MfEmk0swIDAQAB
|
|
||||||
AoGABqXztNlFuNAR8r7QU43tayQqKNc+jUeUo/cSkzg/RBMVEZtOoezVbkbwCQfG
|
|
||||||
Ss6ex4yTzqT//6U9OJvYkbrYpOdq2BinuRv9n/NlKhCJ/Ym9s/DS8D6xdEX/R5lg
|
|
||||||
mIURQYl9uHS6VVnLq79j38BsjIhDAvjuSzZGZUa4v0iBTHkCQQDWp4iPqv2jUzBW
|
|
||||||
UgDP6C+QFqLgYKuYxF+yyBCXO0XzaHaEJUBuxuvTU/kuIifk3lhXnV5r0sXJd2Ax
|
|
||||||
aQkoJXc1AkEA0MU+4SSYPYADQm1gcXXuR4Kjb0/QhIGRiotxVu2nLGS7aULe1D8p
|
|
||||||
XoLpSQCv3j5amtXVx1yTWuaEYZqHVeQxRwJAdOprh1UrMXpuKZYgux1MSr8JmA0P
|
|
||||||
afYL6eTupHC0eQ+8/d0Ma0oNyN1EK8yOzioNFCuy8ierc0CCNGdxhVxiwQJAP/cv
|
|
||||||
fOwpeS5v0TqSAjGQAHkWelSKHw9T+I8g+vF19zQl9+p1O7LeigayU5vSRtX0DNzX
|
|
||||||
022Z+JAIn58pODfioQJBALQGN2kFCSk935VnMUJ4X5qFCKiXBgebuARUSw6tDEpY
|
|
||||||
gFdqyJE4WQ4uWVz0D9M27lCa8wj7pYrOhB/UiMKbuqY=
|
|
||||||
-----END RSA PRIVATE KEY-----
|
|
@ -1,6 +0,0 @@
|
|||||||
-----BEGIN PUBLIC KEY-----
|
|
||||||
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCvDYKaLH2xz4goZYXZWoHo6wyM
|
|
||||||
b24A1iF7s70tB/g3XthEVS+/Wov+ZGqNTMLc0L+HZAJjcEc9h8Br5jPLR4VhaoKi
|
|
||||||
+rezDxTQweaC24ydJWFKRhyXBhm2Wfnhppgzv9EqZKOrFaTlLQHu0F+KWEd7LngP
|
|
||||||
4xcW9qjt19MfEmk0swIDAQAB
|
|
||||||
-----END PUBLIC KEY-----
|
|
@ -1,11 +0,0 @@
|
|||||||
<?php
|
|
||||||
$montant=$_GET['montant'];
|
|
||||||
$ref_com=$_GET['ref'];
|
|
||||||
$auto=$_GET['auto'];
|
|
||||||
$trans=$_GET['trans'];
|
|
||||||
print ("<center><b><h2>Votre transaction a été acceptée</h2></center></b><br>");
|
|
||||||
print ("<br><b>MONTANT : </b>$montant\n");
|
|
||||||
print ("<br><b>REFERENCE : </b>$ref_com\n");
|
|
||||||
print ("<br><b>AUTO : </b>$auto\n");
|
|
||||||
print ("<br><b>TRANS : </b>$trans\n");
|
|
||||||
?>
|
|
@ -1,11 +0,0 @@
|
|||||||
<?php
|
|
||||||
$montant=$_GET['montant'];
|
|
||||||
$ref_com=$_GET['ref'];
|
|
||||||
#$auto=$_GET['auto'];
|
|
||||||
$trans=$_GET['trans'];
|
|
||||||
print ("<center><b><h2>Votre transaction a été annulée</h2></center></b><br>");
|
|
||||||
print ("<br><b>MONTANT : </b>$montant\n");
|
|
||||||
print ("<br><b>REFERENCE : </b>$ref_com\n");
|
|
||||||
#print ("<br><b>AUTO : </b>$auto\n");
|
|
||||||
print ("<br><b>TRANS : </b>$trans\n");
|
|
||||||
?>
|
|
@ -1,79 +0,0 @@
|
|||||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
|
||||||
<html>
|
|
||||||
|
|
||||||
<head>
|
|
||||||
|
|
||||||
<title>Exemple_mail_ticket_client</title>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
<!--
|
|
||||||
/* Font Definitions */
|
|
||||||
@font-face
|
|
||||||
{font-family:"Bernard MT Condensed";
|
|
||||||
panose-1:0 0 0 0 0 0 0 0 0 0;}
|
|
||||||
/* Style Definitions */
|
|
||||||
p.MsoNormal, li.MsoNormal, div.MsoNormal
|
|
||||||
{margin:0cm;
|
|
||||||
margin-bottom:.0001pt;
|
|
||||||
font-size:12.0pt;
|
|
||||||
font-family:"Times New Roman";}
|
|
||||||
a:link, span.MsoHyperlink
|
|
||||||
{color:blue;
|
|
||||||
text-decoration:underline;}
|
|
||||||
a:visited, span.MsoHyperlinkFollowed
|
|
||||||
{color:purple;
|
|
||||||
text-decoration:underline;}
|
|
||||||
p
|
|
||||||
{font-size:12.0pt;
|
|
||||||
font-family:"Times New Roman";}
|
|
||||||
p.style1, li.style1, div.style1
|
|
||||||
{margin-right:0cm;
|
|
||||||
margin-left:0cm;
|
|
||||||
font-size:9.0pt;
|
|
||||||
font-family:"Bernard MT Condensed";}
|
|
||||||
@page Section1
|
|
||||||
{size:595.3pt 841.9pt;
|
|
||||||
margin:70.85pt 70.85pt 70.85pt 70.85pt;}
|
|
||||||
div.Section1
|
|
||||||
{page:Section1;}
|
|
||||||
-->
|
|
||||||
</style>
|
|
||||||
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body lang=FR link=blue vlink=purple>
|
|
||||||
|
|
||||||
<div class=Section1>
|
|
||||||
|
|
||||||
<p class=style1 align=center style='text-align:center'><b><i><span
|
|
||||||
style='font-size:10.0pt;font-family:Arial;color:#333399'>Merci de votre
|
|
||||||
commande</span></i></b></p>
|
|
||||||
|
|
||||||
<p class=style1 align=center style='text-align:center'><b><i><span
|
|
||||||
style='font-size:10.0pt;font-family:Arial;color:#333399'>Celle-ci sera traitée
|
|
||||||
dans les meilleurs délais</span></i></b></p>
|
|
||||||
|
|
||||||
<p class=style1 align=center style='text-align:center'><b><i><span
|
|
||||||
style='font-size:10.0pt;font-family:Arial;color:#333399'>Cordialement,</span></i></b></p>
|
|
||||||
|
|
||||||
<p align=center style='text-align:center'><img width=200 height=50
|
|
||||||
src="Boutique.fr/Images/votre_logo.jpg" alt="Votre Enseigne"></p>
|
|
||||||
|
|
||||||
<p align=center style='margin-top:0cm;margin-right:0cm;margin-bottom:6.0pt;
|
|
||||||
margin-left:0cm;text-align:center'><b><span style='font-size:8.0pt;font-family:
|
|
||||||
Arial;color:#FF9900'>Gardez les références de votre commande et n'hésitez pas à
|
|
||||||
nous contacter si vous avez des questions :</span></b></p>
|
|
||||||
|
|
||||||
<p align=center style='margin:0cm;margin-bottom:.0001pt;text-align:center'><b><span
|
|
||||||
style='font-size:8.0pt;font-family:Arial;color:#FF9900'>tel : 00 00 00 00 00</span></b></p>
|
|
||||||
|
|
||||||
<p align=center style='margin:0cm;margin-bottom:.0001pt;text-align:center'><b><span
|
|
||||||
style='font-size:8.0pt;font-family:Arial;color:#FF9900'>courriel : <a
|
|
||||||
href="mailto:contact@maboutique.fr"><span style='color:#FF9900;text-decoration:
|
|
||||||
none'>contact@maboutique.fr</span></a></span></b></p>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
@ -1,6 +0,0 @@
|
|||||||
-----BEGIN PUBLIC KEY-----
|
|
||||||
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDe+hkicNP7ROHUssGNtHwiT2Ew
|
|
||||||
HFrSk/qwrcq8v5metRtTTFPE/nmzSkRnTs3GMpi57rBdxBBJW5W9cpNyGUh0jNXc
|
|
||||||
VrOSClpD5Ri2hER/GcNrxVRP7RlWOqB1C03q4QYmwjHZ+zlM4OUhCCAtSWflB4wC
|
|
||||||
Ka1g88CjFwRw/PB9kwIDAQAB
|
|
||||||
-----END PUBLIC KEY-----
|
|
@ -1,11 +0,0 @@
|
|||||||
<?php
|
|
||||||
$montant=$_GET['montant'];
|
|
||||||
$ref_com=$_GET['ref'];
|
|
||||||
#$auto=$_GET['auto'];
|
|
||||||
$trans=$_GET['trans'];
|
|
||||||
print ("<center><b><h2>Votre transaction a été refusée</h2></center></b><br>");
|
|
||||||
print ("<br><b>MONTANT : </b>$montant\n");
|
|
||||||
print ("<br><b>REFERENCE : </b>$ref_com\n");
|
|
||||||
#print ("<br><b>AUTO : </b>$auto\n");
|
|
||||||
print ("<br><b>TRANS : </b>$trans\n");
|
|
||||||
?>
|
|
@ -1,102 +0,0 @@
|
|||||||
|
|
||||||
///// script PHP de vérification de la signature Paybox.
|
|
||||||
///// Ce code peut s'executer dans un contexte Apache/PHP.
|
|
||||||
///// Il affiche alors une page web qui permet de vérifier et signer des données.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>formulaire d'exemple pour test signature</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<?php
|
|
||||||
|
|
||||||
$status = "GUY";
|
|
||||||
|
|
||||||
function LoadKey( $keyfile, $pub=true, $pass='' ) { // chargement de la clé (publique par défaut)
|
|
||||||
|
|
||||||
$fp = $filedata = $key = FALSE; // initialisation variables
|
|
||||||
$fsize = filesize( $keyfile ); // taille du fichier
|
|
||||||
if( !$fsize ) return FALSE; // si erreur on quitte de suite
|
|
||||||
$fp = fopen( $keyfile, 'r' ); // ouverture fichier
|
|
||||||
if( !$fp ) return FALSE; // si erreur ouverture on quitte
|
|
||||||
$filedata = fread( $fp, $fsize ); // lecture contenu fichier
|
|
||||||
fclose( $fp ); // fermeture fichier
|
|
||||||
if( !$filedata ) return FALSE; // si erreur lecture, on quitte
|
|
||||||
if( $pub )
|
|
||||||
$key = openssl_pkey_get_public( $filedata ); // recuperation de la cle publique
|
|
||||||
else // ou recuperation de la cle privee
|
|
||||||
$key = openssl_pkey_get_private( array( $filedata, $pass ));
|
|
||||||
return $key; // renvoi cle ( ou erreur )
|
|
||||||
}
|
|
||||||
|
|
||||||
// comme precise la documentation Paybox, la signature doit être
|
|
||||||
// obligatoirement en dernière position pour que cela fonctionne
|
|
||||||
|
|
||||||
function GetSignedData( $qrystr, &$data, &$sig ) { // renvoi les donnes signees et la signature
|
|
||||||
|
|
||||||
$pos = strrpos( $qrystr, '&' ); // cherche dernier separateur
|
|
||||||
$data = substr( $qrystr, 0, $pos ); // et voila les donnees signees
|
|
||||||
$pos= strpos( $qrystr, '=', $pos ) + 1; // cherche debut valeur signature
|
|
||||||
$sig = substr( $qrystr, $pos ); // et voila la signature
|
|
||||||
$sig = base64_decode( urldecode( $sig )); // decodage signature
|
|
||||||
}
|
|
||||||
|
|
||||||
// $querystring = chaine entière retournée par Paybox lors du retour au site (méthode GET)
|
|
||||||
// $keyfile = chemin d'accès complet au fichier de la clé publique Paybox
|
|
||||||
|
|
||||||
function PbxVerSign( $qrystr, $keyfile ) { // verification signature Paybox
|
|
||||||
|
|
||||||
$key = LoadKey( $keyfile ); // chargement de la cle
|
|
||||||
if( !$key ) return -1; // si erreur chargement cle
|
|
||||||
// penser à openssl_error_string() pour diagnostic openssl si erreur
|
|
||||||
GetSignedData( $qrystr, $data, $sig ); // separation et recuperation signature et donnees
|
|
||||||
return openssl_verify( $data, $sig, $key ); // verification : 1 si valide, 0 si invalide, -1 si erreur
|
|
||||||
}
|
|
||||||
|
|
||||||
if( !isset( $_POST['data'] )) // pour alimentation par defaut quand premier affichage du formulaire
|
|
||||||
$_POST['data'] = 'arg1=aaaa&arg2=bbbb&arg3=cccc&arg4=dddd';
|
|
||||||
|
|
||||||
if( isset( $_POST['signer']) ) { // si on a demande la signature
|
|
||||||
|
|
||||||
$key = LoadKey( 'TestK004.prv.pem', false ); // chargement de la cle prive (de test, sans mot de passe)
|
|
||||||
if( $key ) {
|
|
||||||
openssl_sign( $_POST['data'], $signature, $key ); // generation de la signature
|
|
||||||
openssl_free_key( $key ); // liberation ressource (confidentialite cle prive)
|
|
||||||
$status = "OK";
|
|
||||||
}
|
|
||||||
else $status = openssl_error_string(); // diagnostic erreur
|
|
||||||
|
|
||||||
$_POST['signeddata'] = $_POST['data']; // construction chaine data + signature
|
|
||||||
$_POST['signeddata'] .= '&sig=';
|
|
||||||
$_POST['signeddata'] .= urlencode( base64_encode( $signature ));
|
|
||||||
}
|
|
||||||
if( isset( $_POST['verifier']) ) { // si on a demande la verification
|
|
||||||
|
|
||||||
$CheckSig = PbxVerSign( $_POST['signeddata'], 'TestK004.pub.pem' );
|
|
||||||
|
|
||||||
if( $CheckSig == 1 ) $status = "Signature valide";
|
|
||||||
else if( $CheckSig == 0 ) $status = "Signature invalide : donnees alterees ou signature falsifiee";
|
|
||||||
else $status = "Erreur lors de la vérification de la signature";
|
|
||||||
}
|
|
||||||
|
|
||||||
?>
|
|
||||||
<form action="testsign.php" method="POST">
|
|
||||||
<table border="0" cellpadding="3" cellspacing="0" align="center">
|
|
||||||
<tr>
|
|
||||||
<td>status = <?php echo $status; ?></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><input type="text" name="data" size="80"value="<?= $_POST['data'] ?>"></td>
|
|
||||||
<td><input type="submit" name="signer" value="signer"/></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><input type="text" name="signeddata" size="80"value="<?= $_POST['signeddata'] ?>"></td>
|
|
||||||
<td><input type="submit" name="verifier" value="verifier"/></td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</form>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user