diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..03a268b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,34 @@ +# Include any files or directories that you don't want to be copied to your +# container here (e.g., local build artifacts, temporary files, etc.). +# +# For more help, visit the .dockerignore file reference guide at +# https://docs.docker.com/go/build-context-dockerignore/ + +**/.DS_Store +**/__pycache__ +**/.venv +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/bin +**/charts +**/docker-compose* +**/compose.y*ml +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md diff --git a/.gitignore b/.gitignore index 8dd938b..9dcf8fd 100644 --- a/.gitignore +++ b/.gitignore @@ -55,7 +55,7 @@ cover/ *.mo *.pot - +*.__pycache__/ # Flask stuff: instance/ .webassets-cache diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..5a394b3 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,14 @@ +FROM python:3.12-slim + +ENV PYTHONDONTWRITEBYTECODE = 1 +ENV PYTHONUNBUFFERED = 1 +RUN apt-get update && apt-get install -y gcc python3-dev libpq-dev + + +WORKDIR /app +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt +COPY . . + + +CMD ["gunicorn", "--bind", "0.0.0:8000", "--workers", "4", "main.wsgi:application"] \ No newline at end of file diff --git a/Dockerfile.Celery b/Dockerfile.Celery new file mode 100644 index 0000000..fcb6668 --- /dev/null +++ b/Dockerfile.Celery @@ -0,0 +1,51 @@ +# # syntax=docker/dockerfile:1 + +# # Comments are provided throughout this file to help you get started. +# # If you need more help, visit the Dockerfile reference guide at +# # https://docs.docker.com/go/dockerfile-reference/ + +# # Want to help us make this template better? Share your feedback here: https://forms.gle/ybq9Krt8jtBL3iCk7 + +# ARG PYTHON_VERSION=3.13.0 +# FROM python:${PYTHON_VERSION}-slim as base + +# # Prevents Python from writing pyc files. +# ENV PYTHONDONTWRITEBYTECODE=1 + +# # Keeps Python from buffering stdout and stderr to avoid situations where +# # the application crashes without emitting any logs due to buffering. +# ENV PYTHONUNBUFFERED=1 + +# WORKDIR /app + +# # Create a non-privileged user that the app will run under. +# # See https://docs.docker.com/go/dockerfile-user-best-practices/ +# ARG UID=10001 +# RUN adduser \ +# --disabled-password \ +# --gecos "" \ +# --home "/nonexistent" \ +# --shell "/sbin/nologin" \ +# --no-create-home \ +# --uid "${UID}" \ +# appuser + +# # Download dependencies as a separate step to take advantage of Docker's caching. +# # Leverage a cache mount to /root/.cache/pip to speed up subsequent builds. +# # Leverage a bind mount to requirements.txt to avoid having to copy them into +# # into this layer. +# RUN --mount=type=cache,target=/root/.cache/pip \ +# --mount=type=bind,source=requirements.txt,target=requirements.txt \ +# python -m pip install -r requirements.txt + +# # Switch to the non-privileged user to run the application. +# USER appuser + +# # Copy the source code into the container. +# COPY . . + +# # Expose the port that the application listens on. +# EXPOSE 8000 + +# # Run the application. +# CMD gunicorn 'enbv.Lib.site-packages.asgiref.wsgi' --bind=0.0.0.0:8000 diff --git a/README.Docker.md b/README.Docker.md new file mode 100644 index 0000000..6dae561 --- /dev/null +++ b/README.Docker.md @@ -0,0 +1,22 @@ +### Building and running your application + +When you're ready, start your application by running: +`docker compose up --build`. + +Your application will be available at http://localhost:8000. + +### Deploying your application to the cloud + +First, build your image, e.g.: `docker build -t myapp .`. +If your cloud uses a different CPU architecture than your development +machine (e.g., you are on a Mac M1 and your cloud provider is amd64), +you'll want to build the image for that platform, e.g.: +`docker build --platform=linux/amd64 -t myapp .`. + +Then, push it to your registry, e.g. `docker push myregistry.com/myapp`. + +Consult Docker's [getting started](https://docs.docker.com/go/get-started-sharing/) +docs for more detail on building and pushing. + +### References +* [Docker's Python guide](https://docs.docker.com/language/python/) \ No newline at end of file diff --git a/db.sqlite3 b/db.sqlite3 index dd01cae..9bfbaf5 100644 Binary files a/db.sqlite3 and b/db.sqlite3 differ diff --git a/event/__pycache__/models.cpython-313.pyc b/event/__pycache__/models.cpython-313.pyc index c168155..f4cc61d 100644 Binary files a/event/__pycache__/models.cpython-313.pyc and b/event/__pycache__/models.cpython-313.pyc differ diff --git a/event/__pycache__/permissions.cpython-313.pyc b/event/__pycache__/permissions.cpython-313.pyc index 565547c..0f11008 100644 Binary files a/event/__pycache__/permissions.cpython-313.pyc and b/event/__pycache__/permissions.cpython-313.pyc differ diff --git a/event/__pycache__/serializers.cpython-313.pyc b/event/__pycache__/serializers.cpython-313.pyc deleted file mode 100644 index 507ae71..0000000 Binary files a/event/__pycache__/serializers.cpython-313.pyc and /dev/null differ diff --git a/event/__pycache__/urls.cpython-313.pyc b/event/__pycache__/urls.cpython-313.pyc deleted file mode 100644 index e793523..0000000 Binary files a/event/__pycache__/urls.cpython-313.pyc and /dev/null differ diff --git a/event/__pycache__/views.cpython-313.pyc b/event/__pycache__/views.cpython-313.pyc deleted file mode 100644 index 51302c3..0000000 Binary files a/event/__pycache__/views.cpython-313.pyc and /dev/null differ diff --git a/event/permissions.py b/event/permissions.py index 1001cc7..9cb5c7f 100644 --- a/event/permissions.py +++ b/event/permissions.py @@ -10,4 +10,4 @@ def has_permission(self, request, view): def has_object_permission(self, request, view, obj): if request.method in permissions.SAFE_METHODS: return True - return request.user.profile==obj.creator \ No newline at end of file + return request.user==obj.creator \ No newline at end of file diff --git a/event/serializers.py b/event/serializers.py index 2a5f238..0d70e92 100644 --- a/event/serializers.py +++ b/event/serializers.py @@ -56,7 +56,7 @@ def validate(self, data): return data def create(self, validated_data): - """Create a new user and associate a profile.""" + email = validated_data.get("email") username = validated_data.get("username") phone_number = validated_data.pop("phone_number", None) @@ -108,14 +108,14 @@ def get_creator_phone(self, obj): return obj.creator.profile.phone_number except ObjectDoesNotExist: return None - + + def create(self, validated_data): """Associate the event with the creator.""" validated_data["creator"] = self.context["request"].user return super().create(validated_data) - class EventDetailSerializer(EventSerializer): """Serializer for detailed event view.""" @@ -144,8 +144,7 @@ def create(self, validated_data): class ReminderSerializer(serializers.ModelSerializer): - """Serializer for reminders.""" - + class Meta: model = Reminder fields = ["id", "sent_at", "message", "type"] diff --git a/event/tasks.py b/event/tasks.py index 3a1b328..dd019c0 100644 --- a/event/tasks.py +++ b/event/tasks.py @@ -7,42 +7,7 @@ from celery import shared_task from .models import Event, Reminder -@shared_task -def send_event_reminders(): - - tomorrow = timezone.now() + timedelta(days=1) - four_days = timezone.now() + timedelta(days=4) - - # Get events in the next 3 days - upcoming_events = Event.objects.filter( - date__gte=tomorrow, - date__lte=four_days - ) - - for event in upcoming_events: - days_until_event = (event.date - timezone.now()).days - - for attendee in event.attendees.all(): - - if Reminder.objects.filter( - attendee=attendee, - sent_at__date=timezone.now().date() - ).exists(): - continue - - - if days_until_event <= 1: - message = f"REMINDER: The event '{event.title}' is TOMORROW at {event.date.strftime('%H:%M')} in {event.location}." - else: - message = f"REMINDER: The event '{event.title}' is coming up in {days_until_event} days at {event.date.strftime('%H:%M')} in {event.location}." - - # Send email reminder - send_email_reminder(attendee, event, message) - - # Send WhatsApp reminder if phone number is available - if attendee.phone_number: - send_whatsapp_reminder(attendee, message) def send_email_reminder(attendee, event, message): """Send email reminder to an attendee""" @@ -79,4 +44,43 @@ def send_whatsapp_reminder(attendee, message): ) except Exception as e: - print(f"Failed to send WhatsApp reminder: {str(e)}") \ No newline at end of file + print(f"Failed to send WhatsApp reminder: {str(e)}") + + +@shared_task +def send_event_reminders(): + + + tomorrow = timezone.now() + timedelta(days=1) + four_days = timezone.now() + timedelta(days=4) + + # Get events in the next 3 days + upcoming_events = Event.objects.filter( + date__gte=tomorrow, + date__lte=four_days + ) + + for event in upcoming_events: + days_until_event = (event.date - timezone.now()).days + + for attendee in event.attendees.all(): + + if Reminder.objects.filter( + attendee=attendee, + sent_at__date=timezone.now().date() + ).exists(): + continue + + + if days_until_event <= 1: + message = f"REMINDER: The event '{event.title}' is TOMORROW at {event.date.strftime('%H:%M')} in {event.location}." + else: + message = f"REMINDER: The event '{event.title}' is coming up in {days_until_event} days at {event.date.strftime('%H:%M')} in {event.location}." + + # Send email reminder + send_email_reminder(attendee, event, message) + + # Send WhatsApp reminder if phone number is available + if attendee.phone_number: + send_whatsapp_reminder(attendee, message) + diff --git a/event/urls.py b/event/urls.py index 5abff12..91743d9 100644 --- a/event/urls.py +++ b/event/urls.py @@ -10,7 +10,7 @@ urlpatterns = [ path('', include(router.urls)), - path('google/login/', views.GoogleLoginView.as_view(), name='google-login'), + path('google/login/', views.google, name='google-login'), path('auth/',include('rest_framework.urls')), path('logout/', views.logout, name='user-logout'), path('register/', views.UserRegistrationView.as_view(), name='user-registration'), diff --git a/event/views.py b/event/views.py index d2854e2..893e99a 100644 --- a/event/views.py +++ b/event/views.py @@ -1,11 +1,14 @@ from rest_framework import viewsets, permissions, status, generics from rest_framework.decorators import api_view, permission_classes, action from rest_framework.response import Response +from django.template.loader import render_to_string +from django.utils.html import strip_tags from django.contrib.auth.models import User from rest_framework.authentication import TokenAuthentication from rest_framework.authtoken.models import Token from rest_framework.views import APIView import requests +from .permissions import IsEventCreator from django.core.mail import send_mail from rest_framework_simplejwt.tokens import RefreshToken from django.conf import settings @@ -50,7 +53,7 @@ def post(self, request, *args, **kwargs): class EventViewSet(viewsets.ModelViewSet): serializer_class = EventSerializer - permission_classes = [permissions.IsAuthenticated] + permission_classes = [IsEventCreator] authentication_classes = [TokenAuthentication] def get_queryset(self): @@ -76,8 +79,7 @@ def attendees(self, request, pk=None): class PublicEventListView(generics.ListAPIView): serializer_class = EventSerializer queryset = Event.objects.all() - - permission_classes = [permissions.AllowAny] + permission_classes = [permissions.IsAuthenticatedOrReadOnly] class AttendeeView(generics.ListAPIView): @@ -95,13 +97,11 @@ class EventRegistrationView(generics.GenericAPIView): serializer_class = AttendeeSerializer permission_classes = [permissions.AllowAny] - # def get(self, request): + def get(self, request, registration_link=None): - # # Find the event by matching the registration code in the registration_link - # event = get_object_or_404(Event) - # return Response({ - # 'event': EventSerializer(event).data - # }) + # Find the event by matching the registration code in the registration_link + event = get_object_or_404(Event, registration_link=registration_link) + return Response({"event": EventSerializer(event).data}) def post(self, request, registration_link=None): @@ -115,16 +115,24 @@ def post(self, request, registration_link=None): if serializer.is_valid(): serializer.save() - # Send confirmation email - message = f"Thank you for registering for {event.title}, We look forward to seeing you at the event" + context = { + "event_title": event.title, + "event_date": event.date, + "event_time": event.time, + "event_location": event.location, + "attendee_email": attendee, + } + html_message = render_to_string("emails/event_registration.html", context) + plain_message = strip_tags(html_message) try: send_mail( subject=f"You have Successfully registered for {event.title}", - message=message, + message=plain_message, from_email=settings.DEFAULT_FROM_EMAIL, recipient_list=[attendee], fail_silently=False, + html_message=html_message, ) # # Create a reminder record @@ -145,6 +153,7 @@ def post(self, request, registration_link=None): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) +@api_view(["POST"]) def logout(request): try: request.user.auth_token.delete() @@ -161,7 +170,7 @@ def logout(request): def google(request): code = request.GET.get("code") if code is None: - return Response({"message": "No code provided"}, status=400) + return Response({"message": "No code provided"}, status=400) PARAMS = { "client_id": os.environ.get("google_id") or "", @@ -171,18 +180,22 @@ def google(request): "grant_type": "authorization_code", } headers = {"Content-Type": "application/x-www-form-urlencoded"} - token_response = requests.post("https://oauth2.googleapis.com/token", data=PARAMS, headers=headers) + token_response = requests.post( + "https://oauth2.googleapis.com/token", data=PARAMS, headers=headers + ) if token_response.status_code != 200: - print(token_response.json()) - return Response({"message": "Failed to get access token", "error": token_response.json()}, status=400) + return Response( + {"message": "Failed to get access token", "error": token_response.json()}, + status=400, + ) token_data = token_response.json() access_token = token_data.get("access_token") user_info_response = requests.get( "https://www.googleapis.com/oauth2/v2/userinfo", - headers={"Authorization": f"Bearer {access_token}"} + headers={"Authorization": f"Bearer {access_token}"}, ) if user_info_response.status_code != 200: @@ -194,21 +207,24 @@ def google(request): if existing_user: token, created = Token.objects.get_or_create(user=existing_user) user_data = UserSerializer(existing_user).data - return Response({ - 'user': user_data, - 'token': token.key - }, status=200) + return Response({"user": user_data, "token": token.key}, status=200) # Creating a new user - create_user = SocialAccountSeralizer(data={"email": user_info["email"], "username": user_info["given_name"],"phone_number":user_info["phone_number"]}) + create_user = SocialAccountSeralizer( + data={ + "email": user_info["email"], + "username": user_info["given_name"], + "phone_number": user_info["phone_number"], + } + ) if create_user.is_valid(): user_instance = create_user.save() token, created = Token.objects.get_or_create(user=user_instance) - - return Response({ - 'user': create_user.data, - 'token': token.key - }, status=200) - return Response({"message":"An error occured, Pleasee try again later"},status=500) + return Response({"user": create_user.data, "token": token.key}, status=200) + + return Response( + {"message": "An error occured, Please try again later"}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR, + ) diff --git a/main/__init__.py b/main/__init__.py index f576d3e..cd04264 100644 --- a/main/__init__.py +++ b/main/__init__.py @@ -1,3 +1,3 @@ from .celery import app as celery_app -' __all__'== [celery_app] \ No newline at end of file +__all__ = ['celery_app'] diff --git a/main/__pycache__/__init__.cpython-313.pyc b/main/__pycache__/__init__.cpython-313.pyc index 3bd0e2d..6d40c96 100644 Binary files a/main/__pycache__/__init__.cpython-313.pyc and b/main/__pycache__/__init__.cpython-313.pyc differ diff --git a/main/__pycache__/settings.cpython-313.pyc b/main/__pycache__/settings.cpython-313.pyc deleted file mode 100644 index 6332bbd..0000000 Binary files a/main/__pycache__/settings.cpython-313.pyc and /dev/null differ diff --git a/main/__pycache__/urls.cpython-313.pyc b/main/__pycache__/urls.cpython-313.pyc deleted file mode 100644 index f50148b..0000000 Binary files a/main/__pycache__/urls.cpython-313.pyc and /dev/null differ diff --git a/main/settings.py b/main/settings.py index 85e8125..13f0803 100644 --- a/main/settings.py +++ b/main/settings.py @@ -1,14 +1,3 @@ -""" -Django settings for main project. - -Generated by 'django-admin startproject' using Django 5.1.7. - -For more information on this file, see -https://docs.djangoproject.com/en/5.1/topics/settings/ - -For the full list of settings and their values, see -https://docs.djangoproject.com/en/5.1/ref/settings/ -""" import os from rest_framework.authentication import TokenAuthentication @@ -59,6 +48,7 @@ 'API_SECRET': os.getenv('api_secret') } + SIMPLE_JWT = { "ACCESS_TOKEN_LIFETIME": timedelta(days=1), "REFRESH_TOKEN_LIFETIME":timedelta(days=2) @@ -91,7 +81,7 @@ TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], + 'DIRS': [os.path.join(BASE_DIR, 'templates')], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ @@ -170,7 +160,7 @@ EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' EMAIL_HOST = os.environ.get('EMAIL_HOST') EMAIL_PORT = 587 -DEFAULT_FROM_EMAIL= "Eventrio " +DEFAULT_FROM_EMAIL= "Eventrio" EMAIL_USE_TLS = True EMAIL_HOST_USER = os.getenv('EMAIL_HOST_USER') @@ -184,7 +174,7 @@ CELERY_RESULT_SERIALIZER = 'json' CELERY_TIMEZONE = 'UTC' -# Schedule for the reminder task to run daily + CELERY_BEAT_SCHEDULE = { 'send-event-reminders': { 'task': '.tasks.send_event_reminders', diff --git a/main/urls.py b/main/urls.py index 1ebdc94..0004a6b 100644 --- a/main/urls.py +++ b/main/urls.py @@ -1,30 +1,12 @@ -""" -URL configuration for main project. -The `urlpatterns` list routes URLs to views. For more information please see: - https://docs.djangoproject.com/en/5.1/topics/http/urls/ -Examples: -Function views - 1. Add an import: from my_app import views - 2. Add a URL to urlpatterns: path('', views.home, name='home') -Class-based views - 1. Add an import: from other_app.views import Home - 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') -Including another URLconf - 1. Import the include() function: from django.urls import include, path - 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) -""" from django.contrib import admin from django.urls import path,include -from rest_framework_simplejwt.views import ( - TokenObtainPairView, - TokenRefreshView, -) from rest_framework import urls from rest_framework.authtoken import views - +from .views import Homeroute urlpatterns = [ path('admin/', admin.site.urls), + path('',Homeroute,name="HomePage"), path('login', views.ObtainAuthToken.as_view(), name='obtain_auth_token'), path('api/', include('event.urls')), diff --git a/main/views.py b/main/views.py index e69de29..c8ef0d4 100644 --- a/main/views.py +++ b/main/views.py @@ -0,0 +1,8 @@ +from rest_framework.decorators import api_view +from django.http import JsonResponse + + + +@api_view(["GET"]) +def Homeroute(request): + return JsonResponse({"message": " Welcome to Eventrio Official API"}) \ No newline at end of file diff --git a/templates/event_registration.html b/templates/event_registration.html new file mode 100644 index 0000000..d598455 --- /dev/null +++ b/templates/event_registration.html @@ -0,0 +1,57 @@ + + + + + + +
+
+

Event Registration Confirmation

+
+
+

Dear Attendee,

+

Thank you for registering for {{ event_title }}.

+

Here are the event details:

+
    +
  • Date: {{ event_date }}
  • +
  • Time: {{ event_time }}
  • +
  • Location: {{ event_location }}
  • +
+

We look forward to seeing you at the event!

+
+ +
+ + \ No newline at end of file