From 08690e08b682c2cbcc313787f409792f660a08e6 Mon Sep 17 00:00:00 2001 From: Lova ANDRIARIMALALA <43842786+Xpirix@users.noreply.github.com> Date: Mon, 17 Jun 2024 17:03:21 +0300 Subject: [PATCH 1/8] Init social share templates and tag --- .../qgisfeed/templatetags/__init__.py | 0 .../qgisfeed/templatetags/social_share.py | 260 ++++++++++++++++++ qgisfeedproject/qgisfeed/utils.py | 51 ++++ .../templatetags/copy_script.html | 44 +++ .../templatetags/copy_to_clipboard.html | 3 + .../templatetags/pinterest_script.html | 1 + .../templatetags/post_to_facebook.html | 5 + .../templatetags/post_to_gplus.html | 3 + .../templatetags/post_to_linkedin.html | 16 ++ .../templatetags/post_to_reddit.html | 3 + .../templatetags/post_to_telegram.html | 3 + .../templatetags/post_to_twitter.html | 3 + .../templatetags/post_to_whatsapp.html | 3 + .../templatetags/save_to_pinterest.html | 3 + .../templatetags/send_email.html | 3 + 15 files changed, 401 insertions(+) create mode 100644 qgisfeedproject/qgisfeed/templatetags/__init__.py create mode 100644 qgisfeedproject/qgisfeed/templatetags/social_share.py create mode 100644 qgisfeedproject/templates/django_social_share/templatetags/copy_script.html create mode 100644 qgisfeedproject/templates/django_social_share/templatetags/copy_to_clipboard.html create mode 100644 qgisfeedproject/templates/django_social_share/templatetags/pinterest_script.html create mode 100644 qgisfeedproject/templates/django_social_share/templatetags/post_to_facebook.html create mode 100644 qgisfeedproject/templates/django_social_share/templatetags/post_to_gplus.html create mode 100644 qgisfeedproject/templates/django_social_share/templatetags/post_to_linkedin.html create mode 100644 qgisfeedproject/templates/django_social_share/templatetags/post_to_reddit.html create mode 100644 qgisfeedproject/templates/django_social_share/templatetags/post_to_telegram.html create mode 100644 qgisfeedproject/templates/django_social_share/templatetags/post_to_twitter.html create mode 100644 qgisfeedproject/templates/django_social_share/templatetags/post_to_whatsapp.html create mode 100644 qgisfeedproject/templates/django_social_share/templatetags/save_to_pinterest.html create mode 100644 qgisfeedproject/templates/django_social_share/templatetags/send_email.html diff --git a/qgisfeedproject/qgisfeed/templatetags/__init__.py b/qgisfeedproject/qgisfeed/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/qgisfeedproject/qgisfeed/templatetags/social_share.py b/qgisfeedproject/qgisfeed/templatetags/social_share.py new file mode 100644 index 0000000..ceee3ff --- /dev/null +++ b/qgisfeedproject/qgisfeed/templatetags/social_share.py @@ -0,0 +1,260 @@ +# -*- coding: utf-8 -*- +# This code is based on https://github.com/fcurella/django-social-share +# Copyright (C) 2011 by Flavio Curella +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +from __future__ import unicode_literals + +import re + +from django import template + +from django.db.models import Model +from django.template.defaultfilters import urlencode +from django.utils.safestring import mark_safe + + +register = template.Library() + + +TWITTER_ENDPOINT = 'https://twitter.com/intent/tweet?text=%s' +FACEBOOK_ENDPOINT = 'https://www.facebook.com/sharer/sharer.php?u=%s' +GPLUS_ENDPOINT = 'https://plus.google.com/share?url=%s' +MAIL_ENDPOINT = 'mailto:?subject=%s&body=%s' +LINKEDIN_ENDPOINT = 'https://www.linkedin.com/shareArticle?mini=true&title=%s&url=%s' +REDDIT_ENDPOINT = 'https://www.reddit.com/submit?title=%s&url=%s' +TELEGRAM_ENDPOINT = 'https://t.me/share/url?text=%s&url=%s' +WHATSAPP_ENDPOINT = 'https://api.whatsapp.com/send?text=%s' +PINTEREST_ENDPOINT = 'https://www.pinterest.com/pin/create/button/?url=%s' + + +BITLY_REGEX = re.compile(r'^https?://bit\.ly/') + + +def compile_text(context, text): + ctx = template.context.Context(context) + return template.Template(text).render(ctx) + + +def _build_url(request, obj_or_url): + if obj_or_url is not None: + if isinstance(obj_or_url, Model): + return request.build_absolute_uri(obj_or_url.get_absolute_url()) + else: + return request.build_absolute_uri(obj_or_url) + return '' + + +def _compose_tweet(text, url=None): + TWITTER_MAX_NUMBER_OF_CHARACTERS = 140 + TWITTER_LINK_LENGTH = 23 # "A URL of any length will be altered to 23 characters, even if the link itself is less than 23 characters long. + + # Compute length of the tweet + url_length = len(' ') + TWITTER_LINK_LENGTH if url else 0 + total_length = len(text) + url_length + + # Check that the text respects the max number of characters for a tweet + if total_length > TWITTER_MAX_NUMBER_OF_CHARACTERS: + text = text[:(TWITTER_MAX_NUMBER_OF_CHARACTERS - url_length - 1)] + "…" # len("…") == 1 + + return "%s %s" % (text, url) if url else text + + +@register.simple_tag(takes_context=True) +def post_to_twitter_url(context, text, obj_or_url=None): + text = compile_text(context, text) + request = context['request'] + + url = _build_url(request, obj_or_url) + + tweet = _compose_tweet(text, url) + context['tweet_url'] = TWITTER_ENDPOINT % urlencode(tweet) + return context + + +@register.inclusion_tag('django_social_share/templatetags/post_to_twitter.html', takes_context=True) +def post_to_twitter(context, text, obj_or_url=None, link_text='',link_class=""): + context = post_to_twitter_url(context, text, obj_or_url) + + request = context['request'] + url = _build_url(request, obj_or_url) + tweet = _compose_tweet(text, url) + + context['link_class'] = link_class + context['link_text'] = link_text or 'Post to Twitter' + context['full_text'] = tweet + return context + + +@register.simple_tag(takes_context=True) +def post_to_facebook_url(context, obj_or_url=None): + request = context['request'] + url = _build_url(request, obj_or_url) + context['facebook_url'] = FACEBOOK_ENDPOINT % urlencode(url) + return context + + +@register.inclusion_tag('django_social_share/templatetags/post_to_facebook.html', takes_context=True) +def post_to_facebook(context, obj_or_url=None, link_text='',link_class=''): + context = post_to_facebook_url(context, obj_or_url) + context['link_class'] = link_class or '' + context['link_text'] = link_text or 'Post to Facebook' + return context + + +@register.simple_tag(takes_context=True) +def post_to_gplus_url(context, obj_or_url=None): + request = context['request'] + url = _build_url(request, obj_or_url) + context['gplus_url'] = GPLUS_ENDPOINT % urlencode(url) + return context + + +@register.inclusion_tag('django_social_share/templatetags/post_to_gplus.html', takes_context=True) +def post_to_gplus(context, obj_or_url=None, link_text='',link_class=''): + context = post_to_gplus_url(context, obj_or_url) + context['link_class'] = link_class + context['link_text'] = link_text or 'Post to Google+' + return context + + +@register.simple_tag(takes_context=True) +def send_email_url(context, subject, text, obj_or_url=None): + text = compile_text(context, text) + subject = compile_text(context, subject) + request = context['request'] + url = _build_url(request, obj_or_url) + full_text = "%s %s" % (text, url) + context['mailto_url'] = MAIL_ENDPOINT % (urlencode(subject), urlencode(full_text)) + return context + + +@register.inclusion_tag('django_social_share/templatetags/send_email.html', takes_context=True) +def send_email(context, subject, text, obj_or_url=None, link_text='',link_class=''): + context = send_email_url(context, subject, text, obj_or_url) + context['link_class'] = link_class + context['link_text'] = link_text or 'Share via email' + return context + + +@register.filter(name='linkedin_locale') +def linkedin_locale(value): + if "-" not in value: + return value + + lang, country = value.split('-') + return '_'.join([lang, country.upper()]) + + +@register.simple_tag(takes_context=True) +def post_to_linkedin_url(context, obj_or_url=None): + request = context['request'] + url = _build_url(request, obj_or_url) + context['linkedin_url'] = url + return context + + +@register.inclusion_tag('django_social_share/templatetags/post_to_linkedin.html', takes_context=True) +def post_to_linkedin(context, obj_or_url=None,link_class=''): + context = post_to_linkedin_url(context, obj_or_url) + context['link_class'] = link_class + return context + + +@register.simple_tag(takes_context=True) +def post_to_reddit_url(context, title, obj_or_url=None): + request = context['request'] + title = compile_text(context, title) + url = _build_url(request, obj_or_url) + context['reddit_url'] = mark_safe(REDDIT_ENDPOINT % (urlencode(title), urlencode(url))) + return context + + +@register.inclusion_tag('django_social_share/templatetags/post_to_reddit.html', takes_context=True) +def post_to_reddit(context, title, obj_or_url=None, link_text='',link_class=''): + context = post_to_reddit_url(context, title, obj_or_url) + context['link_class'] = link_class + context['link_text'] = link_text or 'Post to Reddit' + return context + + +@register.simple_tag(takes_context=True) +def post_to_telegram_url(context, title, obj_or_url): + request = context['request'] + title = compile_text(context, title) + url = _build_url(request, obj_or_url) + context['telegram_url'] = mark_safe(TELEGRAM_ENDPOINT % (urlencode(title), urlencode(url))) + return context + + +@register.inclusion_tag('django_social_share/templatetags/post_to_telegram.html', takes_context=True) +def post_to_telegram(context, title, obj_or_url=None, link_text='',link_class=''): + context = post_to_telegram_url(context, title, obj_or_url) + context['link_class'] = link_class + context['link_text'] = link_text or 'Post to Telegram' + return context + + +@register.simple_tag(takes_context=True) +def post_to_whatsapp_url(context, obj_or_url=None): + request = context['request'] + url = _build_url(request, obj_or_url) + context['whatsapp_url'] = WHATSAPP_ENDPOINT % urlencode(url) + return context + + +@register.inclusion_tag('django_social_share/templatetags/post_to_whatsapp.html', takes_context=True) +def post_to_whatsapp(context, obj_or_url=None, link_text='',link_class=''): + context = post_to_whatsapp_url(context, obj_or_url) + context['link_class'] = link_class + context['link_text'] = link_text or 'Post to WhatsApp' + return context + + +@register.simple_tag(takes_context=True) +def save_to_pinterest_url(context, obj_or_url=None): + request = context['request'] + url = _build_url(request, obj_or_url) + context['pinterest_url'] = PINTEREST_ENDPOINT % urlencode(url) + return context + + +@register.inclusion_tag('django_social_share/templatetags/save_to_pinterest.html', takes_context=True) +def save_to_pinterest(context, obj_or_url=None, pin_count=False, link_class=''): + context = save_to_pinterest_url(context, obj_or_url) + context['link_class'] = link_class + context['pin_count'] = pin_count + return context + + +@register.inclusion_tag('django_social_share/templatetags/pinterest_script.html', takes_context=False) +def add_pinterest_script(): + pass + +@register.simple_tag(takes_context=True) +def copy_to_clipboard_url(context, obj_or_url=None): + request = context['request'] + url = _build_url(request, obj_or_url) + context['copy_url'] = url + return context + +@register.inclusion_tag('django_social_share/templatetags/copy_to_clipboard.html', takes_context=True) +def copy_to_clipboard(context, obj_or_url, link_text='', link_class=''): + context = copy_to_clipboard_url(context, obj_or_url) + + context['link_class'] = link_class + context['link_text'] = link_text or 'Copy to clipboard' + return context + +@register.inclusion_tag('django_social_share/templatetags/copy_script.html', takes_context=False) +def add_copy_script(): + pass \ No newline at end of file diff --git a/qgisfeedproject/qgisfeed/utils.py b/qgisfeedproject/qgisfeed/utils.py index c8832a1..a518016 100644 --- a/qgisfeedproject/qgisfeed/utils.py +++ b/qgisfeedproject/qgisfeed/utils.py @@ -7,6 +7,7 @@ from django.core.mail import EmailMultiAlternatives from django.core.mail import send_mail from django.contrib.gis.db.models import Model +import requests logger = logging.getLogger('qgisfeed.admin') QGISFEED_FROM_EMAIL = getattr(settings, 'QGISFEED_FROM_EMAIL', 'noreply@qgis.org') @@ -43,3 +44,53 @@ def get_field_max_length(ConfigurationModel: Model, field_name: str): return config.max_characters except ConfigurationModel.DoesNotExist: return 500 + + +def push_to_linkedin(title, content): + access_token = 'YOUR_ACCESS_TOKEN' + headers = { + 'Authorization': f'Bearer {access_token}', + 'Content-Type': 'application/json', + 'X-Restli-Protocol-Version': '2.0.0' + } + payload = { + "author": "urn:li:person:YOUR_PERSON_URN", + "lifecycleState": "PUBLISHED", + "specificContent": { + "com.linkedin.ugc.ShareContent": { + "shareCommentary": { + "text": title + content + }, + "shareMediaCategory": "NONE" + } + }, + "visibility": { + "com.linkedin.ugc.MemberNetworkVisibility": "PUBLIC" + } + } + response = requests.post('https://api.linkedin.com/v2/ugcPosts', headers=headers, json=payload) + return response.json() + + +def push_to_facebook(title, content): + access_token = 'YOUR_ACCESS_TOKEN' + page_id = 'YOUR_PAGE_ID' + url = f'https://graph.facebook.com/{page_id}/feed' + payload = { + 'message': title + content, + 'access_token': access_token + } + response = requests.post(url, data=payload) + return response.json() + + +def push_to_telegram(title, content): + bot_token = 'YOUR_BOT_TOKEN' + chat_id = 'YOUR_GROUP_CHAT_ID' + url = f'https://api.telegram.org/bot{bot_token}/sendMessage' + payload = { + 'chat_id': chat_id, + 'text': title + content + } + response = requests.post(url, data=payload) + return response.json() diff --git a/qgisfeedproject/templates/django_social_share/templatetags/copy_script.html b/qgisfeedproject/templates/django_social_share/templatetags/copy_script.html new file mode 100644 index 0000000..4599d47 --- /dev/null +++ b/qgisfeedproject/templates/django_social_share/templatetags/copy_script.html @@ -0,0 +1,44 @@ + diff --git a/qgisfeedproject/templates/django_social_share/templatetags/copy_to_clipboard.html b/qgisfeedproject/templates/django_social_share/templatetags/copy_to_clipboard.html new file mode 100644 index 0000000..3335a0a --- /dev/null +++ b/qgisfeedproject/templates/django_social_share/templatetags/copy_to_clipboard.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/qgisfeedproject/templates/django_social_share/templatetags/pinterest_script.html b/qgisfeedproject/templates/django_social_share/templatetags/pinterest_script.html new file mode 100644 index 0000000..a5eb223 --- /dev/null +++ b/qgisfeedproject/templates/django_social_share/templatetags/pinterest_script.html @@ -0,0 +1 @@ + diff --git a/qgisfeedproject/templates/django_social_share/templatetags/post_to_facebook.html b/qgisfeedproject/templates/django_social_share/templatetags/post_to_facebook.html new file mode 100644 index 0000000..3d6350d --- /dev/null +++ b/qgisfeedproject/templates/django_social_share/templatetags/post_to_facebook.html @@ -0,0 +1,5 @@ +
+ + {{link_text}} + +
diff --git a/qgisfeedproject/templates/django_social_share/templatetags/post_to_gplus.html b/qgisfeedproject/templates/django_social_share/templatetags/post_to_gplus.html new file mode 100644 index 0000000..c2b7a27 --- /dev/null +++ b/qgisfeedproject/templates/django_social_share/templatetags/post_to_gplus.html @@ -0,0 +1,3 @@ +
+ {{link_text}} +
diff --git a/qgisfeedproject/templates/django_social_share/templatetags/post_to_linkedin.html b/qgisfeedproject/templates/django_social_share/templatetags/post_to_linkedin.html new file mode 100644 index 0000000..a0a25cb --- /dev/null +++ b/qgisfeedproject/templates/django_social_share/templatetags/post_to_linkedin.html @@ -0,0 +1,16 @@ +{% load i18n social_share %}{% get_current_language as LANGUAGE_CODE %} + + + + Post to LinkedIn + + diff --git a/qgisfeedproject/templates/django_social_share/templatetags/post_to_reddit.html b/qgisfeedproject/templates/django_social_share/templatetags/post_to_reddit.html new file mode 100644 index 0000000..a40e073 --- /dev/null +++ b/qgisfeedproject/templates/django_social_share/templatetags/post_to_reddit.html @@ -0,0 +1,3 @@ +
+ {{link_text}} +
diff --git a/qgisfeedproject/templates/django_social_share/templatetags/post_to_telegram.html b/qgisfeedproject/templates/django_social_share/templatetags/post_to_telegram.html new file mode 100644 index 0000000..4fcb727 --- /dev/null +++ b/qgisfeedproject/templates/django_social_share/templatetags/post_to_telegram.html @@ -0,0 +1,3 @@ +
+ {{link_text}} +
diff --git a/qgisfeedproject/templates/django_social_share/templatetags/post_to_twitter.html b/qgisfeedproject/templates/django_social_share/templatetags/post_to_twitter.html new file mode 100644 index 0000000..a3d9e36 --- /dev/null +++ b/qgisfeedproject/templates/django_social_share/templatetags/post_to_twitter.html @@ -0,0 +1,3 @@ +
+ {{link_text}} +
diff --git a/qgisfeedproject/templates/django_social_share/templatetags/post_to_whatsapp.html b/qgisfeedproject/templates/django_social_share/templatetags/post_to_whatsapp.html new file mode 100644 index 0000000..69a5d1c --- /dev/null +++ b/qgisfeedproject/templates/django_social_share/templatetags/post_to_whatsapp.html @@ -0,0 +1,3 @@ +
+ {{link_text}} +
diff --git a/qgisfeedproject/templates/django_social_share/templatetags/save_to_pinterest.html b/qgisfeedproject/templates/django_social_share/templatetags/save_to_pinterest.html new file mode 100644 index 0000000..92be6d1 --- /dev/null +++ b/qgisfeedproject/templates/django_social_share/templatetags/save_to_pinterest.html @@ -0,0 +1,3 @@ + diff --git a/qgisfeedproject/templates/django_social_share/templatetags/send_email.html b/qgisfeedproject/templates/django_social_share/templatetags/send_email.html new file mode 100644 index 0000000..8337665 --- /dev/null +++ b/qgisfeedproject/templates/django_social_share/templatetags/send_email.html @@ -0,0 +1,3 @@ +
+ {{link_text}} +
From c24c16805587b6918c7482d86608a8e31ad6b99e Mon Sep 17 00:00:00 2001 From: Lova ANDRIARIMALALA <43842786+Xpirix@users.noreply.github.com> Date: Mon, 17 Jun 2024 20:52:30 +0300 Subject: [PATCH 2/8] Add icons to social share --- .../templatetags/post_to_facebook.html | 10 +++++----- .../templatetags/post_to_linkedin.html | 13 +++---------- .../templatetags/post_to_reddit.html | 8 +++++--- .../templatetags/post_to_telegram.html | 8 +++++--- .../templatetags/send_email.html | 8 +++++--- 5 files changed, 23 insertions(+), 24 deletions(-) diff --git a/qgisfeedproject/templates/django_social_share/templatetags/post_to_facebook.html b/qgisfeedproject/templates/django_social_share/templatetags/post_to_facebook.html index 3d6350d..54ae0d9 100644 --- a/qgisfeedproject/templates/django_social_share/templatetags/post_to_facebook.html +++ b/qgisfeedproject/templates/django_social_share/templatetags/post_to_facebook.html @@ -1,5 +1,5 @@ -
- - {{link_text}} - -
+ + + + + diff --git a/qgisfeedproject/templates/django_social_share/templatetags/post_to_linkedin.html b/qgisfeedproject/templates/django_social_share/templatetags/post_to_linkedin.html index a0a25cb..4e99172 100644 --- a/qgisfeedproject/templates/django_social_share/templatetags/post_to_linkedin.html +++ b/qgisfeedproject/templates/django_social_share/templatetags/post_to_linkedin.html @@ -1,16 +1,9 @@ -{% load i18n social_share %}{% get_current_language as LANGUAGE_CODE %} - - - Post to LinkedIn + + + - diff --git a/qgisfeedproject/templates/django_social_share/templatetags/post_to_reddit.html b/qgisfeedproject/templates/django_social_share/templatetags/post_to_reddit.html index a40e073..21049f4 100644 --- a/qgisfeedproject/templates/django_social_share/templatetags/post_to_reddit.html +++ b/qgisfeedproject/templates/django_social_share/templatetags/post_to_reddit.html @@ -1,3 +1,5 @@ -
- {{link_text}} -
+ + + + + \ No newline at end of file diff --git a/qgisfeedproject/templates/django_social_share/templatetags/post_to_telegram.html b/qgisfeedproject/templates/django_social_share/templatetags/post_to_telegram.html index 4fcb727..de49140 100644 --- a/qgisfeedproject/templates/django_social_share/templatetags/post_to_telegram.html +++ b/qgisfeedproject/templates/django_social_share/templatetags/post_to_telegram.html @@ -1,3 +1,5 @@ -
- {{link_text}} -
+ + + + + diff --git a/qgisfeedproject/templates/django_social_share/templatetags/send_email.html b/qgisfeedproject/templates/django_social_share/templatetags/send_email.html index 8337665..13f4567 100644 --- a/qgisfeedproject/templates/django_social_share/templatetags/send_email.html +++ b/qgisfeedproject/templates/django_social_share/templatetags/send_email.html @@ -1,3 +1,5 @@ -
- {{link_text}} -
+ + + + + From cbe6a715cc93d4b82910b4fea6bd4c6f9964529e Mon Sep 17 00:00:00 2001 From: Lova ANDRIARIMALALA <43842786+Xpirix@users.noreply.github.com> Date: Tue, 18 Jun 2024 15:45:42 +0300 Subject: [PATCH 3/8] Add item detail page --- qgisfeedproject/qgisfeed/forms.py | 21 ++ qgisfeedproject/qgisfeed/urls.py | 1 + qgisfeedproject/qgisfeed/views.py | 25 +- .../templates/feeds/feed_home_page.html | 11 +- .../templates/feeds/feed_item_detail.html | 238 ++++++++++++++++++ 5 files changed, 294 insertions(+), 2 deletions(-) create mode 100644 qgisfeedproject/templates/feeds/feed_item_detail.html diff --git a/qgisfeedproject/qgisfeed/forms.py b/qgisfeedproject/qgisfeed/forms.py index 61732e8..942438c 100644 --- a/qgisfeedproject/qgisfeed/forms.py +++ b/qgisfeedproject/qgisfeed/forms.py @@ -5,6 +5,7 @@ from .languages import LANGUAGES from django.utils import timezone from django.contrib.auth.models import User +import json class HomePageFilterForm(forms.Form): """ @@ -165,3 +166,23 @@ def get_approvers_choices(self): email__isnull=False ).exclude(email='') if u.has_perm("qgisfeed.publish_qgisfeedentry") ) + + +class ReadOnlyFeedItemForm(forms.ModelForm): + class Meta: + model = QgisFeedEntry + fields = ['spatial_filter'] + + def __init__(self, *args, **kwargs): + super(ReadOnlyFeedItemForm, self).__init__(*args, **kwargs) + self.fields['spatial_filter'].widget = MapWidget(attrs={ + 'geom_type': 'Polygon', + 'default_lat': 0, + 'default_lon': 0, + 'default_zoom': 2, + 'map_options': json.dumps({ + 'editable': False, + 'readonly': True, + 'draggable': False, + }) + }) diff --git a/qgisfeedproject/qgisfeed/urls.py b/qgisfeedproject/qgisfeed/urls.py index 2cd71ed..a7e1ac4 100644 --- a/qgisfeedproject/qgisfeed/urls.py +++ b/qgisfeedproject/qgisfeed/urls.py @@ -7,4 +7,5 @@ path('manage/', views.FeedsListView.as_view(), name='feeds_list'), path('manage/add/', views.FeedEntryAddView.as_view(), name='feed_entry_add'), path('manage/update//', views.FeedEntryUpdateView.as_view(), name='feed_entry_update'), + path('/', views.FeedEntryDetailView.as_view(), name='feed_entry_detail'), ] diff --git a/qgisfeedproject/qgisfeed/views.py b/qgisfeedproject/qgisfeed/views.py index 511489c..d6c7181 100644 --- a/qgisfeedproject/qgisfeed/views.py +++ b/qgisfeedproject/qgisfeed/views.py @@ -19,6 +19,7 @@ from django.contrib.gis.geos import GEOSGeometry from django.http import HttpResponse, HttpResponseBadRequest from django.views import View +from django.views.generic import DetailView from django.db.models import Q from django.contrib.auth.decorators import login_required, user_passes_test from django.shortcuts import render, redirect, get_object_or_404 @@ -28,7 +29,7 @@ from django.db import transaction from django.contrib.auth.models import User -from .forms import FeedEntryFilterForm, FeedItemForm, HomePageFilterForm +from .forms import FeedEntryFilterForm, FeedItemForm, HomePageFilterForm, ReadOnlyFeedItemForm from .utils import get_field_max_length, notify_reviewers from .models import QgisFeedEntry, CharacterLimitConfiguration from .languages import LANGUAGE_KEYS @@ -397,3 +398,25 @@ def post(self, request, pk): } return render(request, self.template_name, args) + + +class FeedEntryDetailView(DetailView): + """ + View to get a feed entry item + """ + model = QgisFeedEntry + template_name = 'feeds/feed_item_detail.html' + context_object_name = 'feed_entry' + form_class = ReadOnlyFeedItemForm + + def get(self, request, pk): + feed_entry = get_object_or_404(QgisFeedEntry, pk=pk) + form = self.form_class(instance=feed_entry) + spatial_filter_geojson = feed_entry.spatial_filter.geojson if feed_entry.spatial_filter else 0 + args = { + "form": form, + "feed_entry": feed_entry, + "spatial_filter_geojson": spatial_filter_geojson + } + + return render(request, self.template_name, args) diff --git a/qgisfeedproject/templates/feeds/feed_home_page.html b/qgisfeedproject/templates/feeds/feed_home_page.html index ba486a8..863b88d 100644 --- a/qgisfeedproject/templates/feeds/feed_home_page.html +++ b/qgisfeedproject/templates/feeds/feed_home_page.html @@ -112,13 +112,22 @@
Filter by
{% endif %} -
+
{{feed.title | default:""}}
{{feed.content | default:"" | safe }}
+
{% endfor %} diff --git a/qgisfeedproject/templates/feeds/feed_item_detail.html b/qgisfeedproject/templates/feeds/feed_item_detail.html new file mode 100644 index 0000000..8d9bf3e --- /dev/null +++ b/qgisfeedproject/templates/feeds/feed_item_detail.html @@ -0,0 +1,238 @@ +{% extends 'layouts/base-fullscreen.html' %} +{% load social_share %} +{% block title %} QGIS Home Page News Feed {% endblock title %} {% load static %} + + +{% block stylesheets %} + +{% endblock stylesheets %} + + +{% block content %} + + {{ form.media }} + +
+ +
+
+
+
+

+ {{feed_entry.title | default:""}} +

+
+
+
+
+ +
+
+
+
+
+ +
+
+ +
+
+ {% if feed_entry.image %} + + {% else %} + {% endif %} +
+
+
+ {{feed_entry.title | default:""}} +
+
+ {{feed_entry.content | default:"" | safe }} +
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
Url: + {% if feed_entry.url %} + {{ feed_entry.url }} + {% else %} + - + {% endif %} +
Sticky: + {% if feed_entry.sticky %} + + + + {% else %} + + + + {% endif %} +
Sorting order: + {{ feed_entry.sorting|default:"-" }} +
Language filter: + {{ feed_entry.language_filter|default:"-" }} +
Publication start: + {{ feed_entry.publish_from|date:"d F Y, H:i"|default:"-" }} +
Publication end: + {{ feed_entry.publish_to|date:"d F Y, H:i"|default:"-" }} +
+
+
+
+
+
+ + Share this: + +
+ + {% post_to_facebook object_or_url link_class="button is-large is-light" %} + {% post_to_linkedin object_or_url link_class="button is-large is-light" %} + {% post_to_telegram "{{feed_entry.title}} \n {{feed_entry.content}}" object_or_url link_class="button is-large is-light" %} + {% post_to_reddit feed_entry.title object_or_url link_class="button is-large is-light" %} + {% send_email feed_entry.title feed_entry.content object_or_url link_class="button is-large is-light" %} +
+
+
+
+ +{% endblock content %} + + + +{% block javascripts %} + +{% endblock javascripts %} \ No newline at end of file From 510873c33a1b9a99694cc8bb14b81d7bdca17c3d Mon Sep 17 00:00:00 2001 From: Lova ANDRIARIMALALA <43842786+Xpirix@users.noreply.github.com> Date: Tue, 18 Jun 2024 16:11:23 +0300 Subject: [PATCH 4/8] Fix linkedin share feature --- qgisfeedproject/qgisfeed/templatetags/social_share.py | 4 ++-- .../django_social_share/templatetags/post_to_linkedin.html | 2 +- qgisfeedproject/templates/feeds/feed_item_detail.html | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/qgisfeedproject/qgisfeed/templatetags/social_share.py b/qgisfeedproject/qgisfeed/templatetags/social_share.py index ceee3ff..0a3f38a 100644 --- a/qgisfeedproject/qgisfeed/templatetags/social_share.py +++ b/qgisfeedproject/qgisfeed/templatetags/social_share.py @@ -30,7 +30,7 @@ FACEBOOK_ENDPOINT = 'https://www.facebook.com/sharer/sharer.php?u=%s' GPLUS_ENDPOINT = 'https://plus.google.com/share?url=%s' MAIL_ENDPOINT = 'mailto:?subject=%s&body=%s' -LINKEDIN_ENDPOINT = 'https://www.linkedin.com/shareArticle?mini=true&title=%s&url=%s' +LINKEDIN_ENDPOINT = 'https://www.linkedin.com/shareArticle?mini=true&title=hello&url=%s' REDDIT_ENDPOINT = 'https://www.reddit.com/submit?title=%s&url=%s' TELEGRAM_ENDPOINT = 'https://t.me/share/url?text=%s&url=%s' WHATSAPP_ENDPOINT = 'https://api.whatsapp.com/send?text=%s' @@ -159,7 +159,7 @@ def linkedin_locale(value): def post_to_linkedin_url(context, obj_or_url=None): request = context['request'] url = _build_url(request, obj_or_url) - context['linkedin_url'] = url + context['linkedin_url'] = LINKEDIN_ENDPOINT % urlencode(url) return context diff --git a/qgisfeedproject/templates/django_social_share/templatetags/post_to_linkedin.html b/qgisfeedproject/templates/django_social_share/templatetags/post_to_linkedin.html index 4e99172..2be5182 100644 --- a/qgisfeedproject/templates/django_social_share/templatetags/post_to_linkedin.html +++ b/qgisfeedproject/templates/django_social_share/templatetags/post_to_linkedin.html @@ -1,5 +1,5 @@ diff --git a/qgisfeedproject/templates/feeds/feed_item_detail.html b/qgisfeedproject/templates/feeds/feed_item_detail.html index 8d9bf3e..8d45ec9 100644 --- a/qgisfeedproject/templates/feeds/feed_item_detail.html +++ b/qgisfeedproject/templates/feeds/feed_item_detail.html @@ -149,10 +149,10 @@
Share this:
- + {% post_to_facebook object_or_url link_class="button is-large is-light" %} {% post_to_linkedin object_or_url link_class="button is-large is-light" %} - {% post_to_telegram "{{feed_entry.title}} \n {{feed_entry.content}}" object_or_url link_class="button is-large is-light" %} + {% post_to_telegram "{{feed_entry.title}}: {{feed_entry.content}}" object_or_url link_class="button is-large is-light" %} {% post_to_reddit feed_entry.title object_or_url link_class="button is-large is-light" %} {% send_email feed_entry.title feed_entry.content object_or_url link_class="button is-large is-light" %}
From abfce92d50fb1c9d6b3971de9d32fed72b40d81a Mon Sep 17 00:00:00 2001 From: Lova ANDRIARIMALALA <43842786+Xpirix@users.noreply.github.com> Date: Tue, 18 Jun 2024 16:37:59 +0300 Subject: [PATCH 5/8] Split tests file into multiple files --- qgisfeedproject/qgisfeed/tests.py | 615 ------------------ qgisfeedproject/qgisfeed/tests/__init__.py | 1 + .../qgisfeed/tests/test_feed_entry.py | 220 +++++++ .../qgisfeed/tests/test_home_page.py | 53 ++ qgisfeedproject/qgisfeed/tests/test_login.py | 19 + .../qgisfeed/tests/test_user_visit.py | 58 ++ qgisfeedproject/qgisfeed/tests/test_views.py | 293 +++++++++ 7 files changed, 644 insertions(+), 615 deletions(-) delete mode 100644 qgisfeedproject/qgisfeed/tests.py create mode 100644 qgisfeedproject/qgisfeed/tests/__init__.py create mode 100644 qgisfeedproject/qgisfeed/tests/test_feed_entry.py create mode 100644 qgisfeedproject/qgisfeed/tests/test_home_page.py create mode 100644 qgisfeedproject/qgisfeed/tests/test_login.py create mode 100644 qgisfeedproject/qgisfeed/tests/test_user_visit.py create mode 100644 qgisfeedproject/qgisfeed/tests/test_views.py diff --git a/qgisfeedproject/qgisfeed/tests.py b/qgisfeedproject/qgisfeed/tests.py deleted file mode 100644 index 29a9e04..0000000 --- a/qgisfeedproject/qgisfeed/tests.py +++ /dev/null @@ -1,615 +0,0 @@ -# coding=utf-8 -""""Tests for QGIS Welcome Page News Feed requests - -.. note:: This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - -""" - -__author__ = 'elpaso@itopen.it' -__date__ = '2019-05-07' -__copyright__ = 'Copyright 2019, ItOpen' - -import json - -from django.test import TestCase -from django.test import Client -from django.contrib.auth.models import Group, User -from django.contrib.admin.sites import AdminSite -from django.utils import timezone -from django.core.paginator import Page -from django.urls import reverse -from django.contrib.gis.geos import Polygon -from django.core.files.uploadedfile import SimpleUploadedFile -from django.conf import settings -from django.db import connection -from django.core import mail - -from .utils import get_field_max_length - -from .models import ( - CharacterLimitConfiguration, QgisFeedEntry, QgisUserVisit, DailyQgisUserVisit, aggregate_user_visit_data -) -from .admin import QgisFeedEntryAdmin - -from os.path import join - -class MockRequest: - - def build_absolute_uri(self, uri): - return uri - -class MockSuperUser: - - def is_superuser(self): - return True - - def has_perm(self, perm): - return True - -class MockStaff: - - def is_superuser(self): - return False - - def is_staff(self): - return True - - def has_perm(self, perm): - return True - - -request = MockRequest() - - -class QgisFeedEntryTestCase(TestCase): - fixtures = ['qgisfeed.json', 'users.json'] - - def setUp(self): - pass - - def test_sorting(self): - c = Client(HTTP_USER_AGENT='Mozilla/5.0 QGIS/32400/Fedora ' - 'Linux (Workstation Edition)') - response = c.get('/') - data = json.loads(response.content) - data[0]['title'] = "Next Microsoft Windows code name revealed" - - def test_unpublished(self): - c = Client(HTTP_USER_AGENT='Mozilla/5.0 QGIS/32400/Fedora ' - 'Linux (Workstation Edition)') - response = c.get('/') - data = json.loads(response.content) - titles = [d['title'] for d in data] - self.assertFalse("QGIS core will be rewritten in FORTRAN" in titles) - - def test_published(self): - c = Client(HTTP_USER_AGENT='Mozilla/5.0 QGIS/32400/Fedora ' - 'Linux (Workstation Edition)') - response = c.get('/') - data = json.loads(response.content) - titles = [d['title'] for d in data] - self.assertTrue("QGIS core will be rewritten in Rust" in titles) - - def test_expired(self): - c = Client(HTTP_USER_AGENT='Mozilla/5.0 QGIS/32400/Fedora ' - 'Linux (Workstation Edition)') - response = c.get('/') - data = json.loads(response.content) - titles = [d['title'] for d in data] - self.assertFalse("QGIS core will be rewritten in PASCAL" in titles) - self.assertFalse("QGIS core will be rewritten in GO" in titles) - - def test_future(self): - c = Client(HTTP_USER_AGENT='Mozilla/5.0 QGIS/32400/Fedora ' - 'Linux (Workstation Edition)') - response = c.get('/') - data = json.loads(response.content) - titles = [d['title'] for d in data] - self.assertFalse("QGIS core will be rewritten in BASIC" in titles) - - def test_lang_filter(self): - c = Client(HTTP_USER_AGENT='Mozilla/5.0 QGIS/32400/Fedora ' - 'Linux (Workstation Edition)') - response = c.get('/?lang=fr') - data = json.loads(response.content) - titles = [d['title'] for d in data] - self.assertFalse("Null Island QGIS Meeting" in titles) - self.assertTrue("QGIS acquired by ESRI" in titles) - - c = Client(HTTP_USER_AGENT='Mozilla/5.0 QGIS/32400/Fedora ' - 'Linux (Workstation Edition)') - response = c.get('/?lang=en') - data = json.loads(response.content) - titles = [d['title'] for d in data] - self.assertTrue("Null Island QGIS Meeting" in titles) - self.assertTrue("QGIS acquired by ESRI" in titles) - - def test_lat_lon_filter(self): - c = Client(HTTP_USER_AGENT='Mozilla/5.0 QGIS/32400/Fedora ' - 'Linux (Workstation Edition)') - response = c.get('/?lat=0&lon=0') - data = json.loads(response.content) - titles = [d['title'] for d in data] - self.assertTrue("Null Island QGIS Meeting" in titles) - self.assertFalse("QGIS Italian Meeting" in titles) - - response = c.get('/?lat=44.5&lon=9.5') - data = json.loads(response.content) - titles = [d['title'] for d in data] - self.assertFalse("Null Island QGIS Meeting" in titles) - self.assertTrue("QGIS Italian Meeting" in titles) - - def test_after(self): - c = Client(HTTP_USER_AGENT='Mozilla/5.0 QGIS/32400/Fedora ' - 'Linux (Workstation Edition)') - response = c.get('/?after=%s' % timezone.datetime(2019, 5, 9).timestamp()) - data = json.loads(response.content) - titles = [d['title'] for d in data] - self.assertFalse("Null Island QGIS Meeting" in titles) - self.assertTrue("QGIS Italian Meeting" in titles) - - # Check that an updated entry is added to the feed even if - # expired, but only with QGIS >= 3.36 - with connection.cursor() as cursor: - cursor.execute("UPDATE qgisfeed_qgisfeedentry SET publish_to='2019-04-09', modified = '2019-05-10', title='Null Island QGIS Hackfest' WHERE title='Null Island QGIS Meeting'") - - response = c.get('/?after=%s' % timezone.datetime(2019, 5, 9).timestamp()) - titles = [d['title'] for d in data] - self.assertFalse("Null Island QGIS Meeting" in titles) - - c = Client(HTTP_USER_AGENT='Mozilla/5.0 QGIS/33600/Fedora ' - 'Linux (Workstation Edition)') - response = c.get('/?after=%s' % timezone.datetime(2019, 5, 9).timestamp()) - data = json.loads(response.content) - null_island = [d for d in data if d['title'] == "Null Island QGIS Hackfest"][0] - self.assertTrue(timezone.datetime(2019, 5, 9).timestamp() > null_island['publish_to']) - - - def test_invalid_parameters(self): - c = Client(HTTP_USER_AGENT='Mozilla/5.0 QGIS/32400/Fedora ' - 'Linux (Workstation Edition)') - response = c.get('/?lat=ZZ&lon=KK') - self.assertEqual(response.status_code, 400) - response = c.get('/?lang=KK') - self.assertEqual(response.status_code, 400) - - def test_image_link(self): - c = Client(HTTP_USER_AGENT='Mozilla/5.0 QGIS/32400/Fedora ' - 'Linux (Workstation Edition)') - response = c.get('/') - data = json.loads(response.content) - image = [d['image'] for d in data if d['image'] != ""][0] - self.assertEqual(image, "http://testserver/media/feedimages/rust.png" ) - - def test_sticky(self): - c = Client(HTTP_USER_AGENT='Mozilla/5.0 QGIS/32400/Fedora ' - 'Linux (Workstation Edition)') - response = c.get('/') - data = json.loads(response.content) - sticky = data[0] - self.assertTrue(sticky['sticky']) - not_sticky = data[-1] - self.assertFalse(not_sticky['sticky']) - - def test_group_is_created(self): - self.assertEqual(Group.objects.filter(name='qgisfeedentry_authors').count(), 1) - perms = sorted([p.codename for p in Group.objects.get(name='qgisfeedentry_authors').permissions.all()]) - self.assertEqual(perms, ['add_qgisfeedentry', 'view_qgisfeedentry']) - # Create a staff user and verify - staff = User(username='staff_user', is_staff=True) - staff.save() - self.assertIsNotNone(staff.groups.get(name='qgisfeedentry_authors')) - self.assertEqual(staff.get_all_permissions(), set(('qgisfeed.add_qgisfeedentry', 'qgisfeed.view_qgisfeedentry'))) - - def test_admin_publish_from(self): - """Test that published entries have publish_from set""" - - site = AdminSite() - ma = QgisFeedEntryAdmin(QgisFeedEntry, site) - obj = QgisFeedEntry(title='Test entry') - request.user = User.objects.get(username='admin') - form = ma.get_form(request, obj) - ma.save_model(request, obj, form, False) - self.assertIsNone(obj.publish_from) - self.assertFalse(obj.published) - obj.published = True - ma.save_model(request, obj, form, True) - self.assertIsNotNone(obj.publish_from) - self.assertTrue(obj.published) - - def test_admin_author_is_set(self): - site = AdminSite() - ma = QgisFeedEntryAdmin(QgisFeedEntry, site) - obj = QgisFeedEntry(title='Test entry 2') - request.user = User.objects.get(username='staff') - form = ma.get_form(request, obj) - ma.save_model(request, obj, form, False) - self.assertEqual(obj.author, request.user) - - - -class HomePageTestCase(TestCase): - """ - Test home page web version - """ - fixtures = ['qgisfeed.json', 'users.json'] - - def setUp(self): - pass - - def test_authenticated_user_access(self): - self.client.login(username='admin', password='admin') - - # Access the all view after logging in - response = self.client.get(reverse('all')) - - # Check if the response status code is 200 (OK) - self.assertEqual(response.status_code, 200) - - # Check if the correct template is used - self.assertTemplateUsed(response, 'feeds/feed_home_page.html') - self.assertTrue('form' in response.context) - - - def test_unauthenticated_user_access(self): - # Access the all view without logging in - response = self.client.get(reverse('all')) - - # Check if the response status code is 200 (OK) - self.assertEqual(response.status_code, 200) - - # Check if the correct template is used - self.assertTemplateUsed(response, 'feeds/feed_home_page.html') - self.assertTrue('form' in response.context) - - def test_feeds_list_filtering(self): - # Test filter homepage feeds - - data = { - 'lang': 'en', - 'publish_from': '2023-12-31', - } - response = self.client.get(reverse('all'), data) - - # Check if the response status code is 200 (OK) - self.assertEqual(response.status_code, 200) - - # Check if the correct template is used - self.assertTemplateUsed(response, 'feeds/feed_home_page.html') - self.assertTrue('form' in response.context) - - -class QgisUserVisitTestCase(TestCase): - - def test_user_visit(self): - c = Client(HTTP_USER_AGENT='Mozilla/5.0 QGIS/32400/Fedora ' - 'Linux (Workstation Edition)') - c.get('/') - user_visit = QgisUserVisit.objects.filter( - platform__icontains='Fedora Linux (Workstation Edition)') - self.assertEqual(user_visit.count(), 1) - self.assertEqual(user_visit.first().qgis_version, '32400') - - def test_ip_address_removed(self): - c = Client(HTTP_USER_AGENT='Mozilla/5.0 QGIS/32400/Fedora ' - 'Linux (Workstation Edition)', - REMOTE_ADDR='180.247.213.170') - c.get('/') - qgis_visit = QgisUserVisit.objects.first() - self.assertTrue(qgis_visit.user_visit.remote_addr == '') - self.assertTrue(qgis_visit.location['country_name'] == 'Indonesia') - - def test_aggregate_visit(self): - c = Client(HTTP_USER_AGENT='Mozilla/5.0 QGIS/31400/Fedora ' - 'Linux (Workstation Edition)', - REMOTE_ADDR='180.247.213.170') - c.get('/') - c = Client(HTTP_USER_AGENT='Mozilla/5.0 QGIS/32400/Windows 10', - REMOTE_ADDR='180.247.213.160') - c.get('/') - c = Client(HTTP_USER_AGENT='Mozilla/5.0 QGIS/32400/Windows XP', - REMOTE_ADDR='180.247.213.160') - c.get('/') - aggregate_user_visit_data() - daily_visit = DailyQgisUserVisit.objects.first() - self.assertTrue(daily_visit.platform['Windows 10'] == 1) - self.assertTrue(daily_visit.qgis_version['32400'] == 2) - self.assertTrue(daily_visit.country['ID'] == 3) - - -class LoginTestCase(TestCase): - """ - Test the login feature - """ - fixtures = ['qgisfeed.json', 'users.json'] - - def setUp(self): - self.client = Client() - - def test_valid_login(self): - response = self.client.login(username='admin', password='admin') - self.assertTrue(response) - - def test_invalid_login(self): - response = self.client.login(username='admin', password='wrongpassword') - self.assertFalse(response) - -class FeedsListViewTestCase(TestCase): - """ - Test the feeds list feature - """ - fixtures = ['qgisfeed.json', 'users.json'] - def setUp(self): - self.client = Client() - - def test_authenticated_user_access(self): - self.client.login(username='admin', password='admin') - - # Access the feeds_list view after logging in - response = self.client.get(reverse('feeds_list')) - - # Check if the response status code is 200 (OK) - self.assertEqual(response.status_code, 200) - - # Check if the correct template is used - self.assertTemplateUsed(response, 'feeds/feeds_list.html') - - def test_unauthenticated_user_redirect_to_login(self): - # Access the feeds_list view without logging in - response = self.client.get(reverse('feeds_list')) - - # Check if the response status code is 302 (Redirect) - self.assertEqual(response.status_code, 302) - - # Check if the user is redirected to the login page - self.assertRedirects(response, reverse('login') + '?next=' + reverse('feeds_list')) - - - def test_nonstaff_user_redirect_to_login(self): - user = User.objects.create_user(username='testuser', password='testpassword') - self.client.login(username='testuser', password='testpassword') - # Access the feeds_list view with a non staff user - response = self.client.get(reverse('feeds_list')) - - # Check if the response status code is 302 (Redirect) - self.assertEqual(response.status_code, 302) - - # Check if the user is redirected to the login page - self.assertRedirects(response, reverse('login') + '?next=' + reverse('feeds_list')) - - def test_feeds_list_filtering(self): - self.client.login(username='admin', password='admin') - # Simulate a GET request with filter parameters - data = { - 'title': 'QGIS', - 'author': 'admin', - 'language_filter': 'en', - 'publish_from': '2019-01-01', - 'publish_to': '2023-12-31', - 'sort_by': 'title', - 'order': 'asc', - } - response = self.client.get(reverse('feeds_list'), data) - - # Check that the response status code is 200 (OK) - self.assertEqual(response.status_code, 200) - - # Check that the response contains the expected context data - self.assertTrue('feeds_entry' in response.context) - self.assertTrue(isinstance(response.context['feeds_entry'], Page)) - self.assertTrue('sort_by' in response.context) - self.assertTrue('order' in response.context) - self.assertTrue('current_order' in response.context) - self.assertTrue('form' in response.context) - self.assertTrue('count' in response.context) - -class FeedsItemFormTestCase(TestCase): - """ - Test the feeds add/update feature - """ - fixtures = ['qgisfeed.json', 'users.json'] - def setUp(self): - self.client = Client() - spatial_filter = Polygon(((0, 0), (0, 1), (1, 1), (1, 0), (0, 0))) - image_path = join(settings.MEDIA_ROOT, "feedimages", "rust.png") - self.post_data = { - 'title': 'QGIS core will be rewritten in Rust', - 'image': open(image_path, "rb"), - 'content': '

