Skip to content
Merged
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
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
[![OPTIMETA Logo](https://projects.tib.eu/fileadmin/_processed_/e/8/csm_Optimeta_Logo_web_98c26141b1.png)](https://projects.tib.eu/optimeta/en/) [![KOMET Logo](https://projects.tib.eu/fileadmin/templates/komet/tib_projects_komet_1150.png)](https://projects.tib.eu/komet/en/)

# OPTIMAP

[![Project Status: WIP – Initial development is in progress, but there has not yet been a stable, usable release suitable for the public.](https://www.repostatus.org/badges/latest/wip.svg)](https://www.repostatus.org/#wip) [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.8198944.svg)](https://doi.org/10.5281/zenodo.8198944)
Expand All @@ -17,6 +15,10 @@ The OPTIMAP has the following features:

OPTIMAP is based on [Django](https://www.djangoproject.com/) (with [GeoDjango](https://docs.djangoproject.com/en/4.1/ref/contrib/gis/) and [Django REST framework](https://www.django-rest-framework.org/)) with a [PostgreSQL](https://www.postgresql.org/)/[PostGIS](https://postgis.net/) database backend.

The development of OPTIMAP was and is supported by the projects [OPTIMETA](https://projects.tib.eu/optimeta/en/) and [KOMET](https://projects.tib.eu/komet/en/).

[![OPTIMETA Logo](https://projects.tib.eu/fileadmin/_processed_/e/8/csm_Optimeta_Logo_web_98c26141b1.png)](https://projects.tib.eu/optimeta/en/) [![KOMET Logo](https://projects.tib.eu/fileadmin/templates/komet/tib_projects_komet_1150.png)](https://projects.tib.eu/komet/en/)

## Configuration

All configuration is done via the file `optimap/settings.py`.
Expand Down Expand Up @@ -222,7 +224,7 @@ python manage.py createsuperuser --username=optimap [email protected]

You will be prompted for a password. After entering one, the superuser will be created immediately. If you omit the --username or --email options, the command will prompt you for those values interactively.

Access the admin interface at http://127.0.0.1:8000/admin/.
Access the admin interface at <http://127.0.0.1:8000/admin/>.

#### Running in a Dockerized App

Expand Down
8 changes: 5 additions & 3 deletions optimap/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,6 @@
"sesame.backends.ModelBackend",
]

AUTH_USER_MODEL = "publications.CustomUser"

INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
Expand All @@ -61,16 +59,18 @@
'django.contrib.staticfiles',
'django.contrib.gis',
'django.contrib.sitemaps',
'publications',
'rest_framework',
'rest_framework_gis',
'publications',
'django_q',
'drf_spectacular',
'drf_spectacular_sidecar',
'leaflet',
'import_export',
]

AUTH_USER_MODEL = "publications.CustomUser"

REST_FRAMEWORK = {
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
Expand Down Expand Up @@ -186,6 +186,8 @@
EMAIL_USE_SSL = env('OPTIMAP_EMAIL_USE_SSL', default=False)
BASE_URL = env("OPTIMAP_BASE_URL", default="http://localhost:8000")
EMAIL_IMAP_SENT_FOLDER = env('OPTIMAP_EMAIL_IMAP_SENT_FOLDER', default='')
OPTIMAP_EMAIL_SEND_DELAY = env("OPTIMAP_EMAIL_SEND_DELAY", default=2)
BASE_URL = env("BASE_URL", default="http://127.0.0.1:8000")
OAI_USERNAME = env("OPTIMAP_OAI_USERNAME", default="")
OAI_PASSWORD = env("OPTIMAP_OAI_PASSWORD", default="")
EMAIL_SEND_DELAY = 2
Expand Down
49 changes: 38 additions & 11 deletions publications/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,11 @@
from leaflet.admin import LeafletGeoAdmin
from publications.models import Publication, Source, HarvestingEvent, BlockedEmail, BlockedDomain
from import_export.admin import ImportExportModelAdmin
from publications.tasks import harvest_oai_endpoint
from publications.models import EmailLog, Subscription, UserProfile
from publications.tasks import harvest_oai_endpoint, schedule_subscription_email_task, send_monthly_email, schedule_monthly_email_task
from django_q.models import Schedule
from django.utils.timezone import now
from django_q.tasks import schedule
from publications.models import EmailLog, UserProfile
from publications.tasks import send_monthly_email, schedule_monthly_email_task
from django.contrib.auth import get_user_model
User = get_user_model()
from publications.models import CustomUser

@admin.action(description="Mark selected publications as published")
def make_public(modeladmin, request, queryset):
Expand Down Expand Up @@ -70,6 +67,33 @@ def trigger_monthly_email_task(modeladmin, request, queryset):
except Exception as e:
messages.error(request, f"Failed to schedule task: {e}")

@admin.action(description="Send subscription-based emails")
def send_subscription_emails(modeladmin, request, queryset):
"""
Admin action to manually send subscription-based emails to selected users.
"""
from publications.tasks import send_subscription_based_email

selected_users = queryset.filter(subscribed=True).values_list('user', flat=True)
if not selected_users:
messages.warning(request, "No active subscribers selected.")
return

send_subscription_based_email(sent_by=request.user, user_ids=list(selected_users))
messages.success(request, "Subscription-based emails have been sent.")

@admin.action(description="Schedule subscription-based Email Task")
def send_subscription_emails_scheduler(modeladmin, request, queryset):
"""
Admin action to manually schedule the email task.
"""
try:
schedule_subscription_email_task(sent_by=request.user)
messages.success(request, "Monthly email task has been scheduled successfully.")
except Exception as e:
messages.error(request, f"Failed to schedule task: {e}")


@admin.action(description="Delete user and block email")
def block_email(modeladmin, request, queryset):
for user in queryset:
Expand Down Expand Up @@ -122,15 +146,14 @@ class EmailLogAdmin(admin.ModelAdmin):
search_fields = ("recipient_email", "subject", "sent_by__username")
actions = [trigger_monthly_email, trigger_monthly_email_task]

class SubscriptionAdmin(admin.ModelAdmin):
list_display = ("user", "region", "subscribed")
actions = [send_subscription_emails, send_subscription_emails_scheduler]

class UserProfileAdmin(admin.ModelAdmin):
list_display = ("user", "notify_new_manuscripts")
search_fields = ("user__email",)


admin.site.register(EmailLog, EmailLogAdmin)
admin.site.register(UserProfile, UserProfileAdmin)

@admin.register(BlockedEmail)
class BlockedEmailAdmin(admin.ModelAdmin):
list_display = ('email', 'created_at', 'blocked_by')
Expand All @@ -141,8 +164,12 @@ class BlockedDomainAdmin(admin.ModelAdmin):
list_display = ('domain', 'created_at', 'blocked_by')
search_fields = ('domain',)

@admin.register(User)
@admin.register(CustomUser)
class UserAdmin(admin.ModelAdmin):
"""User Admin."""
list_display = ("username", "email", "is_active")
actions = [block_email, block_email_and_domain]

admin.site.register(EmailLog, EmailLogAdmin)
admin.site.register(UserProfile, UserProfileAdmin)
admin.site.register(Subscription, SubscriptionAdmin)
35 changes: 18 additions & 17 deletions publications/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Generated by Django 5.1.7 on 2025-03-19 19:34
# Generated by Django 5.1.7 on 2025-04-08 10:02

import django.contrib.auth.models
import django.contrib.auth.validators
Expand All @@ -21,22 +21,6 @@ class Migration(migrations.Migration):
]

operations = [
migrations.CreateModel(
name='Subscription',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=4096)),
('search_term', models.CharField(max_length=4096, null=True)),
('timeperiod_startdate', models.DateField(null=True)),
('timeperiod_enddate', models.DateField(null=True)),
('search_area', django.contrib.gis.db.models.fields.GeometryCollectionField(blank=True, null=True, srid=4326)),
('user_name', models.CharField(max_length=4096)),
],
options={
'verbose_name': 'subscription',
'ordering': ['user_name'],
},
),
migrations.CreateModel(
name='CustomUser',
fields=[
Expand Down Expand Up @@ -121,6 +105,23 @@ class Migration(migrations.Migration):
('source', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='harvesting_events', to='publications.source')),
],
),
migrations.CreateModel(
name='Subscription',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(default='default_subscription', max_length=4096)),
('search_term', models.CharField(max_length=4096, null=True)),
('timeperiod_startdate', models.DateField(null=True)),
('timeperiod_enddate', models.DateField(null=True)),
('region', django.contrib.gis.db.models.fields.GeometryCollectionField(blank=True, null=True, srid=4326)),
('subscribed', models.BooleanField(default=True)),
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='subscriptions', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'subscription',
'ordering': ['name'],
},
),
migrations.CreateModel(
name='UserProfile',
fields=[
Expand Down

This file was deleted.

36 changes: 16 additions & 20 deletions publications/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@
from django_q.models import Schedule
from django.utils.timezone import now
from django.contrib.auth.models import AbstractUser, Group, Permission
import uuid
from django.utils.timezone import now
# handle import/export relations, see https://django-import-export.readthedocs.io/en/stable/advanced_usage.html#creating-non-existent-relations
from import_export import fields, resources
from import_export.widgets import ForeignKeyWidget
from django.conf import settings

import logging
logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -122,26 +126,23 @@ def save(self, *args, **kwargs):
)



class Subscription(models.Model):
name = models.CharField(max_length=4096)
user = models.ForeignKey(CustomUser, on_delete=models.CASCADE, related_name="subscriptions", null=True, blank=True)
name = models.CharField(max_length=4096, default="default_subscription")
search_term = models.CharField(max_length=4096,null=True)
timeperiod_startdate = models.DateField(null=True)
timeperiod_enddate = models.DateField(null=True)
search_area = models.GeometryCollectionField(null=True, blank=True)
user_name = models.CharField(max_length=4096)
region = models.GeometryCollectionField(null=True, blank=True)
subscribed = models.BooleanField(default=True)

def __str__(self):
"""Return string representation."""
return self.name

class Meta:
ordering = ['user_name']
ordering = ['name']
verbose_name = "subscription"

from django.contrib.auth import get_user_model
User = get_user_model()

class EmailLog(models.Model):
TRIGGER_CHOICES = [
("admin", "Admin Panel"),
Expand All @@ -152,7 +153,7 @@ class EmailLog(models.Model):
subject = models.CharField(max_length=255)
sent_at = models.DateTimeField(auto_now_add=True)
email_content = models.TextField(blank=True, null=True)
sent_by = models.ForeignKey(User, null=True, blank=True, on_delete=models.SET_NULL)
sent_by = models.ForeignKey(CustomUser, null=True, blank=True, on_delete=models.SET_NULL)
trigger_source = models.CharField(max_length=50, choices=TRIGGER_CHOICES, default="manual")
status = models.CharField(max_length=10, choices=STATUS_CHOICES, default="success")
error_message = models.TextField(null=True, blank=True)
Expand All @@ -176,18 +177,13 @@ def log_email(cls, recipient, subject, content, sent_by=None, trigger_source="ma

)

# handle import/export relations, see https://django-import-export.readthedocs.io/en/stable/advanced_usage.html#creating-non-existent-relations
from import_export import fields, resources
from import_export.widgets import ForeignKeyWidget
from django.conf import settings

class PublicationResource(resources.ModelResource):
#created_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='username')
#updated_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='username')
created_by = fields.Field(
column_name='created_by',
attribute='created_by',
widget=ForeignKeyWidget(User, field='username'))
widget=ForeignKeyWidget(CustomUser, field='username'))
updated_by = fields.Field(
column_name='updated_by',
attribute='updated_by',
Expand All @@ -199,7 +195,7 @@ class Meta:

class HarvestingEvent(models.Model):
source = models.ForeignKey('Source', on_delete=models.CASCADE, related_name='harvesting_events')
user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)
user = models.ForeignKey(CustomUser, on_delete=models.SET_NULL, null=True, blank=True)
started_at = models.DateTimeField(auto_now_add=True)
completed_at = models.DateTimeField(null=True, blank=True)
status = models.CharField(
Expand All @@ -218,7 +214,7 @@ def __str__(self):


class UserProfile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
user = models.OneToOneField(CustomUser, on_delete=models.CASCADE)
notify_new_manuscripts = models.BooleanField(default=False)

def __str__(self):
Expand All @@ -227,15 +223,15 @@ def __str__(self):
class BlockedEmail(models.Model):
email = models.EmailField(unique=True)
created_at = models.DateTimeField(auto_now_add=True)
blocked_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name="blocked_emails")
blocked_by = models.ForeignKey(CustomUser, on_delete=models.SET_NULL, null=True, blank=True, related_name="blocked_emails")

def __str__(self):
return self.email

class BlockedDomain(models.Model):
domain = models.CharField(max_length=255, unique=True)
created_at = models.DateTimeField(auto_now_add=True)
blocked_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name="blocked_domains")
blocked_by = models.ForeignKey(CustomUser, on_delete=models.SET_NULL, null=True, blank=True, related_name="blocked_domains")

def __str__(self):
return self.domain
2 changes: 1 addition & 1 deletion publications/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class SubscriptionSerializer(serializers.GeoFeatureModelSerializer):
class Meta:
model = Subscription
fields = ("search_term","timeperiod_startdate","timeperiod_enddate","user_name")
geo_field = "search_area"
geo_field = "region"
auto_bbox = True

class EmailChangeSerializer(serializers.ModelSerializer):
Expand Down
Loading
Loading