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
*.pyc
staticfiles
media
.env
*.sqlite3
*.sqlite

View File

@ -1,26 +1,25 @@
from django.core.validators import URLValidator, FileExtensionValidator
from django.db import models
# Create your models here.
class User(models.Model):
pass
class Icon(models.Model):
image = models.ImageField(upload_to='feed_icons')
class Feed(models.Model):
url = models.CharField(max_length=100)
url = models.TextField(blank=False, null=False, validators=[URLValidator(['http', 'https'])])
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):
feed = models.ForeignKey(Feed, on_delete=models.CASCADE)
tweeted = models.BooleanField()
class Tweet(models.Model):
icon = models.ForeignKey(Icon, on_delete=models.CASCADE)
text = models.CharField(max_length=137)
date_time = models.DateTimeField()
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'),
]
MEDIA_ROOT = os.path.join(PROJECT_ROOT, 'media')
MEDIA_URL = '/media/'
# Simplified static file serving.
# https://warehouse.python.org/project/whitenoise/
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'

View File

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

View File

@ -2,9 +2,14 @@ import logging
from django.http import JsonResponse
from rest_framework.decorators import api_view
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__)
@ -58,3 +63,8 @@ class TwitterClass:
def getLastSixTweets():
return JsonResponse({[{"asdf", "asdf", "sdfasdf", "asdf"}, {"asdf", "asdf", "sdfasdf", "asdf"}]}, safe=False,
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):
starttime = time.time()
while True:
# while True:
# 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()

View File

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

View File

