Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 9 additions & 8 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ FROM python:3.12-slim as common-base

#ENV DJANGO_SETTINGS_MODULE foo.settings
ENV UID=2008
ENV PATH="/opt/bitpoll/.venv/bin:$PATH"

RUN usermod -u $UID -g nogroup -d /opt/bitpoll www-data

Expand All @@ -19,11 +20,12 @@ RUN pip install -U pip setuptools

FROM base-builder as dependencies

RUN apt-get update && apt-get -y --no-install-recommends install g++ wget python3-pip make gettext gcc python3-dev libldap2-dev gpg gpg-agent curl libsasl2-dev npm
RUN apt-get update && apt-get -y --no-install-recommends install g++ wget python3-pip make gettext gcc python3-dev libldap2-dev gpg gpg-agent curl libsasl2-dev npm && \
pip install --no-cache-dir uv

COPY requirements-production.txt .
COPY pyproject.toml uv.lock README.md .

RUN URLLIB3_NO_OVERRIDE=1 pip install --no-warn-script-location --prefix=/install -U --no-binary urllib3-future -r requirements-production.txt
RUN uv sync --locked --no-dev --extra production --no-install-project

FROM dependencies as collect-static

Expand All @@ -35,17 +37,16 @@ COPY bitpoll bitpoll
COPY locale locale
COPY docker_files/config/settings.py bitpoll/settings_local.py

# Set Pythonpath tro the packages installed with pip bevore so they are aviable in this step
RUN export PYTHONPATH=/install/lib/python$(python3 --version | cut -d ' ' -f 2 | cut -d '.' -f 1,2)/site-packages && \
python3 /opt/bitpoll/manage.py collectstatic --noinput && \
python3 manage.py compilemessages &&\
# Dependencies are installed in /opt/bitpoll/.venv by uv in the previous stage.
RUN uv run --locked /opt/bitpoll/manage.py collectstatic --noinput && \
uv run --locked manage.py compilemessages &&\
rm bitpoll/settings_local.py

FROM common-base

#RUN apt-get -y --no-install-recommends install python3-psycopg2 python3-ldap3 gettext

COPY --from=dependencies /install /usr/local
COPY --from=dependencies /opt/bitpoll/.venv /opt/bitpoll/.venv
COPY --from=collect-static /opt/bitpoll .

COPY docker_files/run /usr/local/bin
Expand Down
33 changes: 20 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,10 @@ Get the code:
git clone https://github.com/fsinfuhh/Bitpoll
```

Generate a Python virtualenv and install dependencies:
Install Python and development dependencies with uv:

```bash
virtualenv -p $(which python3) .pyenv
source .pyenv/bin/activate
pip install -r requirements.txt
uv sync --group dev
```

Copy `bitpoll/settings_local.sample.py` to `bitpoll/settings_local.py` and customize the local settings.
Expand All @@ -94,9 +92,15 @@ Run Testserver:
./manage.py runserver
```

Run the test suite:

```bash
uv run pytest
```

### Production

In production Senty is used for error reporting.
In production Sentry is used for error reporting.
django-auth-ldap is used vor login via ldap
uwsgi to serve the app

Expand All @@ -109,7 +113,7 @@ sudo apt install g++ make python3-psycopg2 python3-ldap3 gettext gcc python3-dev
Install Python Dependencies

```bash
pip install -r requirements-production.txt
uv sync --extra production
```

Configure examples are in `settings_local.py`
Expand All @@ -126,18 +130,21 @@ For Production systems it is nessesarry to run

## Management of Dependencies

We use pip-tools to manage the dependencies.
After modification or the requirements*.in files or for updates of packages run
We use uv to manage and lock the dependencies. Runtime dependencies are in
`pyproject.toml`; production-only dependencies are in the `production` extra
and test tooling is in the `dev` dependency group.

After changing dependencies or for package updates, run:

```bash
pip-compile --upgrade --output-file requirements.txt requirements.in
pip-compile --upgrade --output-file requirements-production.txt requirements-production.in requirements.in
uv lock --upgrade
```

