Skip to content

Commit af60407

Browse files
authored
Merge pull request #73 from luftdaten-at/71-oranization-management
71 oranization management
2 parents aad6bb9 + dabc8e7 commit af60407

14 files changed

+361
-25
lines changed

app/accounts/urls.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1-
from django.urls import path
1+
from django.urls import path, include
2+
3+
#from .views import SignupPageView
4+
from accounts.views import SignupPageView
25

3-
from .views import SignupPageView
46

57
urlpatterns = [
68
path("signup/", SignupPageView.as_view(), name="signup"),
9+
path("", include("allauth.urls")),
710
]

app/accounts/views.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,23 @@
1+
import datetime
12
from django.urls import reverse_lazy
23
from django.views import generic
34

45
from .forms import CustomUserCreationForm
6+
from campaign.models import OrganizationInvitation
57

68

79
class SignupPageView(generic.CreateView):
810
form_class = CustomUserCreationForm
9-
success_url = reverse_lazy("login")
10-
template_name = "registration/signup.html"
11+
success_url = reverse_lazy("account_login")
12+
template_name = "account/signup.html"
13+
14+
def form_valid(self, form):
15+
user = form.instance
16+
invitations = OrganizationInvitation.objects.filter(email=user.email).all()
17+
time_now = datetime.datetime.now(datetime.timezone.utc)
18+
user.save()
19+
for invitation in invitations:
20+
if not invitation.expiring_date or time_now < invitation.expiring_date:
21+
invitation.organization.users.add(user)
22+
invitation.delete()
23+
return super().form_valid(form)

app/campaign/forms.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,3 +91,12 @@ def __init__(self, *args, **kwargs):
9191
# Initialize form helper
9292
self.helper = FormHelper(self)
9393
self.helper.add_input(Submit('submit', 'Save'))
94+
95+
96+
class OrganizationForm(forms.ModelForm):
97+
class Meta:
98+
model = Organization
99+
fields = ['name', 'description']
100+
widgets = {
101+
'description': forms.Textarea(attrs={'rows': 3}),
102+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Generated by Django 5.1.2 on 2024-12-18 09:32
2+
3+
import django.db.models.deletion
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
('campaign', '0006_alter_campaign_users_alter_organization_users'),
11+
]
12+
13+
operations = [
14+
migrations.CreateModel(
15+
name='OrganizationInvitation',
16+
fields=[
17+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
18+
('expiring_date', models.DateField()),
19+
('email', models.EmailField(max_length=254)),
20+
('organization', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='invitations', to='campaign.organization')),
21+
],
22+
options={
23+
'unique_together': {('email', 'organization')},
24+
},
25+
),
26+
]
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 5.1.2 on 2024-12-18 10:06
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('campaign', '0007_organizationinvitation'),
10+
]
11+
12+
operations = [
13+
migrations.AlterField(
14+
model_name='organizationinvitation',
15+
name='expiring_date',
16+
field=models.DateField(null=True),
17+
),
18+
]

app/campaign/models.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,24 @@ class Organization(models.Model):
6363

6464
def __str__(self):
6565
return self.name
66+
67+
68+
class OrganizationInvitation(models.Model):
69+
"""
70+
if email e gets invited to a campaign
71+
but there is no usesr with email e
72+
an invitation is created.
73+
74+
as soon as users u with email e registers
75+
user u gets added to all organizations where there
76+
exists an invitation. -> invitations get deleted
77+
"""
78+
expiring_date = models.DateField(null=True)
79+
email = models.EmailField()
80+
organization = models.ForeignKey('Organization', on_delete=models.CASCADE, related_name='invitations')
81+
82+
class Meta:
83+
unique_together = ('email', 'organization')
84+
85+
def __str__(self):
86+
return f'{self.email} {self.organization.name} {self.expiring_date}'

app/campaign/urls.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from django.urls import path
2-
from .views import CampaignsHomeView, CampaignsMyView, CampaignsCreateView, CampaignsDetailView, CampaignsUpdateView, CampaignsDeleteView, RoomDetailView, CampaignAddUserView, RoomDeleteView, RoomCreateView
2+
from .views import *
33