Tired with C++ intricacies, the core developers have decided to rewrite QGIS in Rust', - 'url': 'https://www.null.com', - 'sticky': False, - 'sorting': 0, - 'language_filter': 'en', - 'spatial_filter': str(spatial_filter), - 'publish_from': '2023-10-18 14:46:00+00', - 'publish_to': '2023-10-29 14:46:00+00' - } - - - def test_authenticated_user_access(self): - self.client.login(username='admin', password='admin') - - # Access the feed_entry_add view after logging in - response = self.client.get(reverse('feed_entry_add')) - self.assertEqual(response.status_code, 200) - self.assertTemplateUsed(response, 'feeds/feed_item_form.html') - self.assertTrue('form' in response.context) - - # Check if the approver has the permission. - # Here, only the the admin user is listed. - approvers = response.context['form']['approvers'] - self.assertEqual(len(approvers), 1) - approver_id = int(approvers[0].data['value']) - approver = User.objects.get(pk=approver_id) - self.assertTrue(approver.has_perm("qgisfeed.publish_qgisfeedentry")) - - # Access the feed_entry_update view after logging in - response = self.client.get(reverse('feed_entry_update', args=[3])) - self.assertEqual(response.status_code, 200) - self.assertTemplateUsed(response, 'feeds/feed_item_form.html') - self.assertTrue('form' in response.context) - - def test_unauthenticated_user_redirect_to_login(self): - # Access the feed_entry_add view without logging in - response = self.client.get(reverse('feed_entry_add')) - self.assertEqual(response.status_code, 302) - self.assertRedirects(response, reverse('login') + '?next=' + reverse('feed_entry_add')) - self.assertIsNone(response.context) - - # Access the feed_entry_update view without logging in - response = self.client.get(reverse('feed_entry_update', args=[3])) - self.assertEqual(response.status_code, 302) - self.assertRedirects(response, reverse('login') + '?next=' + reverse('feed_entry_update', args=[3])) - self.assertIsNone(response.context) - - def test_nonstaff_user_redirect_to_login(self): - user = User.objects.create_user(username='testuser', password='testpassword') - self.client.login(username='testuser', password='testpassword') - - # Access the feed_entry_add view with a non staff user - response = self.client.get(reverse('feed_entry_add')) - self.assertEqual(response.status_code, 302) - self.assertRedirects(response, reverse('login') + '?next=' + reverse('feed_entry_add')) - self.assertIsNone(response.context) - - # Access the feed_entry_add view with a non staff user - response = self.client.get(reverse('feed_entry_update', args=[3])) - self.assertEqual(response.status_code, 302) - self.assertRedirects(response, reverse('login') + '?next=' + reverse('feed_entry_update', args=[3])) - self.assertIsNone(response.context) - - def test_authenticated_user_add_feed(self): - # Add a feed entry test - self.client.login(username='staff', password='staff') - - response = self.client.post(reverse('feed_entry_add'), data=self.post_data) - self.assertEqual(response.status_code, 302) - self.assertRedirects(response, reverse('feeds_list')) - - - def test_authenticated_user_update_feed(self): - # Update a feed entry test - self.client.login(username='admin', password='admin') - - response = self.client.post(reverse('feed_entry_update', args=[3]), data=self.post_data) - self.assertEqual(response.status_code, 302) - self.assertRedirects(response, reverse('feeds_list')) - - - def test_not_allowed_user_update_feed(self): - # Update a feed entry with a non allowed user - self.client.login(username='staff', password='staff') - - response = self.client.post(reverse('feed_entry_update', args=[7]), data=self.post_data) - self.assertEqual(response.status_code, 302) - self.assertRedirects(response, reverse('login') + '?next=' + reverse('feed_entry_update', args=[7])) - self.assertIsNone(response.context) - - def test_allowed_user_publish_feed(self): - # Publish a feed entry test - self.client.login(username='admin', password='admin') - self.post_data['publish'] = 1 - response = self.client.post(reverse('feed_entry_update', args=[7]), data=self.post_data) - self.assertEqual(response.status_code, 302) - self.assertRedirects(response, reverse('feeds_list')) - - updated_data = QgisFeedEntry.objects.get(pk=7) - self.assertTrue(updated_data.published) - - def test_allowed_staff_publish_feed(self): - # Update a feed entry with an allowed staff user - user = User.objects.get(username='staff') - user.save() - group = Group.objects.get(name='qgisfeedentry_approver') - group.user_set.add(user) - - self.client.login(username='staff', password='staff') - self.post_data['publish'] = 1 - response = self.client.post(reverse('feed_entry_update', args=[7]), data=self.post_data) - self.assertEqual(response.status_code, 302) - self.assertRedirects(response, reverse('feeds_list')) - - updated_data = QgisFeedEntry.objects.get(pk=7) - self.assertTrue(updated_data.published) - - def test_allowed_staff_unpublish_feed(self): - # Update a feed entry with an allowed staff user - user = User.objects.get(username='staff') - user.save() - group = Group.objects.get(name='qgisfeedentry_approver') - group.user_set.add(user) - - self.client.login(username='staff', password='staff') - self.post_data['publish'] = 0 - - response = self.client.post(reverse('feed_entry_update', args=[7]), data=self.post_data) - self.assertEqual(response.status_code, 302) - self.assertRedirects(response, reverse('feeds_list')) - - updated_data = QgisFeedEntry.objects.get(pk=7) - self.assertFalse(updated_data.published) - - def test_authenticated_user_add_invalid_data(self): - # Add a feed entry that contains invalid data - self.client.login(username='staff', password='staff') - spatial_filter = Polygon(((0, 0), (0, 1), (1, 1), (1, 0), (0, 0))) - image_path = join(settings.MEDIA_ROOT, "feedimages", "rust.png") - - # Limit content value to 10 characters - config, created = CharacterLimitConfiguration.objects.update_or_create( - field_name="content", - max_characters=10 - ) - - post_data = { - 'title': '', - 'image': open(image_path, "rb"), - 'content': '

