Implement Feed creation

This commit is contained in:
Tobias Eidelpes 2021-05-02 17:49:42 +02:00
parent b96a8d35c5
commit e648a03166
13 changed files with 186 additions and 113 deletions

1
backend/.gitignore vendored
View File

@ -1,6 +1,7 @@
venv venv
*.pyc *.pyc
staticfiles staticfiles
media
.env .env
*.sqlite3 *.sqlite3
*.sqlite *.sqlite

View File

@ -1,26 +1,25 @@
from django.core.validators import URLValidator, FileExtensionValidator
from django.db import models from django.db import models
# Create your models here.
class User(models.Model): class User(models.Model):
pass pass
class Icon(models.Model):
image = models.ImageField(upload_to='feed_icons')
class Feed(models.Model): class Feed(models.Model):
url = models.CharField(max_length=100) url = models.TextField(blank=False, null=False, validators=[URLValidator(['http', 'https'])])
active = models.BooleanField() active = models.BooleanField()
icon = models.FileField(upload_to='feed-icons', blank=True, null=True,
validators=[FileExtensionValidator(['png', 'svg'])])
keywords = models.TextField(blank=False, null=False)
class FeedEntry(models.Model): class FeedEntry(models.Model):
feed = models.ForeignKey(Feed, on_delete=models.CASCADE)
tweeted = models.BooleanField() tweeted = models.BooleanField()
class Tweet(models.Model): class Tweet(models.Model):
icon = models.ForeignKey(Icon, on_delete=models.CASCADE)
text = models.CharField(max_length=137) text = models.CharField(max_length=137)
date_time = models.DateTimeField() date_time = models.DateTimeField()
url = models.CharField(max_length=100) url = models.CharField(max_length=100)

View File

@ -1,3 +1,26 @@
from rest_framework import serializers from rest_framework.exceptions import ValidationError
from rest_framework.serializers import ModelSerializer
# add serializer here from app_be.models import Feed
class FeedSerializer(ModelSerializer):
class Meta:
model = Feed
fields = '__all__'
def validate_icon(self, value):
if value is not None and value.size > 10240:
raise ValidationError("Invalid icon: Maximum size is 10KB")
return value
def validate_keywords(self, value):
split = [x.strip() for x in value.split(',')]
if len(split) > 3:
raise ValidationError("Invalid keywords: No more than three entries")
elif len(split) == 0:
raise ValidationError("Invalid keywords: Need at least one entry")
for entry in split:
if len(entry) < 3:
raise ValidationError("Invalid keywords: Keywords have to be of length greater than 2")
return value

View File

@ -161,6 +161,9 @@ STATICFILES_DIRS = [
os.path.join(PROJECT_ROOT, 'static'), os.path.join(PROJECT_ROOT, 'static'),
] ]
MEDIA_ROOT = os.path.join(PROJECT_ROOT, 'media')
MEDIA_URL = '/media/'
# Simplified static file serving. # Simplified static file serving.
# https://warehouse.python.org/project/whitenoise/ # https://warehouse.python.org/project/whitenoise/
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage' STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'

View File

@ -28,5 +28,6 @@ urlpatterns = [
] ]
router = DefaultRouter() router = DefaultRouter()
router.register(r'feeds', FeedViewSet, basename='feeds')
urlpatterns.extend(router.urls) urlpatterns.extend(router.urls)

View File

@ -2,9 +2,14 @@ import logging
from django.http import JsonResponse from django.http import JsonResponse
from rest_framework.decorators import api_view
from py_jwt_validator import PyJwtValidator, PyJwtException from py_jwt_validator import PyJwtValidator, PyJwtException
from rest_framework.decorators import api_view
from rest_framework.viewsets import ModelViewSet
from app_be.models import Feed
from app_be.serializers import FeedSerializer
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -58,3 +63,8 @@ class TwitterClass:
def getLastSixTweets(): def getLastSixTweets():
return JsonResponse({[{"asdf", "asdf", "sdfasdf", "asdf"}, {"asdf", "asdf", "sdfasdf", "asdf"}]}, safe=False, return JsonResponse({[{"asdf", "asdf", "sdfasdf", "asdf"}, {"asdf", "asdf", "sdfasdf", "asdf"}]}, safe=False,
status=200) status=200)
class FeedViewSet(ModelViewSet):
queryset = Feed.objects.all()
serializer_class = FeedSerializer