44
urlpatterns = [
55
path('', CampaignsHomeView.as_view(), name='campaigns-home'),
@@ -13,4 +13,9 @@
1313
path('campaigns/<int:pk>/add-user/', CampaignAddUserView.as_view(), name='campaign-add-user'),
1414
path('rooms/<int:pk>/delete/', RoomDeleteView.as_view(), name='room-delete'),
1515
path('room/create/<int:campaign_pk>/', RoomCreateView.as_view(), name='room-create'),
16+
path('organizations/my', OrganizationsView.as_view(), name='organizations-my'),
17+
path('organization/create', OrganizationCreateView.as_view(), name='organization-create'),
18+
path('organizations/<int:pk>', OrganizationDetailView.as_view(), name='organization-detail'),
19+
path('organizations/<int:org_id>/remove-user/<int:user_id>', remove_user_from_organization, name='remove-user-from-organization'),
20+
path('organizations/<int:org_id>/invite-user', invite_user_to_organization, name='invite-user-to-organization'),
1621
]

app/campaign/views.py

Lines changed: 99 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,15 @@
44
from django.views.generic.list import ListView
55
from django.contrib.auth.mixins import LoginRequiredMixin
66
from django.urls import reverse_lazy, reverse
7+
from django.contrib.auth.decorators import login_required
8+
from django.shortcuts import get_object_or_404, redirect
9+
from django.contrib import messages
10+
from django.core.mail import send_mail
711

8-
from .models import Campaign, Room
9-
from .forms import CampaignForm, CampaignUserForm
12+
from main import settings
13+
from .models import Campaign, Room, Organization, OrganizationInvitation
14+
from .forms import CampaignForm, CampaignUserForm, OrganizationForm
15+
from accounts.models import CustomUser
1016

1117

1218
class CampaignsHomeView(ListView):
@@ -159,20 +165,98 @@ def form_valid(self, form):
159165
def get_success_url(self):
160166
return reverse_lazy('campaigns-detail', kwargs={'pk': self.campaign_pk})
161167

162-
163-
'''
164-
class CampaignsCreateView(CreateView):
165-
model = Campaign
166-
form_class = CampaignForm
167-
template_name = 'campaigns/form.html'
168-
success_url = reverse_lazy('campaigns-my') # Redirect after a successful creation
169168

170-
def get_initial(self):
171-
initial = super().get_initial()
172-
initial['user'] = self.request.user # Pass the logged-in user to the form's initial data
173-
return initial
169+
class OrganizationsView(LoginRequiredMixin, ListView):
170+
model = Organization
171+
template_name = 'campaigns/my_organizations.html'
172+
context_object_name = 'owned_organizations'
173+
174+
def get_queryset(self):
175+
# Return organizations where the user is the owner
176+
return Organization.objects.filter(owner=self.request.user)
177+
178+
def get_context_data(self, **kwargs):
179+
context = super().get_context_data(**kwargs)
180+
# Add organizations where the user is a member
181+
context['member_organizations'] = Organization.objects.filter(users=self.request.user)
182+
return context
183+
184+
185+
class OrganizationCreateView(LoginRequiredMixin, CreateView):
186+
model = Organization
187+
form_class = OrganizationForm
188+
template_name = 'campaigns/create_organization.html'
189+
success_url = reverse_lazy('organizations-my') # Redirect to a list view or another page
174190

175191
def form_valid(self, form):
176-
form.instance.owner = self.request.user # Set the owner to the current user
192+
organization = form.save(commit=False)
193+
organization.owner = self.request.user # Set the current user as the owner
194+
organization.save()
195+
organization.users.add(self.request.user)
196+
form.save_m2m() # Save the many-to-many relationships
177197
return super().form_valid(form)
178-
'''
198+
199+
200+
class OrganizationDetailView(DetailView):
201+
model = Organization
202+
template_name = 'campaigns/organization_detail.html'
203+
context_object_name = 'organization'
204+
205+
def get_success_url(self):
206+
# and 'self.object.pk' with the primary key of the newly created object
207+
return reverse_lazy('organizations-my', kwargs={'pk': self.object.pk})
208+
209+
210+
@login_required
211+
def remove_user_from_organization(request, org_id, user_id):
212+
organization = get_object_or_404(Organization, id=org_id)
213+
user = get_object_or_404(CustomUser, id=user_id)
214+
215+
# Ensure the user performing the action has permission
216+
if request.user != organization.owner:
217+
messages.error(request, "You do not have permission to remove users from this organization.")
218+
return redirect('organization-detail', pk=org_id)
219+
220+
organization.users.remove(user)
221+
messages.success(request, f"User {user.username} has been removed.")
222+
return redirect('organization-detail', pk=org_id)
223+
224+
225+
@login_required
226+
def invite_user_to_organization(request, org_id):
227+
if request.method != 'POST':
228+
return redirect(f"organization-detail", pk=org_id)
229+
230+
organization = get_object_or_404(Organization, id=org_id)
231+
email = request.POST.get('email')
232+
233+
if request.user != organization.owner:
234+
messages.error(request, "You do not have permission to invite users to this organization.")
235+
return redirect(f"organization-detail", pk=org_id)
236+
237+
user = CustomUser.objects.filter(email = email).first()
238+
239+
if user:
240+
organization.users.add(user)
241+
else:
242+
# check if invitation already exists
243+
invitation = OrganizationInvitation.objects.filter(email=email, organization__pk = organization.pk).first()
244+
if invitation == None:
245+
# create invitation
246+
invitation = OrganizationInvitation(
247+
expiring_date = None,
248+
email = email,
249+
organization = organization,
250+
)
251+
invitation.save()
252+
# send invitation email
253+
# TODO add link to register
254+
send_mail(
255+
subject=f"You've been invited to join {organization.name}",
256+
message=f"Visit this link to register and join {organization.name}: <registration_link>",
257+
from_email=settings.EMAIL_HOST_USER,
258+
recipient_list=[email],
259+
)
260+
messages.success(request, f"An invitation has been sent to {email}.")
261+
262+
return redirect(f"organization-detail", pk=org_id)