Tired with C++ intricacies, the core developers have decided to rewrite QGIS in Rust', - 'url': '', - 'sticky': False, - 'sorting': 0, - 'language_filter': 'en', - 'spatial_filter': str(spatial_filter), - 'publish_from': '', - 'publish_to': '' - } - - response = self.client.post(reverse('feed_entry_add'), data=post_data) - self.assertEqual(response.status_code, 200) - form = response.context['form'] - self.assertIn('title', form.errors, "This field is required.") - self.assertIn('content', form.errors, "Ensure this value has at most 10 characters (it has 104).") - - def test_get_field_max_length(self): - # Test the get_field_max_length function - content_max_length = get_field_max_length(CharacterLimitConfiguration, field_name="content") - self.assertEqual(content_max_length, 500) - CharacterLimitConfiguration.objects.create( - field_name="content", - max_characters=1000 - ) - content_max_length = get_field_max_length(CharacterLimitConfiguration, field_name="content") - self.assertEqual(content_max_length, 1000) - - def test_add_feed_with_reviewer(self): - # Add a feed entry with specified reviewer test - self.client.login(username='staff', password='staff') - self.post_data['reviewers'] = [1] - - response = self.client.post(reverse('feed_entry_add'), data=self.post_data) - self.assertEqual(response.status_code, 302) - self.assertRedirects(response, reverse('feeds_list')) - - self.assertEqual( - mail.outbox[0].recipients(), - ['me@email.com'] - ) - - self.assertEqual( - mail.outbox[0].from_email, - settings.QGISFEED_FROM_EMAIL - ) - diff --git a/qgisfeedproject/qgisfeed/tests/__init__.py b/qgisfeedproject/qgisfeed/tests/__init__.py new file mode 100644 index 0000000..df954d6 --- /dev/null +++ b/qgisfeedproject/qgisfeed/tests/__init__.py @@ -0,0 +1 @@ +default_app_config = 'qgisfeed.apps.QgisFeedConfig' \ No newline at end of file diff --git a/qgisfeedproject/qgisfeed/tests/test_feed_entry.py b/qgisfeedproject/qgisfeed/tests/test_feed_entry.py new file mode 100644 index 0000000..d6d5912 --- /dev/null +++ b/qgisfeedproject/qgisfeed/tests/test_feed_entry.py @@ -0,0 +1,220 @@ +""""Tests for QGIS Welcome Page News Feed requests + +.. note:: This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + +""" + +__author__ = 'elpaso@itopen.it' +__date__ = '2019-05-07' +__copyright__ = 'Copyright 2019, ItOpen' + +import json + +from django.test import TestCase +from django.test import Client +from django.contrib.auth.models import Group, User +from django.contrib.admin.sites import AdminSite +from django.utils import timezone +from django.db import connection + +from ..models import ( + QgisFeedEntry +) +from ..admin import QgisFeedEntryAdmin + + +class MockRequest: + + def build_absolute_uri(self, uri): + return uri + +class MockSuperUser: + + def is_superuser(self): + return True + + def has_perm(self, perm): + return True + +class MockStaff: + + def is_superuser(self): + return False + + def is_staff(self): + return True + + def has_perm(self, perm): + return True + + +request = MockRequest() + + +class QgisFeedEntryTestCase(TestCase): + fixtures = ['qgisfeed.json', 'users.json'] + + def setUp(self): + pass + + def test_sorting(self): + c = Client(HTTP_USER_AGENT='Mozilla/5.0 QGIS/32400/Fedora ' + 'Linux (Workstation Edition)') + response = c.get('/') + data = json.loads(response.content) + data[0]['title'] = "Next Microsoft Windows code name revealed" + + def test_unpublished(self): + c = Client(HTTP_USER_AGENT='Mozilla/5.0 QGIS/32400/Fedora ' + 'Linux (Workstation Edition)') + response = c.get('/') + data = json.loads(response.content) + titles = [d['title'] for d in data] + self.assertFalse("QGIS core will be rewritten in FORTRAN" in titles) + + def test_published(self): + c = Client(HTTP_USER_AGENT='Mozilla/5.0 QGIS/32400/Fedora ' + 'Linux (Workstation Edition)') + response = c.get('/') + data = json.loads(response.content) + titles = [d['title'] for d in data] + self.assertTrue("QGIS core will be rewritten in Rust" in titles) + + def test_expired(self): + c = Client(HTTP_USER_AGENT='Mozilla/5.0 QGIS/32400/Fedora ' + 'Linux (Workstation Edition)') + response = c.get('/') + data = json.loads(response.content) + titles = [d['title'] for d in data] + self.assertFalse("QGIS core will be rewritten in PASCAL" in titles) + self.assertFalse("QGIS core will be rewritten in GO" in titles) + + def test_future(self): + c = Client(HTTP_USER_AGENT='Mozilla/5.0 QGIS/32400/Fedora ' + 'Linux (Workstation Edition)') + response = c.get('/') + data = json.loads(response.content) + titles = [d['title'] for d in data] + self.assertFalse("QGIS core will be rewritten in BASIC" in titles) + + def test_lang_filter(self): + c = Client(HTTP_USER_AGENT='Mozilla/5.0 QGIS/32400/Fedora ' + 'Linux (Workstation Edition)') + response = c.get('/?lang=fr') + data = json.loads(response.content) + titles = [d['title'] for d in data] + self.assertFalse("Null Island QGIS Meeting" in titles) + self.assertTrue("QGIS acquired by ESRI" in titles) + + c = Client(HTTP_USER_AGENT='Mozilla/5.0 QGIS/32400/Fedora ' + 'Linux (Workstation Edition)') + response = c.get('/?lang=en') + data = json.loads(response.content) + titles = [d['title'] for d in data] + self.assertTrue("Null Island QGIS Meeting" in titles) + self.assertTrue("QGIS acquired by ESRI" in titles) + + def test_lat_lon_filter(self): + c = Client(HTTP_USER_AGENT='Mozilla/5.0 QGIS/32400/Fedora ' + 'Linux (Workstation Edition)') + response = c.get('/?lat=0&lon=0') + data = json.loads(response.content) + titles = [d['title'] for d in data] + self.assertTrue("Null Island QGIS Meeting" in titles) + self.assertFalse("QGIS Italian Meeting" in titles) + + response = c.get('/?lat=44.5&lon=9.5') + data = json.loads(response.content) + titles = [d['title'] for d in data] + self.assertFalse("Null Island QGIS Meeting" in titles) + self.assertTrue("QGIS Italian Meeting" in titles) + + def test_after(self): + c = Client(HTTP_USER_AGENT='Mozilla/5.0 QGIS/32400/Fedora ' + 'Linux (Workstation Edition)') + response = c.get('/?after=%s' % timezone.datetime(2019, 5, 9).timestamp()) + data = json.loads(response.content) + titles = [d['title'] for d in data] + self.assertFalse("Null Island QGIS Meeting" in titles) + self.assertTrue("QGIS Italian Meeting" in titles) + + # Check that an updated entry is added to the feed even if + # expired, but only with QGIS >= 3.36 + with connection.cursor() as cursor: + cursor.execute("UPDATE qgisfeed_qgisfeedentry SET publish_to='2019-04-09', modified = '2019-05-10', title='Null Island QGIS Hackfest' WHERE title='Null Island QGIS Meeting'") + + response = c.get('/?after=%s' % timezone.datetime(2019, 5, 9).timestamp()) + titles = [d['title'] for d in data] + self.assertFalse("Null Island QGIS Meeting" in titles) + + c = Client(HTTP_USER_AGENT='Mozilla/5.0 QGIS/33600/Fedora ' + 'Linux (Workstation Edition)') + response = c.get('/?after=%s' % timezone.datetime(2019, 5, 9).timestamp()) + data = json.loads(response.content) + null_island = [d for d in data if d['title'] == "Null Island QGIS Hackfest"][0] + self.assertTrue(timezone.datetime(2019, 5, 9).timestamp() > null_island['publish_to']) + + + def test_invalid_parameters(self): + c = Client(HTTP_USER_AGENT='Mozilla/5.0 QGIS/32400/Fedora ' + 'Linux (Workstation Edition)') + response = c.get('/?lat=ZZ&lon=KK') + self.assertEqual(response.status_code, 400) + response = c.get('/?lang=KK') + self.assertEqual(response.status_code, 400) + + def test_image_link(self): + c = Client(HTTP_USER_AGENT='Mozilla/5.0 QGIS/32400/Fedora ' + 'Linux (Workstation Edition)') + response = c.get('/') + data = json.loads(response.content) + image = [d['image'] for d in data if d['image'] != ""][0] + self.assertEqual(image, "http://testserver/media/feedimages/rust.png" ) + + def test_sticky(self): + c = Client(HTTP_USER_AGENT='Mozilla/5.0 QGIS/32400/Fedora ' + 'Linux (Workstation Edition)') + response = c.get('/') + data = json.loads(response.content) + sticky = data[0] + self.assertTrue(sticky['sticky']) + not_sticky = data[-1] + self.assertFalse(not_sticky['sticky']) + + def test_group_is_created(self): + self.assertEqual(Group.objects.filter(name='qgisfeedentry_authors').count(), 1) + perms = sorted([p.codename for p in Group.objects.get(name='qgisfeedentry_authors').permissions.all()]) + self.assertEqual(perms, ['add_qgisfeedentry', 'view_qgisfeedentry']) + # Create a staff user and verify + staff = User(username='staff_user', is_staff=True) + staff.save() + self.assertIsNotNone(staff.groups.get(name='qgisfeedentry_authors')) + self.assertEqual(staff.get_all_permissions(), set(('qgisfeed.add_qgisfeedentry', 'qgisfeed.view_qgisfeedentry'))) + + def test_admin_publish_from(self): + """Test that published entries have publish_from set""" + + site = AdminSite() + ma = QgisFeedEntryAdmin(QgisFeedEntry, site) + obj = QgisFeedEntry(title='Test entry') + request.user = User.objects.get(username='admin') + form = ma.get_form(request, obj) + ma.save_model(request, obj, form, False) + self.assertIsNone(obj.publish_from) + self.assertFalse(obj.published) + obj.published = True + ma.save_model(request, obj, form, True) + self.assertIsNotNone(obj.publish_from) + self.assertTrue(obj.published) + + def test_admin_author_is_set(self): + site = AdminSite() + ma = QgisFeedEntryAdmin(QgisFeedEntry, site) + obj = QgisFeedEntry(title='Test entry 2') + request.user = User.objects.get(username='staff') + form = ma.get_form(request, obj) + ma.save_model(request, obj, form, False) + self.assertEqual(obj.author, request.user) \ No newline at end of file diff --git a/qgisfeedproject/qgisfeed/tests/test_home_page.py b/qgisfeedproject/qgisfeed/tests/test_home_page.py new file mode 100644 index 0000000..fd7af60 --- /dev/null +++ b/qgisfeedproject/qgisfeed/tests/test_home_page.py @@ -0,0 +1,53 @@ +from django.test import TestCase +from django.urls import reverse + +class HomePageTestCase(TestCase): + """ + Test home page web version + """ + fixtures = ['qgisfeed.json', 'users.json'] + + def setUp(self): + pass + + def test_authenticated_user_access(self): + self.client.login(username='admin', password='admin') + + # Access the all view after logging in + response = self.client.get(reverse('all')) + + # Check if the response status code is 200 (OK) + self.assertEqual(response.status_code, 200) + + # Check if the correct template is used + self.assertTemplateUsed(response, 'feeds/feed_home_page.html') + self.assertTrue('form' in response.context) + + + def test_unauthenticated_user_access(self): + # Access the all view without logging in + response = self.client.get(reverse('all')) + + # Check if the response status code is 200 (OK) + self.assertEqual(response.status_code, 200) + + # Check if the correct template is used + self.assertTemplateUsed(response, 'feeds/feed_home_page.html') + self.assertTrue('form' in response.context) + + def test_feeds_list_filtering(self): + # Test filter homepage feeds + + data = { + 'lang': 'en', + 'publish_from': '2023-12-31', + } + response = self.client.get(reverse('all'), data) + + # Check if the response status code is 200 (OK) + self.assertEqual(response.status_code, 200) + + # Check if the correct template is used + self.assertTemplateUsed(response, 'feeds/feed_home_page.html') + self.assertTrue('form' in response.context) + diff --git a/qgisfeedproject/qgisfeed/tests/test_login.py b/qgisfeedproject/qgisfeed/tests/test_login.py new file mode 100644 index 0000000..0c99c72 --- /dev/null +++ b/qgisfeedproject/qgisfeed/tests/test_login.py @@ -0,0 +1,19 @@ +from django.test import TestCase +from django.test import Client + +class LoginTestCase(TestCase): + """ + Test the login feature + """ + fixtures = ['qgisfeed.json', 'users.json'] + + def setUp(self): + self.client = Client() + + def test_valid_login(self): + response = self.client.login(username='admin', password='admin') + self.assertTrue(response) + + def test_invalid_login(self): + response = self.client.login(username='admin', password='wrongpassword') + self.assertFalse(response) \ No newline at end of file diff --git a/qgisfeedproject/qgisfeed/tests/test_user_visit.py b/qgisfeedproject/qgisfeed/tests/test_user_visit.py new file mode 100644 index 0000000..b392454 --- /dev/null +++ b/qgisfeedproject/qgisfeed/tests/test_user_visit.py @@ -0,0 +1,58 @@ +""""Tests for QGIS Welcome Page News Feed requests + +.. note:: This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + +""" + +__author__ = 'elpaso@itopen.it' +__date__ = '2019-05-07' +__copyright__ = 'Copyright 2019, ItOpen' + +from django.test import TestCase +from django.test import Client + +from ..models import ( + QgisUserVisit, DailyQgisUserVisit, aggregate_user_visit_data +) + + +class QgisUserVisitTestCase(TestCase): + + def test_user_visit(self): + c = Client(HTTP_USER_AGENT='Mozilla/5.0 QGIS/32400/Fedora ' + 'Linux (Workstation Edition)') + c.get('/') + user_visit = QgisUserVisit.objects.filter( + platform__icontains='Fedora Linux (Workstation Edition)') + self.assertEqual(user_visit.count(), 1) + self.assertEqual(user_visit.first().qgis_version, '32400') + + def test_ip_address_removed(self): + c = Client(HTTP_USER_AGENT='Mozilla/5.0 QGIS/32400/Fedora ' + 'Linux (Workstation Edition)', + REMOTE_ADDR='180.247.213.170') + c.get('/') + qgis_visit = QgisUserVisit.objects.first() + self.assertTrue(qgis_visit.user_visit.remote_addr == '') + self.assertTrue(qgis_visit.location['country_name'] == 'Indonesia') + + def test_aggregate_visit(self): + c = Client(HTTP_USER_AGENT='Mozilla/5.0 QGIS/31400/Fedora ' + 'Linux (Workstation Edition)', + REMOTE_ADDR='180.247.213.170') + c.get('/') + c = Client(HTTP_USER_AGENT='Mozilla/5.0 QGIS/32400/Windows 10', + REMOTE_ADDR='180.247.213.160') + c.get('/') + c = Client(HTTP_USER_AGENT='Mozilla/5.0 QGIS/32400/Windows XP', + REMOTE_ADDR='180.247.213.160') + c.get('/') + aggregate_user_visit_data() + daily_visit = DailyQgisUserVisit.objects.first() + self.assertTrue(daily_visit.platform['Windows 10'] == 1) + self.assertTrue(daily_visit.qgis_version['32400'] == 2) + self.assertTrue(daily_visit.country['ID'] == 3) + diff --git a/qgisfeedproject/qgisfeed/tests/test_views.py b/qgisfeedproject/qgisfeed/tests/test_views.py new file mode 100644 index 0000000..8ad46f8 --- /dev/null +++ b/qgisfeedproject/qgisfeed/tests/test_views.py @@ -0,0 +1,293 @@ +from django.test import TestCase +from django.test import Client +from django.contrib.auth.models import Group, User +from django.core.paginator import Page +from django.urls import reverse +from django.contrib.gis.geos import Polygon +from django.conf import settings +from django.core import mail + +from ..utils import get_field_max_length + +from ..models import ( + CharacterLimitConfiguration, QgisFeedEntry +) + +from os.path import join + +class FeedsListViewTestCase(TestCase): + """ + Test the feeds list feature + """ + fixtures = ['qgisfeed.json', 'users.json'] + def setUp(self): + self.client = Client() + + def test_authenticated_user_access(self): + self.client.login(username='admin', password='admin') + + # Access the feeds_list view after logging in + response = self.client.get(reverse('feeds_list')) + + # Check if the response status code is 200 (OK) + self.assertEqual(response.status_code, 200) + + # Check if the correct template is used + self.assertTemplateUsed(response, 'feeds/feeds_list.html') + + def test_unauthenticated_user_redirect_to_login(self): + # Access the feeds_list view without logging in + response = self.client.get(reverse('feeds_list')) + + # Check if the response status code is 302 (Redirect) + self.assertEqual(response.status_code, 302) + + # Check if the user is redirected to the login page + self.assertRedirects(response, reverse('login') + '?next=' + reverse('feeds_list')) + + + def test_nonstaff_user_redirect_to_login(self): + user = User.objects.create_user(username='testuser', password='testpassword') + self.client.login(username='testuser', password='testpassword') + # Access the feeds_list view with a non staff user + response = self.client.get(reverse('feeds_list')) + + # Check if the response status code is 302 (Redirect) + self.assertEqual(response.status_code, 302) + + # Check if the user is redirected to the login page + self.assertRedirects(response, reverse('login') + '?next=' + reverse('feeds_list')) + + def test_feeds_list_filtering(self): + self.client.login(username='admin', password='admin') + # Simulate a GET request with filter parameters + data = { + 'title': 'QGIS', + 'author': 'admin', + 'language_filter': 'en', + 'publish_from': '2019-01-01', + 'publish_to': '2023-12-31', + 'sort_by': 'title', + 'order': 'asc', + } + response = self.client.get(reverse('feeds_list'), data) + + # Check that the response status code is 200 (OK) + self.assertEqual(response.status_code, 200) + + # Check that the response contains the expected context data + self.assertTrue('feeds_entry' in response.context) + self.assertTrue(isinstance(response.context['feeds_entry'], Page)) + self.assertTrue('sort_by' in response.context) + self.assertTrue('order' in response.context) + self.assertTrue('current_order' in response.context) + self.assertTrue('form' in response.context) + self.assertTrue('count' in response.context) + +class FeedsItemFormTestCase(TestCase): + """ + Test the feeds add/update feature + """ + fixtures = ['qgisfeed.json', 'users.json'] + def setUp(self): + self.client = Client() + spatial_filter = Polygon(((0, 0), (0, 1), (1, 1), (1, 0), (0, 0))) + image_path = join(settings.MEDIA_ROOT, "feedimages", "rust.png") + self.post_data = { + 'title': 'QGIS core will be rewritten in Rust', + 'image': open(image_path, "rb"), + 'content': '