to sync your enviroment with the requirements.txt just run
Synchronize the local environment with the lockfile using:

```bash
pip-sync
uv sync --group dev
```

this will install/deinstall dependencies so that the virtualenv is matching the requirements file
For a production environment use `uv sync --extra production`. The committed
`uv.lock` makes both environments reproducible.
6 changes: 1 addition & 5 deletions bitpoll/base/templatetags/settings_value.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
from django import template
from django.conf import settings

from django.template import TemplateSyntaxError, Variable, Node, Variable, Library

register = template.Library()
from django.template import TemplateSyntaxError, Variable, Node, Library

register = Library()

Expand Down
Empty file added bitpoll/base/tests/__init__.py
Empty file.
56 changes: 56 additions & 0 deletions bitpoll/base/tests/test_post.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import pytest
from model_bakery import baker
from django.forms.models import model_to_dict
from django.urls import reverse

from bitpoll.base.models import BitpollUser
from bitpoll.poll.models import ChoiceValue, Poll, PollWatch, Vote


pytestmark = pytest.mark.django_db


def test_poll_creation_post_creates_poll_and_values(client):
user = baker.make("base.BitpollUser", _fill_optional=True, username="poll-creator")
client.force_login(user)
data = {
"title": "Created poll",
"type": "universal",
"public_listening": "",
"due_date": "",
"url": "created-poll",
"description": "Created through POST",
"anonymous_allowed": "on",
"require_login": "",
"require_invitation": "",
"allow_unauthenticated_vote_changes": "on",
"one_vote_per_user": "on",
"vote_all": "",
}

response = client.post(reverse("index"), data)

assert response.status_code == 302
poll = Poll.objects.get(url="created-poll")
assert poll.user == user
assert ChoiceValue.objects.filter(poll=poll).count() == 4


def test_user_settings_post_updates_user_and_watch(client):
user = baker.make("base.BitpollUser", _fill_optional=True, username="settings-user")
poll = baker.make(Poll, _fill_optional=True, user=user, url="settings-poll", due_date=None)
baker.make(Vote, _fill_optional=True, poll=poll, user=user)
client.force_login(user)
data = model_to_dict(user, fields=["auto_watch", "email_invitation", "timezone", "language"])
data.update({"auto_watch": True, "email_invitation": False, "timezone": "UTC", "language": "english"})

response = client.post(reverse("settings"), data)

assert response.status_code == 200
user.refresh_from_db()
assert user.auto_watch is True
assert user.timezone == "UTC"
assert PollWatch.objects.filter(poll=poll, user=user).exists()



29 changes: 29 additions & 0 deletions bitpoll/base/tests/test_simple_get.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import pytest

from bitpoll.tests._base import get_dynamic_url, get_module_urls


pytestmark = pytest.mark.django_db

URLS = get_module_urls("base")


@pytest.mark.parametrize("url_name,arg_count", URLS)
def test_urls_without_login(client, url_name, arg_count):
response = client.get(get_dynamic_url([], url_name))
match url_name:
case "settings" | "base_autocomplete":
assert response.status_code == 302
case _:
assert response.status_code == 200


@pytest.mark.django_db
@pytest.mark.parametrize("url_name,arg_count", URLS)
def test_urls_with_login(client, django_user_model, url_name, arg_count):
user = django_user_model.objects.create_user(username="base-user", password="password")
client.force_login(user)
response = client.get(get_dynamic_url([], url_name))
assert response.status_code == 200


Empty file.
65 changes: 65 additions & 0 deletions bitpoll/caldav/tests/test_post.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import pytest
from model_bakery import baker
from django.urls import reverse

from bitpoll.caldav.models import DavCalendar


pytestmark = pytest.mark.django_db


@pytest.fixture
def calendar_data(settings):
settings.FIELD_ENCRYPTION_KEY = "this+is+an+example+key+please+generate+one+="
user = baker.make("base.BitpollUser", _fill_optional=True, username="caldav-user")
calendar = baker.make(
DavCalendar,
_fill_optional=True,
user=user,
url="https://calendar.example/",
name="Test calendar",
)
return user, calendar