@ -57,6 +57,7 @@
"karma-coverage-istanbul-reporter": "~2.1.1",
"karma-jasmine": "~2.0.1",
"karma-jasmine-html-reporter": "^1.5.4",
"ngx-material-file-input": "^2.1.1",
"protractor": "~5.4.4",
"ts-node": "~8.5.4",
"tslint": "~5.20.1",
@ -431,9 +432,6 @@
"version": "9.2.4",
"resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-9.2.4.tgz",
"integrity": "sha512-iw2+qHMXHYVC6K/fttHeNHIieSKiTEodVutZoOEcBu9rmRTGbLB26V/CRsfIRmA1RBk+uFYWc6UQZnMC3RdnJQ==",
"dependencies": {
"parse5": "^5.0.0"
},
"optionalDependencies": {
"parse5": "^5.0.0"
}
@ -969,7 +967,6 @@
"dependencies": {
"anymatch": "~3.1.1",
"braces": "~3.0.2",
"fsevents": "~2.1.2",
"glob-parent": "~5.1.0",
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
@ -2149,7 +2146,6 @@
"dependencies": {
"anymatch": "~3.1.1",
"braces": "~3.0.2",
"fsevents": "~2.1.2",
"glob-parent": "~5.1.0",
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
@ -3702,8 +3698,7 @@
"dependencies": {
"esprima": "~1.0.4",
"estraverse": "~1.5.0",
"esutils": "~1.0.0",
"source-map": "~0.1.30"
"esutils": "~1.0.0"
},
"optionalDependencies": {
"source-map": "~0.1.30"
@ -6542,8 +6537,7 @@
"esprima": "^4.0.1",
"estraverse": "^4.2.0",
"esutils": "^2.0.2",
"optionator": "^0.8.1",
"source-map": "~0.6.1"
"optionator": "^0.8.1"
},
"optionalDependencies": {
"source-map": "~0.6.1"
@ -7559,7 +7553,6 @@
"minimist": "^1.2.5",
"neo-async": "^2.6.0",
"source-map": "^0.6.1",
"uglify-js": "^3.1.4",
"wordwrap": "^1.0.0"
},
"optionalDependencies": {
@ -9279,7 +9272,6 @@
"dependencies": {
"anymatch": "~3.1.1",
"braces": "~3.0.2",
"fsevents": "~2.1.2",
"glob-parent": "~5.1.0",
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
@ -9419,12 +9411,8 @@
"clone": "^2.1.2",
"errno": "^0.1.1",
"graceful-fs": "^4.1.2",
"image-size": "~0.5.0",
"make-dir": "^2.1.0",
"mime": "^1.4.1",
"promise": "^7.1.1",
"request": "^2.83.0",
"source-map": "~0.6.0",
"tslib": "^1.10.0"
},
"optionalDependencies": {
@ -9644,7 +9632,6 @@
"anymatch": "^2.0.0",
"async-each": "^1.0.1",
"braces": "^2.3.2",
"fsevents": "^1.2.7",
"glob-parent": "^3.1.0",
"inherits": "^2.0.3",
"is-binary-path": "^1.0.0",
@ -10727,6 +10714,19 @@
"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": {
"version": "1.0.5",
"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",
"integrity": "sha512-gfE1455AEazVVTJoeQtcOq/U6GSxwoj4XPSWVsuWmgIxj7sBQNLDOSA82PbdMe+cP8ql8fR1jogPFe8Wg8g4SQ==",
"dev": true,
"dependencies": {
"fsevents": "~2.1.2"
},
"optionalDependencies": {
"fsevents": "~2.1.2"
}
@ -13775,7 +13772,6 @@
"dependencies": {
"anymatch": "~3.1.1",
"braces": "~3.0.2",
"fsevents": "~2.1.2",
"glob-parent": "~5.1.0",
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
@ -16518,10 +16514,8 @@
"integrity": "sha512-aWAgTW4MoSJzZPAicljkO1hsi1oKj/RRq/OJQh2PKI2UKL04c2Bs+MBOB+BBABHTXJpf9mCwHN7ANCvYsvY2sg==",
"dev": true,
"dependencies": {
"chokidar": "^3.4.1",
"graceful-fs": "^4.1.2",
"neo-async": "^2.5.0",
"watchpack-chokidar2": "^2.0.0"
"neo-async": "^2.5.0"
},
"optionalDependencies": {
"chokidar": "^3.4.1",
@ -17227,7 +17221,6 @@
"anymatch": "^2.0.0",
"async-each": "^1.0.1",
"braces": "^2.3.2",
"fsevents": "^1.2.7",
"glob-parent": "^3.1.0",
"inherits": "^2.0.3",
"is-binary-path": "^1.0.0",
@ -29344,6 +29337,13 @@
"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": {
"version": "1.0.5",
"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-jasmine": "~2.0.1",
"karma-jasmine-html-reporter": "^1.5.4",
"ngx-material-file-input": "^2.1.1",
"protractor": "~5.4.4",
"ts-node": "~8.5.4",
"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 {MatCheckboxModule} from '@angular/material/checkbox';
import { EditierenComponent } from './component/einstellungen/editieren/editieren.component';
import {MaterialFileInputModule} from 'ngx-material-file-input';
@NgModule({
declarations: [LandingComponent, LoginComponent, NavigationComponent,
@ -48,7 +49,8 @@ import { EditierenComponent } from './component/einstellungen/editieren/editiere
MatIconModule,
MatMenuModule,
MatSnackBarModule,
MatCheckboxModule
MatCheckboxModule,
MaterialFileInputModule
],
// enables injecting
providers: [

View File

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

View File

@ -1,5 +1,13 @@
import {Component, OnInit} from '@angular/core';
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({
selector: 'app-editieren',
@ -7,13 +15,22 @@ import {ActivatedRoute} from '@angular/router';
styleUrls: ['./editieren.component.css']
})
export class EditierenComponent implements OnInit {
icon;
id;
constructor(private route: ActivatedRoute) {
this.icon = 'assets/logo.svg';
feedForm: FormGroup;
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.id = paramMap.get('id');
if (this.id) {
@ -23,6 +40,12 @@ export class EditierenComponent implements OnInit {
}
ngOnInit(): void {
this.feedForm = this.formBuilder.group({
url: this.url,
active: this.active,
icon: [undefined, [FileValidator.maxContentSize(this.maxSize)]],
keywords: this.keywords
});
}
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"
iconChooser() {
document.getElementById('feed-icon-picker').click();
}
fileChangeEvent(event) {
if (event && event.target) {
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]);
saveFeed(feedData) {
const form: FormData = new FormData();
form.append('url', feedData.url);
form.append('active', feedData.active);
if (feedData.keywords != null) {
form.append('keywords', feedData.keywords);
}
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);
}
);
}
}