Tired with C++ intricacies, the core developers have decided to rewrite QGIS in Rust', + 'url': 'https://www.null.com', + 'sticky': False, + 'sorting': 0, + 'language_filter': 'en', + 'spatial_filter': str(spatial_filter), + 'publish_from': '2023-10-18 14:46:00+00', + 'publish_to': '2023-10-29 14:46:00+00' + } + + + def test_authenticated_user_access(self): + self.client.login(username='admin', password='admin') + + # Access the feed_entry_add view after logging in + response = self.client.get(reverse('feed_entry_add')) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'feeds/feed_item_form.html') + self.assertTrue('form' in response.context) + + # Check if the approver has the permission. + # Here, only the the admin user is listed. + approvers = response.context['form']['approvers'] + self.assertEqual(len(approvers), 1) + approver_id = int(approvers[0].data['value']) + approver = User.objects.get(pk=approver_id) + self.assertTrue(approver.has_perm("qgisfeed.publish_qgisfeedentry")) + + # Access the feed_entry_update view after logging in + response = self.client.get(reverse('feed_entry_update', args=[3])) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'feeds/feed_item_form.html') + self.assertTrue('form' in response.context) + + def test_unauthenticated_user_redirect_to_login(self): + # Access the feed_entry_add view without logging in + response = self.client.get(reverse('feed_entry_add')) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, reverse('login') + '?next=' + reverse('feed_entry_add')) + self.assertIsNone(response.context) + + # Access the feed_entry_update view without logging in + response = self.client.get(reverse('feed_entry_update', args=[3])) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, reverse('login') + '?next=' + reverse('feed_entry_update', args=[3])) + self.assertIsNone(response.context) + + def test_nonstaff_user_redirect_to_login(self): + user = User.objects.create_user(username='testuser', password='testpassword') + self.client.login(username='testuser', password='testpassword') + + # Access the feed_entry_add view with a non staff user + response = self.client.get(reverse('feed_entry_add')) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, reverse('login') + '?next=' + reverse('feed_entry_add')) + self.assertIsNone(response.context) + + # Access the feed_entry_add view with a non staff user + response = self.client.get(reverse('feed_entry_update', args=[3])) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, reverse('login') + '?next=' + reverse('feed_entry_update', args=[3])) + self.assertIsNone(response.context) + + def test_authenticated_user_add_feed(self): + # Add a feed entry test + self.client.login(username='staff', password='staff') + + response = self.client.post(reverse('feed_entry_add'), data=self.post_data) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, reverse('feeds_list')) + + + def test_authenticated_user_update_feed(self): + # Update a feed entry test + self.client.login(username='admin', password='admin') + + response = self.client.post(reverse('feed_entry_update', args=[3]), data=self.post_data) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, reverse('feeds_list')) + + + def test_not_allowed_user_update_feed(self): + # Update a feed entry with a non allowed user + self.client.login(username='staff', password='staff') + + response = self.client.post(reverse('feed_entry_update', args=[7]), data=self.post_data) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, reverse('login') + '?next=' + reverse('feed_entry_update', args=[7])) + self.assertIsNone(response.context) + + def test_allowed_user_publish_feed(self): + # Publish a feed entry test + self.client.login(username='admin', password='admin') + self.post_data['publish'] = 1 + response = self.client.post(reverse('feed_entry_update', args=[7]), data=self.post_data) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, reverse('feeds_list')) + + updated_data = QgisFeedEntry.objects.get(pk=7) + self.assertTrue(updated_data.published) + + def test_allowed_staff_publish_feed(self): + # Update a feed entry with an allowed staff user + user = User.objects.get(username='staff') + user.save() + group = Group.objects.get(name='qgisfeedentry_approver') + group.user_set.add(user) + + self.client.login(username='staff', password='staff') + self.post_data['publish'] = 1 + response = self.client.post(reverse('feed_entry_update', args=[7]), data=self.post_data) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, reverse('feeds_list')) + + updated_data = QgisFeedEntry.objects.get(pk=7) + self.assertTrue(updated_data.published) + + def test_allowed_staff_unpublish_feed(self): + # Update a feed entry with an allowed staff user + user = User.objects.get(username='staff') + user.save() + group = Group.objects.get(name='qgisfeedentry_approver') + group.user_set.add(user) + + self.client.login(username='staff', password='staff') + self.post_data['publish'] = 0 + + response = self.client.post(reverse('feed_entry_update', args=[7]), data=self.post_data) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, reverse('feeds_list')) + + updated_data = QgisFeedEntry.objects.get(pk=7) + self.assertFalse(updated_data.published) + + def test_authenticated_user_add_invalid_data(self): + # Add a feed entry that contains invalid data + self.client.login(username='staff', password='staff') + spatial_filter = Polygon(((0, 0), (0, 1), (1, 1), (1, 0), (0, 0))) + image_path = join(settings.MEDIA_ROOT, "feedimages", "rust.png") + + # Limit content value to 10 characters + config, created = CharacterLimitConfiguration.objects.update_or_create( + field_name="content", + max_characters=10 + ) + + post_data = { + 'title': '', + 'image': open(image_path, "rb"), + 'content': '

