commit 20f7c7234f756f182a9509f062a6442c6c60d192 Author: Nils Büchner Date: Mon Mar 25 10:42:15 2024 +0100 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9db5ed9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +__pycache__/ +*.py[cod] +venv +olm +src +.local +.cmake +.cache +.ubottu +.bashrc +.python_history +.bash_history +.mbp +*.db +*.log +plugins/* +ubottu/maubot.db diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/ubottu/factoids/__init__.py b/ubottu/factoids/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ubottu/factoids/admin.py b/ubottu/factoids/admin.py new file mode 100644 index 0000000..6a051b0 --- /dev/null +++ b/ubottu/factoids/admin.py @@ -0,0 +1,26 @@ +from django.contrib import admin +from django.forms import ModelForm, Textarea +from .models import Fact +from .forms import FactForm + +class FactAdmin(admin.ModelAdmin): + list_display = ('name', 'value', 'ftype', 'create_date', 'change_date', 'author', 'popularity') + list_filter = ('ftype', 'create_date', 'author') # Fields to add filters for + search_fields = ('name', 'value') + form = FactForm + def get_form(self, request, obj=None, **kwargs): + # Check if an instance is being added (obj is None) to exclude 'author_id' field + if obj is None: # This means a new instance is being created + self.exclude = ('author',) + else: # Editing an existing instance + self.exclude = [] + form = super(FactAdmin, self).get_form(request, obj, **kwargs) + return form + + def save_model(self, request, obj, form, change): + if not obj.pk: # Indicates a new instance + # Automatically set 'author_id' to current user for new instances + obj.author_id = request.user.id + super().save_model(request, obj, form, change) + +admin.site.register(Fact, FactAdmin) diff --git a/ubottu/factoids/apps.py b/ubottu/factoids/apps.py new file mode 100644 index 0000000..0fcd820 --- /dev/null +++ b/ubottu/factoids/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class FactoidsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'factoids' diff --git a/ubottu/factoids/forms.py b/ubottu/factoids/forms.py new file mode 100644 index 0000000..c2f97b8 --- /dev/null +++ b/ubottu/factoids/forms.py @@ -0,0 +1,9 @@ +from django.forms import ModelForm, Textarea +from .models import Fact +class FactForm(ModelForm): + class Meta: + model = Fact + fields = '__all__' + widgets = { + 'value': Textarea(attrs={'cols': 60, 'rows': 10}), + } \ No newline at end of file diff --git a/ubottu/factoids/models.py b/ubottu/factoids/models.py new file mode 100644 index 0000000..de87304 --- /dev/null +++ b/ubottu/factoids/models.py @@ -0,0 +1,34 @@ +from django.db import models +from django.utils import timezone +from django.contrib.auth.models import User + +# Define Author model +class Author(models.Model): + name = models.CharField(max_length=64) + +def get_sentinel_author(): + # This assumes 'Author' model is already defined as shown above + return Author.objects.get_or_create(name='deleted')[0] +class Fact(models.Model): + name = models.CharField(max_length=32) + value = models.TextField() + FTYPE_CHOICES = ( + ("REPLY", "Reply"), + ("ALIAS", "Alias") + ) + ftype = models.CharField( + max_length=32, + choices=FTYPE_CHOICES, + default="REPLY" + ) + + id = models.BigAutoField(primary_key=True) + author = models.ForeignKey(User, on_delete=models.SET_NULL, null=True) + create_date = models.DateTimeField("date published", default=timezone.now) + change_date = models.DateTimeField("date changed", default=timezone.now) + popularity = models.IntegerField(default=0) + def __str__(self): + return self.name + + def was_published_recently(self): + return self.create_date >= timezone.now() - datetime.timedelta(days=7) diff --git a/ubottu/factoids/serializers.py b/ubottu/factoids/serializers.py new file mode 100644 index 0000000..37dece8 --- /dev/null +++ b/ubottu/factoids/serializers.py @@ -0,0 +1,12 @@ +from rest_framework import serializers +from .models import Fact + +class FactSerializer(serializers.ModelSerializer): + author_name = serializers.SerializerMethodField() + class Meta: + model = Fact + fields = ['id', 'name', 'value', 'ftype', 'author_name', 'create_date', 'change_date', 'popularity'] + + def get_author_name(self, obj): + # Assuming the author field can be null + return obj.author.username if obj.author else None \ No newline at end of file diff --git a/ubottu/factoids/static/factoids/css/styles.css b/ubottu/factoids/static/factoids/css/styles.css new file mode 100644 index 0000000..984ff9d --- /dev/null +++ b/ubottu/factoids/static/factoids/css/styles.css @@ -0,0 +1,107 @@ +/* static/css/styles.css */ + +body { + font-family: 'Open Sans', sans-serif; + font-size: 10pt; +} + +.dark-theme { + background-color: #343a40; + color: #ffffff; + } + +.dark-theme .table { + color: #ffffff; + background-color: #454d55; +} + +.dark-theme .table thead th { + color: #ffffff; + background-color: #343a40; +} + +.dark-theme .table tbody tr:nth-of-type(odd) { + background-color: #454d55; +} + +/* Hover effect for light theme (or the default theme) */ +.table tbody tr:hover { + background-color: #e9ecef; /* Light grey for hover in light theme */ + color: #212529; /* Optional: Darker text color if needed */ +} + +/* Hover effect for dark theme */ +.dark-theme .table tbody tr:hover { + background-color: #5a6268; /* Darker grey for hover in dark theme */ + color: #f8f9fa; /* Lighter text color for dark theme */ +} + +/* Light theme link hover color */ +.table tbody tr:hover a { + color: #007bff; /* Bootstrap's default link color */ +} + +/* Dark theme link hover color */ +.dark-theme .table tbody tr:hover a { + color: #80bdff; /* A lighter shade for dark mode */ +} + +.expandable { + max-height: 12px; /* Adjust based on your line height */ + max-width: 420px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + cursor: pointer; +} + +.toggle-switch { + position: relative; + display: inline-block; + width: 40px; /* Reduced from 60px */ + height: 22px; /* Reduced from 34px */ + background-color: #ccc; + border-radius: 22px; /* Match height for full roundness */ + transition: background-color .3s; +} + +.toggle-switch .switch { + position: absolute; + top: 1px; /* Adjusted for smaller size */ + left: 1px; /* Adjusted for smaller size */ + width: 20px; /* Reduced from 30px */ + height: 20px; /* Reduced from 30px */ + background-color: white; + border-radius: 50%; + transition: transform .3s; +} + +.toggle-switch.active { + background-color: #4CAF50; +} + +.toggle-switch.active .switch { + transform: translateX(18px); /* Adjusted to fit the smaller toggle size */ +} + +.theme-toggle-container { + display: flex; + align-items: center; /* Aligns the button and text vertically */ + gap: 10px; /* Adds some space between the button and the text */ + font-family: 'Open Sans', sans-serif; /* This should match the font you've chosen */ +} + +.toggle-description { + font-size: 16px; /* Adjust as needed */ + color: #333; /* Adjust based on your light theme color */ + font-weight: 600; /* Makes the font slightly bolder */ +} + +/* Additional styles for dark theme */ +.dark-theme .toggle-description { + color: #fff; /* Adjust based on your dark theme color */ +} + +.bi-info-circle-fill { + font-size: 2rem; /* Adjust the size as needed */ +} \ No newline at end of file diff --git a/ubottu/factoids/static/factoids/images/favicon.ico b/ubottu/factoids/static/factoids/images/favicon.ico new file mode 100644 index 0000000..ca23307 Binary files /dev/null and b/ubottu/factoids/static/factoids/images/favicon.ico differ diff --git a/ubottu/factoids/templates/factoids/base.html b/ubottu/factoids/templates/factoids/base.html new file mode 100644 index 0000000..e748e82 --- /dev/null +++ b/ubottu/factoids/templates/factoids/base.html @@ -0,0 +1,50 @@ +{% load static %} + + + + + + + + + + {% block title %}Factoids{% endblock %} + + + +
+

Factoids

+ +
+
+ + Dark Mode +
+
+ {% block content %} + {% endblock content %} +
+ + + + diff --git a/ubottu/factoids/templates/factoids/list_facts.html b/ubottu/factoids/templates/factoids/list_facts.html new file mode 100644 index 0000000..9201e7f --- /dev/null +++ b/ubottu/factoids/templates/factoids/list_facts.html @@ -0,0 +1,48 @@ +{% extends "factoids/base.html" %} +{% block content %} + +
+

Facts List

+
+ + + + + + + + + + + + {% for fact in facts %} + + + + + + + + {% endfor %} + +
NameValueTypeAuthorPopularity
{{ fact.name }}{{ fact.get_ftype_display }}{% if fact.author %}{{ fact.author.username }}{% else %}N/A{% endif %}{{ fact.popularity }}
+
+ +
+{% endblock %} diff --git a/ubottu/factoids/tests.py b/ubottu/factoids/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/ubottu/factoids/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/ubottu/factoids/urls.py b/ubottu/factoids/urls.py new file mode 100644 index 0000000..54373c4 --- /dev/null +++ b/ubottu/factoids/urls.py @@ -0,0 +1,11 @@ +from django.urls import path +from .views import FactList +from . import views + +urlpatterns = [ + path('', views.list_facts, name='facts-list'), + path('api/citytime//', views.city_time, name='citytime'), + path('api/facts/', FactList.as_view(), name='fact-list'), # For listing all facts + path('api/facts//', FactList.as_view(), name='fact-detail-by-id'), # For fetching by id + path('api/facts//', FactList.as_view(), name='fact-detail-by-name'), # For fetching by name +] diff --git a/ubottu/factoids/views.py b/ubottu/factoids/views.py new file mode 100644 index 0000000..fecb491 --- /dev/null +++ b/ubottu/factoids/views.py @@ -0,0 +1,93 @@ +from django.http import HttpResponse, Http404 +from rest_framework.views import APIView +from rest_framework.decorators import api_view +from rest_framework.response import Response +from .models import Fact +from .serializers import FactSerializer +from django.shortcuts import render +from geopy.geocoders import Nominatim +from timezonefinder import TimezoneFinder +from datetime import datetime +from rest_framework import status +import pytz +import json + +def index(request): + return HttpResponse("Hello, world. You're at the factoids index.") + +@api_view(['GET']) +def city_time(request, city_name): + try: + geolocator = Nominatim(user_agent="Ubottu", timeout=10) + location = geolocator.geocode(city_name, exactly_one=True, language='en') + if location is None: + # If the location wasn't found, return an appropriate response + return Response({'error': 'Location not found'}, status=status.HTTP_404_NOT_FOUND) + + tf = TimezoneFinder() + timezone_str = tf.timezone_at(lat=location.latitude, lng=location.longitude) # Get the timezone name + + if timezone_str is None: + # If the timezone wasn't found, return an appropriate response + return Response({'error': 'Timezone not found for the given location'}, status=status.HTTP_404_NOT_FOUND) + + timezone = pytz.timezone(timezone_str) + datetime_obj = datetime.now(timezone) + local_time = datetime_obj.strftime('%A, %d %B %Y, %H:%M') + city_name = str(location).split(',')[0] + data = {'location': str(location), 'city': city_name, 'local_time': local_time} + return Response(data) + except Exception as e: + # Log the exception if needed + print(f"Error processing request for city {city_name}: {str(e)}") + # Return a JSON response indicating an error occurred + # Returning False directly is not recommended for API responses + return Response({'error': 'An error occurred processing your request'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + +def list_facts(request): + #facts = Fact.objects.all() + sort_by = request.GET.get('sort', 'name') # Default sort by 'name' + allowed_sorts = ['name', 'ftype', 'popularity'] + if sort_by not in allowed_sorts: + sort_by = 'name' # Fallback to a safe default + if sort_by == 'popularity': + facts = Fact.objects.order_by('-popularity', 'name') + else: + facts = Fact.objects.order_by(sort_by) + return render(request, 'factoids/list_facts.html', {'facts': facts}) + +class FactList(APIView): + """ + List all Fact items or retrieve a single Fact item by name or id. + """ + def get(self, request, *args, **kwargs): + # Check if an 'id' parameter is provided in the URL. + fact_id = kwargs.get('id') + # Check if a 'name' parameter is provided in the URL. + name = kwargs.get('name') + + if fact_id: + # Fetching the Fact item by id. + try: + fact = Fact.objects.get(id=fact_id) + except Fact.DoesNotExist: + raise Http404("Fact not found") + elif name: + # Fetching the Fact item by name. + try: + fact = Fact.objects.get(name=name) + fact.popularity += 1 # Increment popularity + fact.save(update_fields=['popularity']) # Save the change + except Fact.DoesNotExist: + raise Http404("Fact not found") + else: + # If neither 'id' nor 'name' is provided, you might want to list all facts + # or handle the situation differently (e.g., return an error response). + facts = Fact.objects.all() + serializer = FactSerializer(facts, many=True) + return Response(serializer.data) + + # Serializing the retrieved Fact item. + serializer = FactSerializer(fact) + return Response(serializer.data) \ No newline at end of file diff --git a/ubottu/manage.py b/ubottu/manage.py new file mode 100755 index 0000000..9e06365 --- /dev/null +++ b/ubottu/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ubottu.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/ubottu/ubottu/__init__.py b/ubottu/ubottu/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ubottu/ubottu/asgi.py b/ubottu/ubottu/asgi.py new file mode 100644 index 0000000..f5d1cde --- /dev/null +++ b/ubottu/ubottu/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for ubottu project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/5.0/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ubottu.settings') + +application = get_asgi_application() diff --git a/ubottu/ubottu/settings.py b/ubottu/ubottu/settings.py new file mode 100644 index 0000000..0e4e45b --- /dev/null +++ b/ubottu/ubottu/settings.py @@ -0,0 +1,135 @@ +""" +Django settings for ubottu project. + +Generated by 'django-admin startproject' using Django 5.0.3. + +For more information on this file, see +https://docs.djangoproject.com/en/5.0/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/5.0/ref/settings/ +""" + +from pathlib import Path + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = 'aekeijahsheik8AhghaiDie5awuen3ahvaeHeeyirahsaic4einaePeiyoophahd' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = ['*'] + +REST_FRAMEWORK = { + 'DEFAULT_RENDERER_CLASSES': [ + 'rest_framework.renderers.JSONRenderer', + ], +} + +# Application definition + +INSTALLED_APPS = [ + 'rest_framework', + 'factoids.apps.FactoidsConfig', + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'ubottu.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [BASE_DIR / 'templates'], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'ubottu.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/5.0/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.mysql', + 'HOST': '127.0.0.1', + 'USER': 'ubottu', + 'PASSWORD': 'iyoiyiG7Kahy', + 'NAME': 'ubottu' + } +} + + +# Password validation +# https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/5.0/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/5.0/howto/static-files/ + +STATIC_URL = 'static/' +#STATIC_ROOT = "/home/maubot/ubottu-web/static/" +STATICFILES_DIRS = [BASE_DIR / 'static'] + +# Default primary key field type +# https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' diff --git a/ubottu/ubottu/urls.py b/ubottu/ubottu/urls.py new file mode 100644 index 0000000..61a2b75 --- /dev/null +++ b/ubottu/ubottu/urls.py @@ -0,0 +1,9 @@ +from django.contrib import admin +from django.urls import include, path +from django.shortcuts import redirect + +urlpatterns = [ + path("factoids/", include("factoids.urls")), + path("admin/", admin.site.urls), + path('', lambda request: redirect('factoids/', permanent=False)), # Redirect from root to 'factoids' +] \ No newline at end of file diff --git a/ubottu/ubottu/wsgi.py b/ubottu/ubottu/wsgi.py new file mode 100644 index 0000000..f1d8832 --- /dev/null +++ b/ubottu/ubottu/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for ubottu project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/5.0/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ubottu.settings') + +application = get_wsgi_application()