View File

@ -39,10 +39,10 @@ class twitter_bot:
def scan_active_feed(self): def scan_active_feed(self):
starttime = time.time() starttime = time.time()
while True: # while True:
# Search RSS Feed # Search RSS Feed
time.sleep(60.0 - ((time.time() - starttime) % 60.0)) # time.sleep(60.0 - ((time.time() - starttime) % 60.0))
twitter_bot = twitter_bot() twitter_bot = twitter_bot()

View File

@ -7,9 +7,9 @@ services:
container_name: waecm_g4_be_container container_name: waecm_g4_be_container
hostname: waecm_g4_be hostname: waecm_g4_be
image: pfingstfrosch/waecm-2021-group-04-bsp-1-be image: pfingstfrosch/waecm-2021-group-04-bsp-1-be
# build: build:
# context: ./backend context: ./backend
# dockerfile: ./Dockerfile dockerfile: ./Dockerfile
command: python manage.py runserver 0.0.0.0:8000 command: python manage.py runserver 0.0.0.0:8000
ports: ports:
- 8000:8000 - 8000:8000
@ -18,8 +18,8 @@ services:
container_name: waecm_g4_fe_container container_name: waecm_g4_fe_container
hostname: waecm_g4_fe hostname: waecm_g4_fe
image: pfingstfrosch/waecm-2021-group-04-bsp-1-fe image: pfingstfrosch/waecm-2021-group-04-bsp-1-fe
# build: build:
# context: ./frontend context: ./frontend
# dockerfile: ./Dockerfile dockerfile: ./Dockerfile
ports: ports:
- 4200:80 - 4200:80

View File