Tired with C++ intricacies, the core developers have decided to rewrite QGIS in Rust', + 'url': '', + 'sticky': False, + 'sorting': 0, + 'language_filter': 'en', + 'spatial_filter': str(spatial_filter), + 'publish_from': '', + 'publish_to': '' + } + + response = self.client.post(reverse('feed_entry_add'), data=post_data) + self.assertEqual(response.status_code, 200) + form = response.context['form'] + self.assertIn('title', form.errors, "This field is required.") + self.assertIn('content', form.errors, "Ensure this value has at most 10 characters (it has 104).") + + def test_get_field_max_length(self): + # Test the get_field_max_length function + content_max_length = get_field_max_length(CharacterLimitConfiguration, field_name="content") + self.assertEqual(content_max_length, 500) + CharacterLimitConfiguration.objects.create( + field_name="content", + max_characters=1000 + ) + content_max_length = get_field_max_length(CharacterLimitConfiguration, field_name="content") + self.assertEqual(content_max_length, 1000) + + def test_add_feed_with_reviewer(self): + # Add a feed entry with specified reviewer test + self.client.login(username='staff', password='staff') + self.post_data['reviewers'] = [1] + + response = self.client.post(reverse('feed_entry_add'), data=self.post_data) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, reverse('feeds_list')) + + self.assertEqual( + mail.outbox[0].recipients(), + ['me@email.com'] + ) + + self.assertEqual( + mail.outbox[0].from_email, + settings.QGISFEED_FROM_EMAIL + ) + From 6fd59857ae7a46a1c0e1dcae2fc489abfadb45e6 Mon Sep 17 00:00:00 2001 From: Lova ANDRIARIMALALA <43842786+Xpirix@users.noreply.github.com> Date: Tue, 18 Jun 2024 17:07:27 +0300 Subject: [PATCH 6/8] Use the original django-social-share package --- REQUIREMENTS.txt | 1 + .../qgisfeed/templatetags/__init__.py | 0 .../qgisfeed/templatetags/social_share.py | 260 ------------------ .../qgisfeed/tests/test_feed_detail.py | 55 ++++ qgisfeedproject/qgisfeedproject/settings.py | 5 +- .../templatetags/post_to_linkedin.html | 2 +- 6 files changed, 61 insertions(+), 262 deletions(-) delete mode 100644 qgisfeedproject/qgisfeed/templatetags/__init__.py delete mode 100644 qgisfeedproject/qgisfeed/templatetags/social_share.py create mode 100644 qgisfeedproject/qgisfeed/tests/test_feed_detail.py diff --git a/REQUIREMENTS.txt b/REQUIREMENTS.txt index 5b4fe44..c5139da 100644 --- a/REQUIREMENTS.txt +++ b/REQUIREMENTS.txt @@ -10,6 +10,7 @@ Django==4.2.8 django-appconf==1.0.3 django-extensions==3.1.5 django-imagekit==5.0.0 +django-social-share==2.3.0 django-tinymce==3.6.1 django-user-visit==0.5.1 django-webpack-loader==2.0.1 diff --git a/qgisfeedproject/qgisfeed/templatetags/__init__.py b/qgisfeedproject/qgisfeed/templatetags/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/qgisfeedproject/qgisfeed/templatetags/social_share.py b/qgisfeedproject/qgisfeed/templatetags/social_share.py deleted file mode 100644 index 0a3f38a..0000000 --- a/qgisfeedproject/qgisfeed/templatetags/social_share.py +++ /dev/null @@ -1,260 +0,0 @@ -# -*- coding: utf-8 -*- -# This code is based on https://github.com/fcurella/django-social-share -# Copyright (C) 2011 by Flavio Curella -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. - -from __future__ import unicode_literals - -import re - -from django import template - -from django.db.models import Model -from django.template.defaultfilters import urlencode -from django.utils.safestring import mark_safe - - -register = template.Library() - - -TWITTER_ENDPOINT = 'https://twitter.com/intent/tweet?text=%s' -FACEBOOK_ENDPOINT = 'https://www.facebook.com/sharer/sharer.php?u=%s' -GPLUS_ENDPOINT = 'https://plus.google.com/share?url=%s' -MAIL_ENDPOINT = 'mailto:?subject=%s&body=%s' -LINKEDIN_ENDPOINT = 'https://www.linkedin.com/shareArticle?mini=true&title=hello&url=%s' -REDDIT_ENDPOINT = 'https://www.reddit.com/submit?title=%s&url=%s' -TELEGRAM_ENDPOINT = 'https://t.me/share/url?text=%s&url=%s' -WHATSAPP_ENDPOINT = 'https://api.whatsapp.com/send?text=%s' -PINTEREST_ENDPOINT = 'https://www.pinterest.com/pin/create/button/?url=%s' - - -BITLY_REGEX = re.compile(r'^https?://bit\.ly/') - - -def compile_text(context, text): - ctx = template.context.Context(context) - return template.Template(text).render(ctx) - - -def _build_url(request, obj_or_url): - if obj_or_url is not None: - if isinstance(obj_or_url, Model): - return request.build_absolute_uri(obj_or_url.get_absolute_url()) - else: - return request.build_absolute_uri(obj_or_url) - return '' - - -def _compose_tweet(text, url=None): - TWITTER_MAX_NUMBER_OF_CHARACTERS = 140 - TWITTER_LINK_LENGTH = 23 # "A URL of any length will be altered to 23 characters, even if the link itself is less than 23 characters long. - - # Compute length of the tweet - url_length = len(' ') + TWITTER_LINK_LENGTH if url else 0 - total_length = len(text) + url_length - - # Check that the text respects the max number of characters for a tweet - if total_length > TWITTER_MAX_NUMBER_OF_CHARACTERS: - text = text[:(TWITTER_MAX_NUMBER_OF_CHARACTERS - url_length - 1)] + "…" # len("…") == 1 - - return "%s %s" % (text, url) if url else text - - -@register.simple_tag(takes_context=True) -def post_to_twitter_url(context, text, obj_or_url=None): - text = compile_text(context, text) - request = context['request'] - - url = _build_url(request, obj_or_url) - - tweet = _compose_tweet(text, url) - context['tweet_url'] = TWITTER_ENDPOINT % urlencode(tweet) - return context - - -@register.inclusion_tag('django_social_share/templatetags/post_to_twitter.html', takes_context=True) -def post_to_twitter(context, text, obj_or_url=None, link_text='',link_class=""): - context = post_to_twitter_url(context, text, obj_or_url) - - request = context['request'] - url = _build_url(request, obj_or_url) - tweet = _compose_tweet(text, url) - - context['link_class'] = link_class - context['link_text'] = link_text or 'Post to Twitter' - context['full_text'] = tweet - return context - - -@register.simple_tag(takes_context=True) -def post_to_facebook_url(context, obj_or_url=None): - request = context['request'] - url = _build_url(request, obj_or_url) - context['facebook_url'] = FACEBOOK_ENDPOINT % urlencode(url) - return context - - -@register.inclusion_tag('django_social_share/templatetags/post_to_facebook.html', takes_context=True) -def post_to_facebook(context, obj_or_url=None, link_text='',link_class=''): - context = post_to_facebook_url(context, obj_or_url) - context['link_class'] = link_class or '' - context['link_text'] = link_text or 'Post to Facebook' - return context - - -@register.simple_tag(takes_context=True) -def post_to_gplus_url(context, obj_or_url=None): - request = context['request'] - url = _build_url(request, obj_or_url) - context['gplus_url'] = GPLUS_ENDPOINT % urlencode(url) - return context - - -@register.inclusion_tag('django_social_share/templatetags/post_to_gplus.html', takes_context=True) -def post_to_gplus(context, obj_or_url=None, link_text='',link_class=''): - context = post_to_gplus_url(context, obj_or_url) - context['link_class'] = link_class - context['link_text'] = link_text or 'Post to Google+' - return context - - -@register.simple_tag(takes_context=True) -def send_email_url(context, subject, text, obj_or_url=None): - text = compile_text(context, text) - subject = compile_text(context, subject) - request = context['request'] - url = _build_url(request, obj_or_url) - full_text = "%s %s" % (text, url) - context['mailto_url'] = MAIL_ENDPOINT % (urlencode(subject), urlencode(full_text)) - return context - - -@register.inclusion_tag('django_social_share/templatetags/send_email.html', takes_context=True) -def send_email(context, subject, text, obj_or_url=None, link_text='',link_class=''): - context = send_email_url(context, subject, text, obj_or_url) - context['link_class'] = link_class - context['link_text'] = link_text or 'Share via email' - return context - - -@register.filter(name='linkedin_locale') -def linkedin_locale(value): - if "-" not in value: - return value - - lang, country = value.split('-') - return '_'.join([lang, country.upper()]) - - -@register.simple_tag(takes_context=True) -def post_to_linkedin_url(context, obj_or_url=None): - request = context['request'] - url = _build_url(request, obj_or_url) - context['linkedin_url'] = LINKEDIN_ENDPOINT % urlencode(url) - return context - - -@register.inclusion_tag('django_social_share/templatetags/post_to_linkedin.html', takes_context=True) -def post_to_linkedin(context, obj_or_url=None,link_class=''): - context = post_to_linkedin_url(context, obj_or_url) - context['link_class'] = link_class - return context - - -@register.simple_tag(takes_context=True) -def post_to_reddit_url(context, title, obj_or_url=None): - request = context['request'] - title = compile_text(context, title) - url = _build_url(request, obj_or_url) - context['reddit_url'] = mark_safe(REDDIT_ENDPOINT % (urlencode(title), urlencode(url))) - return context - - -@register.inclusion_tag('django_social_share/templatetags/post_to_reddit.html', takes_context=True) -def post_to_reddit(context, title, obj_or_url=None, link_text='',link_class=''): - context = post_to_reddit_url(context, title, obj_or_url) - context['link_class'] = link_class - context['link_text'] = link_text or 'Post to Reddit' - return context - - -@register.simple_tag(takes_context=True) -def post_to_telegram_url(context, title, obj_or_url): - request = context['request'] - title = compile_text(context, title) - url = _build_url(request, obj_or_url) - context['telegram_url'] = mark_safe(TELEGRAM_ENDPOINT % (urlencode(title), urlencode(url))) - return context - - -@register.inclusion_tag('django_social_share/templatetags/post_to_telegram.html', takes_context=True) -def post_to_telegram(context, title, obj_or_url=None, link_text='',link_class=''): - context = post_to_telegram_url(context, title, obj_or_url) - context['link_class'] = link_class - context['link_text'] = link_text or 'Post to Telegram' - return context - - -@register.simple_tag(takes_context=True) -def post_to_whatsapp_url(context, obj_or_url=None): - request = context['request'] - url = _build_url(request, obj_or_url) - context['whatsapp_url'] = WHATSAPP_ENDPOINT % urlencode(url) - return context - - -@register.inclusion_tag('django_social_share/templatetags/post_to_whatsapp.html', takes_context=True) -def post_to_whatsapp(context, obj_or_url=None, link_text='',link_class=''): - context = post_to_whatsapp_url(context, obj_or_url) - context['link_class'] = link_class - context['link_text'] = link_text or 'Post to WhatsApp' - return context - - -@register.simple_tag(takes_context=True) -def save_to_pinterest_url(context, obj_or_url=None): - request = context['request'] - url = _build_url(request, obj_or_url) - context['pinterest_url'] = PINTEREST_ENDPOINT % urlencode(url) - return context - - -@register.inclusion_tag('django_social_share/templatetags/save_to_pinterest.html', takes_context=True) -def save_to_pinterest(context, obj_or_url=None, pin_count=False, link_class=''): - context = save_to_pinterest_url(context, obj_or_url) - context['link_class'] = link_class - context['pin_count'] = pin_count - return context - - -@register.inclusion_tag('django_social_share/templatetags/pinterest_script.html', takes_context=False) -def add_pinterest_script(): - pass - -@register.simple_tag(takes_context=True) -def copy_to_clipboard_url(context, obj_or_url=None): - request = context['request'] - url = _build_url(request, obj_or_url) - context['copy_url'] = url - return context - -@register.inclusion_tag('django_social_share/templatetags/copy_to_clipboard.html', takes_context=True) -def copy_to_clipboard(context, obj_or_url, link_text='', link_class=''): - context = copy_to_clipboard_url(context, obj_or_url) - - context['link_class'] = link_class - context['link_text'] = link_text or 'Copy to clipboard' - return context - -@register.inclusion_tag('django_social_share/templatetags/copy_script.html', takes_context=False) -def add_copy_script(): - pass \ No newline at end of file diff --git a/qgisfeedproject/qgisfeed/tests/test_feed_detail.py b/qgisfeedproject/qgisfeed/tests/test_feed_detail.py new file mode 100644 index 0000000..24ee25d --- /dev/null +++ b/qgisfeedproject/qgisfeed/tests/test_feed_detail.py @@ -0,0 +1,55 @@ +# tests.py +from django.test import TestCase, RequestFactory +from django.urls import reverse +from django.contrib.auth.models import User +from django.utils import timezone +from django.shortcuts import get_object_or_404 +from ..models import QgisFeedEntry +from ..views import FeedEntryDetailView +from django.contrib.gis.geos import Polygon +from django.conf import settings +from django.test import Client +from os.path import join + +class FeedEntryDetailViewTestCase(TestCase): + """ + Test feed detail page web version + """ + fixtures = ['qgisfeed.json', 'users.json'] + def setUp(self): + self.client = Client() + spatial_filter = Polygon(((0, 0), (0, 1), (1, 1), (1, 0), (0, 0))) + image_path = join(settings.MEDIA_ROOT, "feedimages", "rust.png") + self.post_data = { + 'title': 'QGIS core will be rewritten in Rust', + 'image': open(image_path, "rb"), + 'content': '

