wbnews 2.2.1__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- wbnews/.coveragerc +23 -0
- wbnews/__init__.py +1 -0
- wbnews/admin.py +27 -0
- wbnews/apps.py +9 -0
- wbnews/factories.py +33 -0
- wbnews/import_export/__init__.py +0 -0
- wbnews/import_export/backends/__init__.py +1 -0
- wbnews/import_export/backends/news.py +35 -0
- wbnews/import_export/handlers/__init__.py +1 -0
- wbnews/import_export/handlers/news.py +25 -0
- wbnews/import_export/parsers/__init__.py +0 -0
- wbnews/import_export/parsers/emails/__init__.py +0 -0
- wbnews/import_export/parsers/emails/news.py +48 -0
- wbnews/import_export/parsers/emails/utils.py +61 -0
- wbnews/import_export/parsers/rss/__init__.py +0 -0
- wbnews/import_export/parsers/rss/news.py +63 -0
- wbnews/migrations/0001_initial_squashed_0005_alter_news_import_source.py +349 -0
- wbnews/migrations/0006_alter_news_language.py +122 -0
- wbnews/migrations/0007_auto_20240103_0955.py +43 -0
- wbnews/migrations/0008_alter_news_language.py +123 -0
- wbnews/migrations/0009_newsrelationship_analysis_newsrelationship_sentiment.py +94 -0
- wbnews/migrations/__init__.py +0 -0
- wbnews/models/__init__.py +3 -0
- wbnews/models/news.py +116 -0
- wbnews/models/relationships.py +20 -0
- wbnews/models/sources.py +43 -0
- wbnews/serializers.py +83 -0
- wbnews/signals.py +4 -0
- wbnews/tests/__init__.py +0 -0
- wbnews/tests/conftest.py +6 -0
- wbnews/tests/test_models.py +15 -0
- wbnews/tests/tests.py +12 -0
- wbnews/urls.py +29 -0
- wbnews/viewsets/__init__.py +12 -0
- wbnews/viewsets/buttons.py +23 -0
- wbnews/viewsets/display.py +133 -0
- wbnews/viewsets/endpoints.py +18 -0
- wbnews/viewsets/menu.py +23 -0
- wbnews/viewsets/titles.py +39 -0
- wbnews/viewsets/views.py +140 -0
- wbnews-2.2.1.dist-info/METADATA +8 -0
- wbnews-2.2.1.dist-info/RECORD +43 -0
- wbnews-2.2.1.dist-info/WHEEL +5 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Generated by Django 5.0.9 on 2024-10-24 11:00
|
|
2
|
+
|
|
3
|
+
import django.contrib.postgres.fields
|
|
4
|
+
from django.db import migrations, models
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def migrate_new_source(apps, schema_editor):
|
|
8
|
+
from wbcore.contrib.io.models import (
|
|
9
|
+
DataBackend,
|
|
10
|
+
ImportSource,
|
|
11
|
+
ParserHandler,
|
|
12
|
+
Source,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
parser_handler = ParserHandler.objects.get(parser="wbnews.import_export.parsers.rss.news")
|
|
16
|
+
io_news_sources = Source.objects.filter(parser_handler=parser_handler)
|
|
17
|
+
crontab = io_news_sources.last().crontab
|
|
18
|
+
data_backend = DataBackend.objects.get(backend_class_path="wbnews.import_export.backends.news")
|
|
19
|
+
default_news_source, created = Source.objects.get_or_create(
|
|
20
|
+
data_backend=data_backend,
|
|
21
|
+
defaults={
|
|
22
|
+
"crontab": crontab,
|
|
23
|
+
"title": "RSS Feeds -> News",
|
|
24
|
+
},
|
|
25
|
+
)
|
|
26
|
+
default_news_source.parser_handler.set([parser_handler])
|
|
27
|
+
ImportSource.objects.filter(source__in=io_news_sources).update(source=default_news_source)
|
|
28
|
+
io_news_sources.exclude(id=default_news_source.id).delete()
|
|
29
|
+
NewsSource = apps.get_model("wbnews", "NewsSource")
|
|
30
|
+
NewsSource.objects.filter(title__icontains="Email").update(type="EMAIL")
|
|
31
|
+
NewsSource.objects.filter(type="RSS").filter(url__isnull=True, identifier__isnull=False).update(
|
|
32
|
+
url=models.F("identifier")
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
unvalid_rss_feeds = [
|
|
36
|
+
"https://www.dailytelegraph.com.au/help-rss",
|
|
37
|
+
"https://techcrunch.com/rssfeeds/",
|
|
38
|
+
"https://www.bbc.co.uk/news/",
|
|
39
|
+
"https://www.nytimes.com/section/technology",
|
|
40
|
+
"http://online.wsj.com/page/2_0006.html",
|
|
41
|
+
"http://online.wsj.com",
|
|
42
|
+
"https://www.bbc.co.uk/news/technology",
|
|
43
|
+
"https://www.bbc.co.uk/news/world",
|
|
44
|
+
"https://www.bbc.co.uk/news/business",
|
|
45
|
+
"https://www.techworld.com/news/rss",
|
|
46
|
+
]
|
|
47
|
+
NewsSource.objects.filter(url__in=unvalid_rss_feeds).update(is_active=False)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class Migration(migrations.Migration):
|
|
51
|
+
dependencies = [
|
|
52
|
+
("wbnews", "0008_alter_news_language"),
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
operations = [
|
|
56
|
+
migrations.AddField(
|
|
57
|
+
model_name="newsrelationship",
|
|
58
|
+
name="analysis",
|
|
59
|
+
field=models.TextField(blank=True, null=True),
|
|
60
|
+
),
|
|
61
|
+
migrations.AddField(
|
|
62
|
+
model_name="newsrelationship",
|
|
63
|
+
name="sentiment",
|
|
64
|
+
field=models.PositiveIntegerField(blank=True, null=True),
|
|
65
|
+
),
|
|
66
|
+
migrations.AddField(
|
|
67
|
+
model_name="newssource",
|
|
68
|
+
name="clean_content",
|
|
69
|
+
field=models.BooleanField(default=False),
|
|
70
|
+
),
|
|
71
|
+
migrations.AddField(
|
|
72
|
+
model_name="newssource",
|
|
73
|
+
name="is_active",
|
|
74
|
+
field=models.BooleanField(default=True),
|
|
75
|
+
),
|
|
76
|
+
migrations.AddField(
|
|
77
|
+
model_name="newssource",
|
|
78
|
+
name="type",
|
|
79
|
+
field=models.CharField(choices=[("RSS", "RSS"), ("EMAIL", "EMAIL")], default="RSS", max_length=6),
|
|
80
|
+
),
|
|
81
|
+
migrations.AlterField(
|
|
82
|
+
model_name="newssource",
|
|
83
|
+
name="description",
|
|
84
|
+
field=models.TextField(blank=True, default=""),
|
|
85
|
+
),
|
|
86
|
+
migrations.AlterField(
|
|
87
|
+
model_name="newssource",
|
|
88
|
+
name="tags",
|
|
89
|
+
field=django.contrib.postgres.fields.ArrayField(
|
|
90
|
+
base_field=models.CharField(max_length=16), blank=True, default=list, size=None
|
|
91
|
+
),
|
|
92
|
+
),
|
|
93
|
+
migrations.RunPython(migrate_new_source),
|
|
94
|
+
]
|
|
File without changes
|
wbnews/models/news.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from celery import chord, shared_task
|
|
4
|
+
from celery.canvas import Signature
|
|
5
|
+
from django.conf.global_settings import LANGUAGES
|
|
6
|
+
from django.contrib.postgres.fields import ArrayField
|
|
7
|
+
from django.db import models
|
|
8
|
+
from django.db.models.signals import post_save
|
|
9
|
+
from django.dispatch import receiver
|
|
10
|
+
from django.utils.translation import gettext_lazy as _
|
|
11
|
+
from wbcore.contrib.ai.llm.decorators import llm
|
|
12
|
+
from wbcore.contrib.io.mixins import ImportMixin
|
|
13
|
+
from wbcore.models import WBModel
|
|
14
|
+
from wbnews.import_export.handlers.news import NewsImportHandler
|
|
15
|
+
from wbnews.models.llm.cleaned_news import clean_news_config, summarized_news_config
|
|
16
|
+
from wbnews.models.relationships import NewsRelationship
|
|
17
|
+
from wbnews.models.sources import NewsSource
|
|
18
|
+
from wbnews.signals import create_news_relationships
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@shared_task
|
|
22
|
+
def create_relationship(chain_results: list[list[dict[str, Any]]], news_id: int):
|
|
23
|
+
objs = []
|
|
24
|
+
for relationships in chain_results:
|
|
25
|
+
for relationship in relationships:
|
|
26
|
+
objs.append(NewsRelationship(news_id=news_id, **relationship))
|
|
27
|
+
NewsRelationship.objects.bulk_create(
|
|
28
|
+
objs,
|
|
29
|
+
ignore_conflicts=True,
|
|
30
|
+
unique_fields=["content_type", "object_id", "news"],
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@llm([clean_news_config, summarized_news_config])
|
|
35
|
+
class News(ImportMixin, WBModel):
|
|
36
|
+
errors = {
|
|
37
|
+
"relationship_signal": "using the fetch_new_relationships signal must return a list of tuples, sender: {0} did not."
|
|
38
|
+
}
|
|
39
|
+
import_export_handler_class = NewsImportHandler
|
|
40
|
+
|
|
41
|
+
datetime = models.DateTimeField(verbose_name=_("Datetime"))
|
|
42
|
+
title = models.CharField(max_length=500, verbose_name=_("Title"))
|
|
43
|
+
description = models.TextField(blank=True, verbose_name=_("Description"))
|
|
44
|
+
summary = models.TextField(blank=True, verbose_name=_("Summary"))
|
|
45
|
+
language = models.CharField(max_length=16, choices=LANGUAGES, blank=True, null=True, verbose_name=_("Language"))
|
|
46
|
+
link = models.CharField(max_length=500, blank=True, null=True, verbose_name=_("Link"))
|
|
47
|
+
tags = ArrayField(models.CharField(max_length=16), default=list)
|
|
48
|
+
enclosures = ArrayField(models.URLField(), default=list)
|
|
49
|
+
source = models.ForeignKey(
|
|
50
|
+
"wbnews.NewsSource", on_delete=models.CASCADE, related_name="news", verbose_name=_("Source")
|
|
51
|
+
)
|
|
52
|
+
image_url = models.URLField(blank=True, null=True)
|
|
53
|
+
|
|
54
|
+
class Meta:
|
|
55
|
+
unique_together = ["title", "source", "datetime"]
|
|
56
|
+
|
|
57
|
+
def __str__(self) -> str:
|
|
58
|
+
return f"{self.title} ({self.source.title})"
|
|
59
|
+
|
|
60
|
+
def update_and_create_news_relationships(self, synchronous: bool = False):
|
|
61
|
+
"""
|
|
62
|
+
This methods fires the signal to fetch the possible relationship to be linked to the news
|
|
63
|
+
"""
|
|
64
|
+
tasks = []
|
|
65
|
+
for sender, task_signature in create_news_relationships.send(sender=News, instance=self):
|
|
66
|
+
assert isinstance(task_signature, Signature), self.errors["relationship_signal"].format(sender)
|
|
67
|
+
tasks.append(task_signature)
|
|
68
|
+
if tasks:
|
|
69
|
+
res = chord(tasks, create_relationship.s(self.id))
|
|
70
|
+
if synchronous:
|
|
71
|
+
res.apply()
|
|
72
|
+
else:
|
|
73
|
+
res.apply_async()
|
|
74
|
+
|
|
75
|
+
# TODO: Consider moving this into a get_or_create queryset method on NewsSource?
|
|
76
|
+
@classmethod
|
|
77
|
+
def source_dict_to_model(cls, data: dict) -> NewsSource:
|
|
78
|
+
sources = NewsSource.objects
|
|
79
|
+
if "id" in data:
|
|
80
|
+
return sources.get(id=data["id"])
|
|
81
|
+
if identifier := data.get("identifier"):
|
|
82
|
+
sources = sources.filter(identifier=identifier)
|
|
83
|
+
elif url := data.get("url"):
|
|
84
|
+
sources = sources.filter(url=url)
|
|
85
|
+
elif title := data.get("title"):
|
|
86
|
+
sources = sources.filter(title=title)
|
|
87
|
+
if sources.count() == 1:
|
|
88
|
+
return sources.first()
|
|
89
|
+
else:
|
|
90
|
+
return NewsSource.objects.create(**data)
|
|
91
|
+
|
|
92
|
+
@classmethod
|
|
93
|
+
def get_representation_endpoint(cls) -> str:
|
|
94
|
+
return "wbnews:news-list"
|
|
95
|
+
|
|
96
|
+
@classmethod
|
|
97
|
+
def get_representation_value_key(cls) -> str:
|
|
98
|
+
return "id"
|
|
99
|
+
|
|
100
|
+
@classmethod
|
|
101
|
+
def get_representation_label_key(cls) -> str:
|
|
102
|
+
return "{{title}} ({{datetime}})"
|
|
103
|
+
|
|
104
|
+
@classmethod
|
|
105
|
+
def get_endpoint_basename(cls) -> str:
|
|
106
|
+
return "wbnews:news"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@receiver(post_save, sender="wbnews.News")
|
|
110
|
+
def post_save_create_news_relationships(sender: type, instance: "News", raw: bool, created: bool, **kwargs):
|
|
111
|
+
"""
|
|
112
|
+
Post save to lazy create relationship between an instrument and a news upon creation
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
if not raw and created:
|
|
116
|
+
instance.update_and_create_news_relationships()
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from django.contrib.contenttypes.fields import GenericForeignKey
|
|
2
|
+
from django.contrib.contenttypes.models import ContentType
|
|
3
|
+
from django.db import models
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class NewsRelationship(models.Model):
|
|
7
|
+
news = models.ForeignKey(to="wbnews.News", related_name="relationships", on_delete=models.CASCADE)
|
|
8
|
+
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
|
|
9
|
+
object_id = models.PositiveIntegerField()
|
|
10
|
+
content_object = GenericForeignKey("content_type", "object_id")
|
|
11
|
+
|
|
12
|
+
sentiment = models.PositiveIntegerField(null=True, blank=True)
|
|
13
|
+
analysis = models.TextField(null=True, blank=True)
|
|
14
|
+
|
|
15
|
+
def __str__(self) -> str:
|
|
16
|
+
return f"{self.news.title} -> {self.content_object}"
|
|
17
|
+
|
|
18
|
+
class Meta:
|
|
19
|
+
verbose_name = "News Relationship"
|
|
20
|
+
indexes = [models.Index(fields=["content_type", "object_id"])]
|
wbnews/models/sources.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from django.contrib.postgres.fields import ArrayField
|
|
2
|
+
from django.db import models
|
|
3
|
+
from wbcore.models import WBModel
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class NewsSource(WBModel):
|
|
7
|
+
class Type(models.TextChoices):
|
|
8
|
+
RSS = "RSS", "RSS"
|
|
9
|
+
EMAIL = "EMAIL", "EMAIL"
|
|
10
|
+
|
|
11
|
+
type = models.CharField(default=Type.RSS, choices=Type.choices, max_length=6)
|
|
12
|
+
title = models.CharField(max_length=255)
|
|
13
|
+
identifier = models.CharField(max_length=255, unique=True, blank=True, null=True)
|
|
14
|
+
tags = ArrayField(models.CharField(max_length=16), default=list, blank=True)
|
|
15
|
+
image = models.URLField(blank=True, null=True)
|
|
16
|
+
description = models.TextField(default="", blank=True)
|
|
17
|
+
author = models.CharField(max_length=255, default="")
|
|
18
|
+
clean_content = models.BooleanField(default=False)
|
|
19
|
+
url = models.URLField(
|
|
20
|
+
blank=True,
|
|
21
|
+
null=True,
|
|
22
|
+
unique=True,
|
|
23
|
+
)
|
|
24
|
+
is_active = models.BooleanField(default=True)
|
|
25
|
+
|
|
26
|
+
def __str__(self):
|
|
27
|
+
return f"{self.title}"
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def get_representation_endpoint(cls) -> str:
|
|
31
|
+
return "wbnews:sourcerepresentation-list"
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def get_representation_value_key(cls) -> str:
|
|
35
|
+
return "id"
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def get_representation_label_key(cls) -> str:
|
|
39
|
+
return "{{title}}"
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
def get_endpoint_basename(cls) -> str:
|
|
43
|
+
return "wbnews:source"
|
wbnews/serializers.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
from django.utils.translation import gettext_lazy as _
|
|
2
|
+
from rest_framework.reverse import reverse
|
|
3
|
+
from wbcore import serializers as wb_serializers
|
|
4
|
+
|
|
5
|
+
from .models import News, NewsSource
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SourceRepresentationSerializer(wb_serializers.RepresentationSerializer):
|
|
9
|
+
_detail = wb_serializers.HyperlinkField(reverse_name="wbnews:source-detail")
|
|
10
|
+
|
|
11
|
+
class Meta:
|
|
12
|
+
model = NewsSource
|
|
13
|
+
fields = ("id", "title", "_detail")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SourceModelSerializer(wb_serializers.ModelSerializer):
|
|
17
|
+
title = wb_serializers.CharField(read_only=True, label=_("Title"))
|
|
18
|
+
identifier = wb_serializers.CharField(read_only=True, label=_("Identifier"))
|
|
19
|
+
image = wb_serializers.CharField(read_only=True)
|
|
20
|
+
description = wb_serializers.CharField(read_only=True, label=_("Description"))
|
|
21
|
+
author = wb_serializers.CharField(read_only=True, label=_("Author"))
|
|
22
|
+
updated = wb_serializers.DateTimeField(read_only=True, label=_("Updated"))
|
|
23
|
+
|
|
24
|
+
@wb_serializers.register_resource()
|
|
25
|
+
def news(self, instance, request, user):
|
|
26
|
+
return {"news": reverse("wbnews:source-news-list", args=[instance.id], request=request)}
|
|
27
|
+
|
|
28
|
+
class Meta:
|
|
29
|
+
model = NewsSource
|
|
30
|
+
fields = ("id", "title", "identifier", "image", "description", "author", "updated", "_additional_resources")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class NewsRepresentationSerializer(wb_serializers.RepresentationSerializer):
|
|
34
|
+
_detail = wb_serializers.HyperlinkField(reverse_name="wbnews:news-detail")
|
|
35
|
+
|
|
36
|
+
class Meta:
|
|
37
|
+
model = News
|
|
38
|
+
fields = ("id", "datetime", "title", "_detail")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class NewsModelSerializer(wb_serializers.ModelSerializer):
|
|
42
|
+
_source = SourceRepresentationSerializer(source="source")
|
|
43
|
+
image_url = wb_serializers.ImageURLField()
|
|
44
|
+
|
|
45
|
+
@wb_serializers.register_resource()
|
|
46
|
+
def open_link(self, instance, request, user):
|
|
47
|
+
if instance.link:
|
|
48
|
+
return {"open_link": instance.link}
|
|
49
|
+
return {}
|
|
50
|
+
|
|
51
|
+
# link = wb_serializers.URL()
|
|
52
|
+
class Meta:
|
|
53
|
+
model = News
|
|
54
|
+
fields = (
|
|
55
|
+
"id",
|
|
56
|
+
"datetime",
|
|
57
|
+
"title",
|
|
58
|
+
"description",
|
|
59
|
+
"summary",
|
|
60
|
+
"link",
|
|
61
|
+
"language",
|
|
62
|
+
"image_url",
|
|
63
|
+
"source",
|
|
64
|
+
"_source",
|
|
65
|
+
"_additional_resources",
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class NewsRelationshipModelSerializer(wb_serializers.ModelSerializer):
|
|
70
|
+
sentiment = wb_serializers.IntegerField(required=False)
|
|
71
|
+
analysis = wb_serializers.TextField(required=False)
|
|
72
|
+
|
|
73
|
+
class Meta:
|
|
74
|
+
model = News
|
|
75
|
+
fields = (
|
|
76
|
+
"id",
|
|
77
|
+
"datetime",
|
|
78
|
+
"sentiment",
|
|
79
|
+
"analysis",
|
|
80
|
+
"title",
|
|
81
|
+
"description",
|
|
82
|
+
"summary",
|
|
83
|
+
)
|
wbnews/signals.py
ADDED
wbnews/tests/__init__.py
ADDED
|
File without changes
|
wbnews/tests/conftest.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
@pytest.mark.django_db
|
|
5
|
+
class TestSource:
|
|
6
|
+
@pytest.mark.parametrize("news_source__title", ["source1"])
|
|
7
|
+
def test_str(self, news_source):
|
|
8
|
+
assert str(news_source) == f"{news_source.title}"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@pytest.mark.django_db
|
|
12
|
+
class TestNews:
|
|
13
|
+
@pytest.mark.parametrize("news__title", ["new1"])
|
|
14
|
+
def test_str(self, news):
|
|
15
|
+
assert str(news) == f"{news.title} ({news.source.title})"
|
wbnews/tests/tests.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from wbcore.test import GenerateTest, default_config
|
|
3
|
+
|
|
4
|
+
config = {}
|
|
5
|
+
for key, value in default_config.items():
|
|
6
|
+
config[key] = list(filter(lambda x: x.__module__.startswith("wbnews"), value))
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@pytest.mark.django_db
|
|
10
|
+
@GenerateTest(config)
|
|
11
|
+
class TestProject:
|
|
12
|
+
pass
|
wbnews/urls.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from django.urls import include, path
|
|
2
|
+
from wbcore.routers import WBCoreRouter
|
|
3
|
+
from wbnews.viewsets import views
|
|
4
|
+
|
|
5
|
+
router = WBCoreRouter()
|
|
6
|
+
router.register(r"newsrepresentation", views.NewsRepresentationViewSet, basename="newsrepresentation")
|
|
7
|
+
router.register(r"newssourcerepresentation", views.SourceRepresentationViewSet, basename="sourcerepresentation")
|
|
8
|
+
router.register(r"news", views.NewsModelViewSet, basename="news")
|
|
9
|
+
router.register(r"newssource", views.SourceModelViewSet, basename="source")
|
|
10
|
+
router.register(r"newsrelationship", views.NewsRelationshipModelViewSet, basename="newsrelationship")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
source_router = WBCoreRouter()
|
|
14
|
+
source_router.register(r"news", views.NewsSourceModelViewSet, basename="source-news")
|
|
15
|
+
|
|
16
|
+
urlpatterns = [
|
|
17
|
+
path("", include(router.urls)),
|
|
18
|
+
path("source/<int:source_id>/", include(source_router.urls)),
|
|
19
|
+
path(
|
|
20
|
+
"contentnews/<int:content_type>/<int:content_id>/",
|
|
21
|
+
views.NewsModelViewSet.as_view({"get": "list"}),
|
|
22
|
+
name="news_content_object",
|
|
23
|
+
),
|
|
24
|
+
path(
|
|
25
|
+
"contentnewsrelationship/<int:content_type>/<int:content_id>/",
|
|
26
|
+
views.NewsRelationshipModelViewSet.as_view({"get": "list"}),
|
|
27
|
+
name="news_relationship_content_object",
|
|
28
|
+
),
|
|
29
|
+
]
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from .buttons import NewsButtonConfig
|
|
2
|
+
from .display import NewsDisplayConfig, NewsSourceDisplayConfig, SourceDisplayConfig
|
|
3
|
+
from .endpoints import NewsEndpointConfig, NewsSourceEndpointConfig
|
|
4
|
+
from .menu import NEWS_MENUITEM, NEWSSOURCE_MENUITEM
|
|
5
|
+
from .titles import NewsSourceModelTitleConfig, NewsTitleConfig, SourceModelTitleConfig
|
|
6
|
+
from .views import (
|
|
7
|
+
NewsModelViewSet,
|
|
8
|
+
NewsRepresentationViewSet,
|
|
9
|
+
SourceModelViewSet,
|
|
10
|
+
SourceRepresentationViewSet,
|
|
11
|
+
NewsRelationshipModelViewSet,
|
|
12
|
+
)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from django.dispatch import receiver
|
|
2
|
+
from django.utils.translation import gettext as _
|
|
3
|
+
from rest_framework.reverse import reverse
|
|
4
|
+
from wbcore.contrib.icons import WBIcon
|
|
5
|
+
from wbcore.metadata.configs import buttons as bt
|
|
6
|
+
from wbcore.metadata.configs.buttons.view_config import ButtonViewConfig
|
|
7
|
+
from wbcore.signals.instance_buttons import add_extra_button
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class NewsButtonConfig(ButtonViewConfig):
|
|
11
|
+
def get_custom_list_instance_buttons(self):
|
|
12
|
+
return {bt.HyperlinkButton(key="open_link", label=_("Open News"), icon=WBIcon.LINK.icon)}
|
|
13
|
+
|
|
14
|
+
def get_custom_instance_buttons(self):
|
|
15
|
+
return self.get_custom_list_instance_buttons()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@receiver(add_extra_button)
|
|
19
|
+
def add_new_extra_button(sender, instance, request, view, pk=None, **kwargs):
|
|
20
|
+
if instance and pk and view:
|
|
21
|
+
content_type = view.get_content_type()
|
|
22
|
+
endpoint = reverse("wbnews:news_relationship_content_object", args=[content_type.id, pk], request=request)
|
|
23
|
+
return bt.WidgetButton(endpoint=endpoint, label="News", icon=WBIcon.NEWSPAPER.icon)
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from django.utils.translation import gettext as _
|
|
4
|
+
from wbcore.metadata.configs import display as dp
|
|
5
|
+
from wbcore.metadata.configs.display.instance_display.shortcuts import (
|
|
6
|
+
Display,
|
|
7
|
+
create_simple_display,
|
|
8
|
+
create_simple_section,
|
|
9
|
+
)
|
|
10
|
+
from wbcore.metadata.configs.display.instance_display.utils import repeat_field
|
|
11
|
+
from wbcore.metadata.configs.display.view_config import DisplayViewConfig
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SourceDisplayConfig(DisplayViewConfig):
|
|
15
|
+
def get_list_display(self) -> Optional[dp.ListDisplay]:
|
|
16
|
+
return dp.ListDisplay(
|
|
17
|
+
fields=[
|
|
18
|
+
dp.Field(key="title", label=_("Title")),
|
|
19
|
+
dp.Field(key="identifier", label=_("RSS feed")),
|
|
20
|
+
dp.Field(key="author", label=_("Author")),
|
|
21
|
+
dp.Field(key="description", label=_("Description")),
|
|
22
|
+
dp.Field(key="updated", label=_("Last Update")),
|
|
23
|
+
]
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
def get_instance_display(self) -> Display:
|
|
27
|
+
return create_simple_display(
|
|
28
|
+
[
|
|
29
|
+
["title", "identifier"],
|
|
30
|
+
["author", "updated"],
|
|
31
|
+
[repeat_field(2, "description")],
|
|
32
|
+
[repeat_field(2, "news_section")],
|
|
33
|
+
],
|
|
34
|
+
[create_simple_section("news_section", "News", [["news"]], "news", collapsed=False)],
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class NewsDisplayConfig(DisplayViewConfig):
|
|
39
|
+
def get_instance_display(self) -> Display:
|
|
40
|
+
return create_simple_display(
|
|
41
|
+
[
|
|
42
|
+
["datetime", "source"],
|
|
43
|
+
["language", "link"],
|
|
44
|
+
[repeat_field(2, "summary")],
|
|
45
|
+
[repeat_field(2, "description")],
|
|
46
|
+
]
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
def get_list_display(self) -> Optional[dp.ListDisplay]:
|
|
50
|
+
return dp.ListDisplay(
|
|
51
|
+
fields=[
|
|
52
|
+
dp.Field(key="datetime", label=_("Datetime")),
|
|
53
|
+
dp.Field(key="title", label=_("Title")),
|
|
54
|
+
dp.Field(key="summary", label=_("Summary")),
|
|
55
|
+
dp.Field(key="description", label=_("Description")),
|
|
56
|
+
# dp.Field(key="tags", label=_("Edited")),
|
|
57
|
+
dp.Field(key="source", label=_("Source")),
|
|
58
|
+
dp.Field(key="language", label=_("Language")),
|
|
59
|
+
dp.Field(key="image_url", label=_("Image")),
|
|
60
|
+
]
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class NewsRelationshipDisplayConfig(DisplayViewConfig):
|
|
65
|
+
def get_instance_display(self) -> Display:
|
|
66
|
+
return create_simple_display(
|
|
67
|
+
[
|
|
68
|
+
["datetime", "source"],
|
|
69
|
+
["language", "link"],
|
|
70
|
+
[repeat_field(2, "summary")],
|
|
71
|
+
[repeat_field(2, "description")],
|
|
72
|
+
]
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
def get_list_display(self) -> Optional[dp.ListDisplay]:
|
|
76
|
+
POSITIVE = "#96DD99"
|
|
77
|
+
SLIGHTLY_POSITIVE = "#FFEE8C"
|
|
78
|
+
SLIGHTLY_NEGATIVE = "#FF964F"
|
|
79
|
+
NEGATIVE = "#FF6961"
|
|
80
|
+
|
|
81
|
+
return dp.ListDisplay(
|
|
82
|
+
fields=[
|
|
83
|
+
dp.Field(key="datetime", label=_("Datetime")),
|
|
84
|
+
dp.Field(key="analysis", label=_("Analysis")),
|
|
85
|
+
dp.Field(key="title", label=_("Title")),
|
|
86
|
+
dp.Field(key="summary", label=_("Summary")),
|
|
87
|
+
dp.Field(key="description", label=_("Description")),
|
|
88
|
+
],
|
|
89
|
+
formatting=[
|
|
90
|
+
dp.Formatting(
|
|
91
|
+
column="sentiment",
|
|
92
|
+
formatting_rules=[
|
|
93
|
+
dp.FormattingRule(condition=("==", 4), style={"backgroundColor": POSITIVE}),
|
|
94
|
+
dp.FormattingRule(condition=("==", 3), style={"backgroundColor": SLIGHTLY_POSITIVE}),
|
|
95
|
+
dp.FormattingRule(condition=("==", 2), style={"backgroundColor": SLIGHTLY_NEGATIVE}),
|
|
96
|
+
dp.FormattingRule(condition=("==", 1), style={"backgroundColor": NEGATIVE}),
|
|
97
|
+
],
|
|
98
|
+
)
|
|
99
|
+
],
|
|
100
|
+
legends=[
|
|
101
|
+
dp.Legend(
|
|
102
|
+
items=[
|
|
103
|
+
dp.LegendItem(icon=POSITIVE, label=_("Positive")),
|
|
104
|
+
dp.LegendItem(icon=SLIGHTLY_POSITIVE, label=_("Slightly Positive")),
|
|
105
|
+
dp.LegendItem(icon=SLIGHTLY_NEGATIVE, label=_("Slightly Negative")),
|
|
106
|
+
dp.LegendItem(icon=NEGATIVE, label=_("Negative")),
|
|
107
|
+
]
|
|
108
|
+
)
|
|
109
|
+
],
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class NewsSourceDisplayConfig(DisplayViewConfig):
|
|
114
|
+
def get_instance_display(self) -> Display:
|
|
115
|
+
return create_simple_display(
|
|
116
|
+
[
|
|
117
|
+
[repeat_field(2, "title")],
|
|
118
|
+
["datetime", "language", "link"],
|
|
119
|
+
[repeat_field(2, "description")],
|
|
120
|
+
]
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
def get_list_display(self) -> Optional[dp.ListDisplay]:
|
|
124
|
+
return dp.ListDisplay(
|
|
125
|
+
fields=[
|
|
126
|
+
dp.Field(key="datetime", label=_("Datetime")),
|
|
127
|
+
dp.Field(key="title", label=_("Title")),
|
|
128
|
+
dp.Field(key="description", label=_("Description")),
|
|
129
|
+
dp.Field(key="language", label=_("Language")),
|
|
130
|
+
# dp.Field(key="tags", label="_(Edited")),
|
|
131
|
+
# dp.Field(key="link", label="_(Link"))
|
|
132
|
+
]
|
|
133
|
+
)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from rest_framework.reverse import reverse
|
|
2
|
+
from wbcore.metadata.configs.endpoints import EndpointViewConfig
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class NewsEndpointConfig(EndpointViewConfig):
|
|
6
|
+
def get_endpoint(self, **kwargs):
|
|
7
|
+
return None
|
|
8
|
+
|
|
9
|
+
def get_list_endpoint(self, **kwargs):
|
|
10
|
+
return reverse("wbnews:news-list", request=self.request)
|
|
11
|
+
|
|
12
|
+
def get_instance_endpoint(self, **kwargs):
|
|
13
|
+
return self.get_list_endpoint()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class NewsSourceEndpointConfig(NewsEndpointConfig):
|
|
17
|
+
def get_list_endpoint(self, **kwargs):
|
|
18
|
+
return reverse("wbnews:source-news-list", args=[self.view.kwargs["source_id"]], request=self.request)
|
wbnews/viewsets/menu.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from django.utils.translation import gettext_lazy as _
|
|
2
|
+
from wbcore.menus import ItemPermission, MenuItem
|
|
3
|
+
from wbcore.permissions.shortcuts import is_internal_user
|
|
4
|
+
|
|
5
|
+
NEWS_MENUITEM = MenuItem(
|
|
6
|
+
label=_("News"),
|
|
7
|
+
endpoint="wbnews:news-list",
|
|
8
|
+
permission=ItemPermission(permissions=["wbnews.view_news"]),
|
|
9
|
+
)
|
|
10
|
+
NEWSSOURCE_MENUITEM = MenuItem(
|
|
11
|
+
label=_("Sources"),
|
|
12
|
+
endpoint="wbnews:source-list",
|
|
13
|
+
permission=ItemPermission(
|
|
14
|
+
method=lambda request: is_internal_user(request.user), permissions=["wbnews.view_newssource"]
|
|
15
|
+
),
|
|
16
|
+
add=MenuItem(
|
|
17
|
+
label=_("Create Source"),
|
|
18
|
+
endpoint="wbnews:source-list",
|
|
19
|
+
permission=ItemPermission(
|
|
20
|
+
method=lambda request: is_internal_user(request.user), permissions=["wbnews.add_newssource"]
|
|
21
|
+
),
|
|
22
|
+
),
|
|
23
|
+
)
|