@ -57,6 +57,7 @@
"karma-coverage-istanbul-reporter": "~2.1.1", "karma-coverage-istanbul-reporter": "~2.1.1",
"karma-jasmine": "~2.0.1", "karma-jasmine": "~2.0.1",
"karma-jasmine-html-reporter": "^1.5.4", "karma-jasmine-html-reporter": "^1.5.4",
"ngx-material-file-input": "^2.1.1",
"protractor": "~5.4.4", "protractor": "~5.4.4",
"ts-node": "~8.5.4", "ts-node": "~8.5.4",
"tslint": "~5.20.1", "tslint": "~5.20.1",
@ -431,9 +432,6 @@
"version": "9.2.4", "version": "9.2.4",
"resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-9.2.4.tgz", "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-9.2.4.tgz",
"integrity": "sha512-iw2+qHMXHYVC6K/fttHeNHIieSKiTEodVutZoOEcBu9rmRTGbLB26V/CRsfIRmA1RBk+uFYWc6UQZnMC3RdnJQ==", "integrity": "sha512-iw2+qHMXHYVC6K/fttHeNHIieSKiTEodVutZoOEcBu9rmRTGbLB26V/CRsfIRmA1RBk+uFYWc6UQZnMC3RdnJQ==",
"dependencies": {
"parse5": "^5.0.0"
},
"optionalDependencies": { "optionalDependencies": {
"parse5": "^5.0.0" "parse5": "^5.0.0"
} }
@ -969,7 +967,6 @@
"dependencies": { "dependencies": {
"anymatch": "~3.1.1", "anymatch": "~3.1.1",
"braces": "~3.0.2", "braces": "~3.0.2",
"fsevents": "~2.1.2",
"glob-parent": "~5.1.0", "glob-parent": "~5.1.0",
"is-binary-path": "~2.1.0", "is-binary-path": "~2.1.0",
"is-glob": "~4.0.1", "is-glob": "~4.0.1",
@ -2149,7 +2146,6 @@
"dependencies": { "dependencies": {
"anymatch": "~3.1.1", "anymatch": "~3.1.1",
"braces": "~3.0.2", "braces": "~3.0.2",
"fsevents": "~2.1.2",
"glob-parent": "~5.1.0", "glob-parent": "~5.1.0",
"is-binary-path": "~2.1.0", "is-binary-path": "~2.1.0",
"is-glob": "~4.0.1", "is-glob": "~4.0.1",
@ -3702,8 +3698,7 @@
"dependencies": { "dependencies": {
"esprima": "~1.0.4", "esprima": "~1.0.4",
"estraverse": "~1.5.0", "estraverse": "~1.5.0",
"esutils": "~1.0.0", "esutils": "~1.0.0"
"source-map": "~0.1.30"
}, },
"optionalDependencies": { "optionalDependencies": {
"source-map": "~0.1.30" "source-map": "~0.1.30"
@ -6542,8 +6537,7 @@
"esprima": "^4.0.1", "esprima": "^4.0.1",
"estraverse": "^4.2.0", "estraverse": "^4.2.0",
"esutils": "^2.0.2", "esutils": "^2.0.2",
"optionator": "^0.8.1", "optionator": "^0.8.1"
"source-map": "~0.6.1"
}, },
"optionalDependencies": { "optionalDependencies": {
"source-map": "~0.6.1" "source-map": "~0.6.1"
@ -7559,7 +7553,6 @@
"minimist": "^1.2.5", "minimist": "^1.2.5",
"neo-async": "^2.6.0", "neo-async": "^2.6.0",
"source-map": "^0.6.1", "source-map": "^0.6.1",
"uglify-js": "^3.1.4",
"wordwrap": "^1.0.0" "wordwrap": "^1.0.0"
}, },
"optionalDependencies": { "optionalDependencies": {
@ -9279,7 +9272,6 @@
"dependencies": { "dependencies": {
"anymatch": "~3.1.1", "anymatch": "~3.1.1",
"braces": "~3.0.2", "braces": "~3.0.2",
"fsevents": "~2.1.2",
"glob-parent": "~5.1.0", "glob-parent": "~5.1.0",
"is-binary-path": "~2.1.0", "is-binary-path": "~2.1.0",
"is-glob": "~4.0.1", "is-glob": "~4.0.1",
@ -9419,12 +9411,8 @@
"clone": "^2.1.2", "clone": "^2.1.2",
"errno": "^0.1.1", "errno": "^0.1.1",
"graceful-fs": "^4.1.2", "graceful-fs": "^4.1.2",
"image-size": "~0.5.0",
"make-dir": "^2.1.0",
"mime": "^1.4.1", "mime": "^1.4.1",
"promise": "^7.1.1",
"request": "^2.83.0", "request": "^2.83.0",
"source-map": "~0.6.0",
"tslib": "^1.10.0" "tslib": "^1.10.0"
}, },
"optionalDependencies": { "optionalDependencies": {
@ -9644,7 +9632,6 @@
"anymatch": "^2.0.0", "anymatch": "^2.0.0",
"async-each": "^1.0.1", "async-each": "^1.0.1",
"braces": "^2.3.2", "braces": "^2.3.2",
"fsevents": "^1.2.7",
"glob-parent": "^3.1.0", "glob-parent": "^3.1.0",
"inherits": "^2.0.3", "inherits": "^2.0.3",
"is-binary-path": "^1.0.0", "is-binary-path": "^1.0.0",
@ -10727,6 +10714,19 @@
"vlq": "^1.0.0" "vlq": "^1.0.0"
} }
}, },
"node_modules/ngx-material-file-input": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ngx-material-file-input/-/ngx-material-file-input-2.1.1.tgz",
"integrity": "sha512-FbaIjiJnL6BZtZYWLvMSn9aSaM62AZaJegloTUphmLz5jopXPzE5W+3aC+dsf9h1IIqHSCLcyv0w+qH0ypBhMA==",
"dev": true,
"peerDependencies": {
"@angular/cdk": "^8.1.1 || ^9.0.0",
"@angular/common": "^8.1.3 || ^9.0.0",
"@angular/core": "^8.1.3 || ^9.0.0",
"@angular/material": "^8.1.1 || ^9.0.0",
"tslib": "^1.10.0"
}
},
"node_modules/nice-try": { "node_modules/nice-try": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
@ -13626,9 +13626,6 @@
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.1.0.tgz", "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.1.0.tgz",
"integrity": "sha512-gfE1455AEazVVTJoeQtcOq/U6GSxwoj4XPSWVsuWmgIxj7sBQNLDOSA82PbdMe+cP8ql8fR1jogPFe8Wg8g4SQ==", "integrity": "sha512-gfE1455AEazVVTJoeQtcOq/U6GSxwoj4XPSWVsuWmgIxj7sBQNLDOSA82PbdMe+cP8ql8fR1jogPFe8Wg8g4SQ==",
"dev": true, "dev": true,
"dependencies": {
"fsevents": "~2.1.2"
},
"optionalDependencies": { "optionalDependencies": {
"fsevents": "~2.1.2" "fsevents": "~2.1.2"
} }
@ -13775,7 +13772,6 @@
"dependencies": { "dependencies": {
"anymatch": "~3.1.1", "anymatch": "~3.1.1",
"braces": "~3.0.2", "braces": "~3.0.2",
"fsevents": "~2.1.2",
"glob-parent": "~5.1.0", "glob-parent": "~5.1.0",
"is-binary-path": "~2.1.0", "is-binary-path": "~2.1.0",
"is-glob": "~4.0.1", "is-glob": "~4.0.1",
@ -16518,10 +16514,8 @@
"integrity": "sha512-aWAgTW4MoSJzZPAicljkO1hsi1oKj/RRq/OJQh2PKI2UKL04c2Bs+MBOB+BBABHTXJpf9mCwHN7ANCvYsvY2sg==", "integrity": "sha512-aWAgTW4MoSJzZPAicljkO1hsi1oKj/RRq/OJQh2PKI2UKL04c2Bs+MBOB+BBABHTXJpf9mCwHN7ANCvYsvY2sg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"chokidar": "^3.4.1",
"graceful-fs": "^4.1.2", "graceful-fs": "^4.1.2",
"neo-async": "^2.5.0", "neo-async": "^2.5.0"
"watchpack-chokidar2": "^2.0.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"chokidar": "^3.4.1", "chokidar": "^3.4.1",
@ -17227,7 +17221,6 @@
"anymatch": "^2.0.0", "anymatch": "^2.0.0",
"async-each": "^1.0.1", "async-each": "^1.0.1",
"braces": "^2.3.2", "braces": "^2.3.2",
"fsevents": "^1.2.7",
"glob-parent": "^3.1.0", "glob-parent": "^3.1.0",
"inherits": "^2.0.3", "inherits": "^2.0.3",
"is-binary-path": "^1.0.0", "is-binary-path": "^1.0.0",
@ -29344,6 +29337,13 @@
"vlq": "^1.0.0" "vlq": "^1.0.0"
} }
}, },
"ngx-material-file-input": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ngx-material-file-input/-/ngx-material-file-input-2.1.1.tgz",
"integrity": "sha512-FbaIjiJnL6BZtZYWLvMSn9aSaM62AZaJegloTUphmLz5jopXPzE5W+3aC+dsf9h1IIqHSCLcyv0w+qH0ypBhMA==",
"dev": true,
"requires": {}
},
"nice-try": { "nice-try": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",

View File

@ -60,6 +60,7 @@
"karma-coverage-istanbul-reporter": "~2.1.1", "karma-coverage-istanbul-reporter": "~2.1.1",
"karma-jasmine": "~2.0.1", "karma-jasmine": "~2.0.1",
"karma-jasmine-html-reporter": "^1.5.4", "karma-jasmine-html-reporter": "^1.5.4",
"ngx-material-file-input": "^2.1.1",
"protractor": "~5.4.4", "protractor": "~5.4.4",
"ts-node": "~8.5.4", "ts-node": "~8.5.4",
"tslint": "~5.20.1", "tslint": "~5.20.1",

View File

@ -27,6 +27,7 @@ import { NavigationComponent } from './component/navigation/navigation.component
import {MatSnackBarModule} from '@angular/material/snack-bar'; import {MatSnackBarModule} from '@angular/material/snack-bar';
import {MatCheckboxModule} from '@angular/material/checkbox'; import {MatCheckboxModule} from '@angular/material/checkbox';
import { EditierenComponent } from './component/einstellungen/editieren/editieren.component'; import { EditierenComponent } from './component/einstellungen/editieren/editieren.component';
import {MaterialFileInputModule} from 'ngx-material-file-input';
@NgModule({ @NgModule({
declarations: [LandingComponent, LoginComponent, NavigationComponent, declarations: [LandingComponent, LoginComponent, NavigationComponent,
@ -48,7 +49,8 @@ import { EditierenComponent } from './component/einstellungen/editieren/editiere
MatIconModule, MatIconModule,
MatMenuModule, MatMenuModule,
MatSnackBarModule, MatSnackBarModule,
MatCheckboxModule MatCheckboxModule,
MaterialFileInputModule
], ],
// enables injecting // enables injecting
providers: [ providers: [

View File

@ -2,34 +2,42 @@
<div class="content"> <div class="content">
<div class="text-center"> <div class="text-center">
<p class="font-weight-bold">RSS-Feed erstellen</p> <p class="font-weight-bold">RSS-Feed erstellen</p>
<form [formGroup]="feedForm" enctype="multipart/form-data">
<div class=" input-row"> <div class=" input-row">
<mat-form-field appearance="standard" class="input"> <mat-form-field appearance="standard" class="input">
<mat-label>Vollständige URL des RSS-Feeds:</mat-label> <mat-label>Vollständige URL des RSS-Feeds</mat-label>
<input matInput placeholder="https://rss.orf.at/news.xml"> <input matInput formControlName="url" placeholder="https://rss.orf.at/news.xml">
</mat-form-field> </mat-form-field>
</div> </div>
<div class="input-row text-left"> <div class="input-row text-left">
<mat-form-field appearance="standard" class="input"> <mat-form-field appearance="standard" class="input">
<mat-label>Folgende Stichwörter im Feed suchen:</mat-label> <mat-label>Gesuchte Stichwörter</mat-label>
<input matInput placeholder="Spiel, Spaß, Schokolade"> <input matInput formControlName="keywords" placeholder="Spiel,Spaß,Schokolade">
</mat-form-field> </mat-form-field>
<mat-checkbox>Alle Stichworte müssen enthalten sein</mat-checkbox> <mat-checkbox>Alle Stichworte müssen enthalten sein</mat-checkbox>
</div> </div>
<div class="input-row text-left"> <div class="input-row text-left">
<span>Optionales Icon:</span> <mat-form-field class="col">
<br> <ngx-mat-file-input formControlName="icon" placeholder="Optionales Icon"
<img (click)="iconChooser()" class="feed-icon" src="{{icon}}" alt="Feed-Icon"> ></ngx-mat-file-input>
<input hidden type="file" <mat-icon matSuffix>folder</mat-icon>
(change)="fileChangeEvent($event)" <mat-error *ngIf="feedForm.get('icon').hasError('maxContentSize')">
id="feed-icon-picker" name="icon" Die maximale Dateigröße ist {{feedForm.get('icon')?.getError('maxContentSize').maxSize | byteFormat}}
accept="image/png, image/svg+xml"> ({{feedForm.get('icon')?.getError('maxContentSize').actualSize
| byteFormat}}).
</mat-error>
</mat-form-field>
</div> </div>
<div class="input-row text-left"> <div class="input-row text-left">
<mat-slide-toggle color="primary">Slide me!</mat-slide-toggle> <mat-slide-toggle color="primary" formControlName="active">Slide me!</mat-slide-toggle>
</div> </div>
<div class="input-row margin-auto einstellungen_buttons_wrapper"> <div class="input-row margin-auto einstellungen_buttons_wrapper">
<button routerLink="/einstellungen" mat-raised-button>Abbrechen</button> <button routerLink="/einstellungen" mat-raised-button>Abbrechen</button>
<button mat-raised-button><mat-icon>save</mat-icon> Speichern</button> <button mat-raised-button (click)="saveFeed(feedForm.value)">
</div> <mat-icon>save</mat-icon>
Speichern
</button>
</div>
</form>
</div> </div>
</div> </div>

View File

@ -1,5 +1,13 @@
import {Component, OnInit} from '@angular/core'; import {Component, OnInit} from '@angular/core';
import {ActivatedRoute} from '@angular/router'; import {ActivatedRoute} from '@angular/router';
import {FormGroup, FormBuilder, FormControl} from '@angular/forms';
import {FileInput, FileValidator} from 'ngx-material-file-input';
import {HttpClient} from '@angular/common/http';
import {environment} from '../../../../environments/environment';
import {throwError} from 'rxjs';
import {NGXLogger} from 'ngx-logger';
import {MatSnackBar} from '@angular/material/snack-bar';
import {isElementScrolledOutsideView} from '@angular/cdk/overlay/position/scroll-clip';
@Component({ @Component({
selector: 'app-editieren', selector: 'app-editieren',
@ -7,13 +15,22 @@ import {ActivatedRoute} from '@angular/router';
styleUrls: ['./editieren.component.css'] styleUrls: ['./editieren.component.css']
}) })
export class EditierenComponent implements OnInit { export class EditierenComponent implements OnInit {
icon;
id; id;
constructor(private route: ActivatedRoute) { feedForm: FormGroup;
this.icon = 'assets/logo.svg'; url: String;
active = true;
icon: File;
keywords: String;
readonly maxSize = 10240;
private currentLocation = environment.location;
constructor(private route: ActivatedRoute,
private formBuilder: FormBuilder,
private http: HttpClient,
private _snackbar: MatSnackBar,
private _logger: NGXLogger) {
this.route.paramMap.subscribe(paramMap => { this.route.paramMap.subscribe(paramMap => {
this.id = paramMap.get('id'); this.id = paramMap.get('id');
if (this.id) { if (this.id) {
@ -23,6 +40,12 @@ export class EditierenComponent implements OnInit {
} }
ngOnInit(): void { ngOnInit(): void {
this.feedForm = this.formBuilder.group({
url: this.url,
active: this.active,
icon: [undefined, [FileValidator.maxContentSize(this.maxSize)]],
keywords: this.keywords
});
} }
loadFeed(id) { loadFeed(id) {
@ -33,22 +56,24 @@ export class EditierenComponent implements OnInit {
// TODO: bei Input-Words die Leerzeichen vor- und nach dem letzten Zeichen entfernen: " Formel 1 " wird zu "Formel 1" // TODO: bei Input-Words die Leerzeichen vor- und nach dem letzten Zeichen entfernen: " Formel 1 " wird zu "Formel 1"
iconChooser() { saveFeed(feedData) {
document.getElementById('feed-icon-picker').click(); const form: FormData = new FormData();
} form.append('url', feedData.url);
form.append('active', feedData.active);
fileChangeEvent(event) { if (feedData.keywords != null) {
if (event && event.target) { form.append('keywords', feedData.keywords);
const files = event.target.files;
if (files && files[0]) {
const reader = new FileReader();
reader.onload = () => {
if (reader.result) {
this.icon = reader.result;
}
};
reader.readAsDataURL(files[0]);
} }
if (feedData.icon != null) {
form.append('icon', feedData.icon._files['0']);
} }
this.http.post('http://127.0.0.1:8000/feeds/', form).subscribe(
() => {
this._snackbar.open('Feed erfolgreich gespeichert!', 'Schließen', {duration: 3000});
},
err => {
this._logger.error(err);
return throwError(err);
}
);
} }
} }