Tired with C++ intricacies, the core developers have decided to rewrite QGIS in Rust', + 'url': 'https://www.null.com', + 'sticky': False, + 'sorting': 0, + 'language_filter': 'en', + 'spatial_filter': str(spatial_filter), + 'publish_from': '2023-10-18 14:46:00+00', + 'publish_to': '2023-10-29 14:46:00+00' + } + # Add a feed entry test + self.client.login(username='staff', password='staff') + + self.client.post(reverse('feed_entry_add'), data=self.post_data) + + self.entry = QgisFeedEntry.objects.last() + + self.factory = RequestFactory() + + def test_feed_entry_detail_view(self): + url = reverse('feed_entry_detail', kwargs={'pk': self.entry.pk}) + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'feeds/feed_item_detail.html') + self.assertEqual(response.context['feed_entry'], self.entry) + self.assertIsNotNone(response.context['spatial_filter_geojson']) + + def test_feed_entry_detail_view_not_found(self): + url = reverse('feed_entry_detail', kwargs={'pk': 999}) + response = self.client.get(url) + self.assertEqual(response.status_code, 404) diff --git a/qgisfeedproject/qgisfeedproject/settings.py b/qgisfeedproject/qgisfeedproject/settings.py index 695b0ec..050a4ab 100644 --- a/qgisfeedproject/qgisfeedproject/settings.py +++ b/qgisfeedproject/qgisfeedproject/settings.py @@ -53,7 +53,10 @@ 'user_visit', # Webpack - 'webpack_loader' + 'webpack_loader', + + # Social share + 'django_social_share' ] # Useful debugging extensions diff --git a/qgisfeedproject/templates/django_social_share/templatetags/post_to_linkedin.html b/qgisfeedproject/templates/django_social_share/templatetags/post_to_linkedin.html index 2be5182..b3699df 100644 --- a/qgisfeedproject/templates/django_social_share/templatetags/post_to_linkedin.html +++ b/qgisfeedproject/templates/django_social_share/templatetags/post_to_linkedin.html @@ -1,5 +1,5 @@ From cd95194b70d40ce9e22131351a388a0642451c81 Mon Sep 17 00:00:00 2001 From: Lova ANDRIARIMALALA <43842786+Xpirix@users.noreply.github.com> Date: Wed, 19 Jun 2024 10:23:01 +0300 Subject: [PATCH 7/8] Remove unused functions --- qgisfeedproject/qgisfeed/utils.py | 50 ------------------------------- 1 file changed, 50 deletions(-) diff --git a/qgisfeedproject/qgisfeed/utils.py b/qgisfeedproject/qgisfeed/utils.py index a518016..28aaf29 100644 --- a/qgisfeedproject/qgisfeed/utils.py +++ b/qgisfeedproject/qgisfeed/utils.py @@ -44,53 +44,3 @@ def get_field_max_length(ConfigurationModel: Model, field_name: str): return config.max_characters except ConfigurationModel.DoesNotExist: return 500 - - -def push_to_linkedin(title, content): - access_token = 'YOUR_ACCESS_TOKEN' - headers = { - 'Authorization': f'Bearer {access_token}', - 'Content-Type': 'application/json', - 'X-Restli-Protocol-Version': '2.0.0' - } - payload = { - "author": "urn:li:person:YOUR_PERSON_URN", - "lifecycleState": "PUBLISHED", - "specificContent": { - "com.linkedin.ugc.ShareContent": { - "shareCommentary": { - "text": title + content - }, - "shareMediaCategory": "NONE" - } - }, - "visibility": { - "com.linkedin.ugc.MemberNetworkVisibility": "PUBLIC" - } - } - response = requests.post('https://api.linkedin.com/v2/ugcPosts', headers=headers, json=payload) - return response.json() - - -def push_to_facebook(title, content): - access_token = 'YOUR_ACCESS_TOKEN' - page_id = 'YOUR_PAGE_ID' - url = f'https://graph.facebook.com/{page_id}/feed' - payload = { - 'message': title + content, - 'access_token': access_token - } - response = requests.post(url, data=payload) - return response.json() - - -def push_to_telegram(title, content): - bot_token = 'YOUR_BOT_TOKEN' - chat_id = 'YOUR_GROUP_CHAT_ID' - url = f'https://api.telegram.org/bot{bot_token}/sendMessage' - payload = { - 'chat_id': chat_id, - 'text': title + content - } - response = requests.post(url, data=payload) - return response.json() From bc0846d9141b3543493471af38727bc8cba0ec43 Mon Sep 17 00:00:00 2001 From: Lova ANDRIARIMALALA <43842786+Xpirix@users.noreply.github.com> Date: Wed, 19 Jun 2024 10:31:02 +0300 Subject: [PATCH 8/8] Remove unused imports in utils.py --- qgisfeedproject/qgisfeed/utils.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/qgisfeedproject/qgisfeed/utils.py b/qgisfeedproject/qgisfeed/utils.py index 28aaf29..f3a1278 100644 --- a/qgisfeedproject/qgisfeed/utils.py +++ b/qgisfeedproject/qgisfeed/utils.py @@ -5,9 +5,7 @@ from django.urls import reverse from django.conf import settings from django.core.mail import EmailMultiAlternatives -from django.core.mail import send_mail from django.contrib.gis.db.models import Model -import requests logger = logging.getLogger('qgisfeed.admin') QGISFEED_FROM_EMAIL = getattr(settings, 'QGISFEED_FROM_EMAIL', 'noreply@qgis.org')