app/main/settings.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -146,11 +146,10 @@
146146

147147
from django.utils.translation import gettext_lazy as _
148148

149-
LANGUAGE_CODE = 'en-us' # Default language
149+
LANGUAGE_CODE = 'en' # Default language
150150

151151
LANGUAGES = [
152-
('en', _('English')),
153-
('de', _('German')),
152+
('en', 'English'),
154153
# Add more languages here
155154
]
156155

@@ -189,7 +188,14 @@
189188
"django.contrib.auth.backends.ModelBackend",
190189
"allauth.account.auth_backends.AuthenticationBackend",
191190
)
191+
192192
EMAIL_BACKEND = env.str("DJANGO_EMAIL_BACKEND", default="django.core.mail.backends.console.EmailBackend")
193+
EMAIL_HOST=env.str('EMAIL_HOST')
194+
EMAIL_HOST_USER=env.str('EMAIL_HOST_USER')
195+
EMAIL_HOST_PASSWORD=env.str('EMAIL_HOST_PASSWORD')
196+
EMAIL_PORT=env.str('EMAIL_PORT')
197+
EMAIL_USE_TLS=env.str('EMAIL_USE_TLS')
198+
193199
ACCOUNT_SESSION_REMEMBER = True
194200
ACCOUNT_SIGNUP_PASSWORD_ENTER_TWICE = False
195201

app/main/urls.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
# Django admin
2424
path("backend/", admin.site.urls),
2525
# User management
26-
path("accounts/", include("allauth.urls")),
26+
path("accounts/", include("accounts.urls")),
2727
# Local apps
2828
path("", include("pages.urls")),
2929
path("api/", include("api.urls")),
@@ -36,4 +36,4 @@
3636
# Your URLs that require localization
3737
# Include set_language URL
3838
path('i18n/', include('django.conf.urls.i18n')),
39-
)
39+
)

app/templates/_base.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
{{ user.username }}
4141
</a>
4242
<ul class="dropdown-menu dropdown-menu-end">
43+
<li><a class="dropdown-item" href="{% url 'organizations-my' %}">{% trans "My orgnizations" %}</a></li>
4344
<li><a class="dropdown-item" href="{% url 'campaigns-my' %}">{% trans "My campaigns" %}</a></li>
4445
<li><a class="dropdown-item" href="{% url 'devices-my' %}">{% trans "My devices" %}</a></li>
4546
{% if host != "arbeitsplatz.luftdaten.at" %}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{% extends '_base.html' %}
2+
{% load i18n %}
3+
{% load static %}
4+
{% load crispy_forms_tags %}
5+
6+
{% block styles %}
7+
{% endblock styles %}
8+
9+
{% block content %}
10+
<h2>{% trans "Create Organization" %}</h2>
11+
<form method="post">
12+
{% csrf_token %}
13+
{{ form|crispy }}
14+
<button class="btn btn-success" type="submit">{% trans "Save" %}</button>
15+
</form>
16+
{% endblock %}

0 commit comments

Comments
 (0)