def test_delete_calendar_post_deletes_calendar(client, calendar_data):
user, calendar = calendar_data
client.force_login(user)

response = client.post(
reverse("change_calendar"),
{"delete": str(calendar.pk)},
)

assert response.status_code == 302
assert not DavCalendar.objects.filter(pk=calendar.pk).exists()


def test_create_calendar_post_saves_calendar(client, calendar_data, monkeypatch):
user, _ = calendar_data
client.force_login(user)

class FakeCalendar:
def __init__(self, **kwargs):
pass

def date_search(self, *args):
return []

monkeypatch.setattr("bitpoll.caldav.views.Calendar", FakeCalendar)
monkeypatch.setattr("bitpoll.caldav.views.DAVClient", lambda url: object())

response = client.post(
reverse("change_calendar"),
{
"url_0": "https://new-calendar.example/",
"url_1": "",
"url_2": "",
"name": "New calendar",
},
)

assert response.status_code == 302
assert DavCalendar.objects.filter(user=user, name="New calendar").exists()


45 changes: 45 additions & 0 deletions bitpoll/caldav/tests/test_simple_get.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import pytest

from bitpoll.caldav.models import DavCalendar
from bitpoll.tests._base import get_dynamic_url, get_module_urls


URLS = get_module_urls("caldav")


@pytest.fixture
def calendar_data(db, django_user_model, settings):
settings.FIELD_ENCRYPTION_KEY = "this+is+an+example+key+please+generate+one+="
user = django_user_model.objects.create_user(username="caldav-user", password="password")
calendar = DavCalendar.objects.create(user=user, url="https://calendar.example/", name="Test calendar")
return user, calendar


def url_args(url_name, calendar):
return [calendar.pk] if "edit" in url_name else []


@pytest.mark.parametrize("url_name,arg_count", URLS)
def test_urls_without_login(client, calendar_data, url_name, arg_count):
_, calendar = calendar_data
response = client.get(get_dynamic_url(url_args(url_name, calendar), url_name))
match url_name:
case "change_calendar" | "edit_save_calendar":
assert response.status_code == 405
case "edit_calendar":
assert response.status_code == 302
case _:
assert response.status_code == 200


@pytest.mark.parametrize("url_name,arg_count", URLS)
def test_urls_with_login(client, calendar_data, url_name, arg_count):
user, calendar = calendar_data
client.force_login(user)
response = client.get(get_dynamic_url(url_args(url_name, calendar), url_name))
match url_name:
case "change_calendar" | "edit_save_calendar":
assert response.status_code == 405
case _:
assert response.status_code == 200

2 changes: 1 addition & 1 deletion bitpoll/caldav/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ def get_caldav(choices: List[Choice], current_poll: Poll, user: BitpollUser, req
pass
cache.set(cache_key, events_calendar)
except AuthorizationError as e:
messages.warning(request, ugettext_lazy('Could not access your calendar "%s" due to an authorization error' % calendar_obj.name))
messages.warning(request, gettext_lazy('Could not access your calendar "%s" due to an authorization error' % calendar_obj.name))
events += events_calendar
for choice in choices:
ev_tmp = []
Expand Down
3 changes: 2 additions & 1 deletion bitpoll/groups/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,5 @@ def get_invitations(self):
return invitations

def save(self):
self.get_invitation().save()
for invitation in self.get_invitations():
invitation.save()
2 changes: 1 addition & 1 deletion bitpoll/groups/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ class GroupInvitation(models.Model):
invited_by = models.ForeignKey(settings.AUTH_USER_MODEL,
related_name='given_invitations', on_delete=models.CASCADE)

def __unicode__(self):
def __str__(self):
return u'Invitation to {0} for {1}'.format(self.group, self.invitee)

def accept(self):
Expand Down
Empty file.
Loading