diff --git a/backend/.gitignore b/backend/.gitignore index d879270..cbeb2f6 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -1,6 +1,7 @@ venv *.pyc staticfiles +media .env *.sqlite3 *.sqlite diff --git a/backend/app_be/models.py b/backend/app_be/models.py index d13ca31..2576ad2 100644 --- a/backend/app_be/models.py +++ b/backend/app_be/models.py @@ -1,7 +1,7 @@ +from django.core.validators import URLValidator, FileExtensionValidator from django.db import models -# Create your models here. class User(models.Model): pass @@ -11,11 +11,15 @@ class Icon(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() + 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() @@ -23,4 +27,4 @@ class Tweet(models.Model): icon = models.ForeignKey(Icon, on_delete=models.CASCADE,null=True) text = models.CharField(max_length=137) date_time = models.DateTimeField() - url = models.CharField(max_length=100) \ No newline at end of file + url = models.CharField(max_length=100) diff --git a/backend/app_be/serializers.py b/backend/app_be/serializers.py index de8ae38..2b06766 100644 --- a/backend/app_be/serializers.py +++ b/backend/app_be/serializers.py @@ -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 diff --git a/backend/app_be/settings.py b/backend/app_be/settings.py index ef4a739..968b258 100644 --- a/backend/app_be/settings.py +++ b/backend/app_be/settings.py @@ -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' diff --git a/backend/app_be/urls.py b/backend/app_be/urls.py index 0294874..5d662bb 100644 --- a/backend/app_be/urls.py +++ b/backend/app_be/urls.py @@ -23,9 +23,10 @@ urlpatterns = [ path('admin/', admin.site.urls), url(r'^api/login', LoginClass.login), url(r'^getSixTweets', TwitterClass.getLastSixTweets), - url(r'^getTwelveTweets',TwitterClass.getLastSixTweets) + url(r'^getTwelveTweets', TwitterClass.getLastSixTweets) ] router = DefaultRouter() +router.register(r'feeds', FeedViewSet, basename='feeds') urlpatterns.extend(router.urls) diff --git a/backend/app_be/views/rest_api.py b/backend/app_be/views/rest_api.py index 0936875..656e104 100644 --- a/backend/app_be/views/rest_api.py +++ b/backend/app_be/views/rest_api.py @@ -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__) @@ -51,8 +56,15 @@ class LoginClass: return JsonResponse({'user': user_sub}, safe=False, status=200) + class TwitterClass: @staticmethod @api_view(['GET']) def getLastSixTweets(): - return JsonResponse({[{"asdf","asdf","sdfasdf","asdf"},{"asdf","asdf","sdfasdf","asdf"}]}, safe=False, status=200) \ No newline at end of file + return JsonResponse({[{"asdf", "asdf", "sdfasdf", "asdf"}, {"asdf", "asdf", "sdfasdf", "asdf"}]}, safe=False, + status=200) + + +class FeedViewSet(ModelViewSet): + queryset = Feed.objects.all() + serializer_class = FeedSerializer diff --git a/docker-compose.yml b/docker-compose.yml index b916195..7231e35 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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 diff --git a/frontend/package-lock.json b/frontend/package-lock.json index d8b14f7..c99d544 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -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", diff --git a/frontend/package.json b/frontend/package.json index 41e3ad4..df74520 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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", diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 71e6116..33d2221 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -27,29 +27,31 @@ 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, TweetsComponent, EinstellungenComponent, EditierenComponent], - imports: [ - ReactiveFormsModule, - BrowserModule, - BrowserAnimationsModule, - AppRoutingModule, - LoggerModule.forRoot({level: environment.log_level, serverLogLevel: NgxLoggerLevel.ERROR}), - HttpClientModule, - MatFormFieldModule, - FormsModule, - MatButtonModule, - MatInputModule, - MatSlideToggleModule, - MatSliderModule, - MatToolbarModule, - MatIconModule, - MatMenuModule, - MatSnackBarModule, - MatCheckboxModule - ], + imports: [ + ReactiveFormsModule, + BrowserModule, + BrowserAnimationsModule, + AppRoutingModule, + LoggerModule.forRoot({level: environment.log_level, serverLogLevel: NgxLoggerLevel.ERROR}), + HttpClientModule, + MatFormFieldModule, + FormsModule, + MatButtonModule, + MatInputModule, + MatSlideToggleModule, + MatSliderModule, + MatToolbarModule, + MatIconModule, + MatMenuModule, + MatSnackBarModule, + MatCheckboxModule, + MaterialFileInputModule + ], // enables injecting providers: [ AuthService, diff --git a/frontend/src/app/component/einstellungen/editieren/editieren.component.html b/frontend/src/app/component/einstellungen/editieren/editieren.component.html index 3f76b46..66b7f25 100644 --- a/frontend/src/app/component/einstellungen/editieren/editieren.component.html +++ b/frontend/src/app/component/einstellungen/editieren/editieren.component.html @@ -2,34 +2,42 @@

RSS-Feed erstellen

-
- - Vollständige URL des RSS-Feeds: - - -
-
- - Folgende Stichwörter im Feed suchen: - - - Alle Stichworte müssen enthalten sein -
-
- Optionales Icon: -
- Feed-Icon - -
-
- Slide me! -
-
- - -
+
+
+ + Vollständige URL des RSS-Feeds + + +
+
+ + Gesuchte Stichwörter + + + Alle Stichworte müssen enthalten sein +
+
+ + + folder + + Die maximale Dateigröße ist {{feedForm.get('icon')?.getError('maxContentSize').maxSize | byteFormat}} + ({{feedForm.get('icon')?.getError('maxContentSize').actualSize + | byteFormat}}). + + +
+
+ Slide me! +
+
+ + +
+
diff --git a/frontend/src/app/component/einstellungen/editieren/editieren.component.ts b/frontend/src/app/component/einstellungen/editieren/editieren.component.ts index d6b213e..9ebc554 100644 --- a/frontend/src/app/component/einstellungen/editieren/editieren.component.ts +++ b/frontend/src/app/component/einstellungen/editieren/editieren.component.ts @@ -1,5 +1,13 @@ -import { Component, OnInit } from '@angular/core'; +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,14 +15,23 @@ 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'; - this.route.paramMap.subscribe( paramMap => { + 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) { this.loadFeed(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); + } + ); } } diff --git a/frontend/src/app/component/tweets/tweets.component.ts b/frontend/src/app/component/tweets/tweets.component.ts index 1be4041..e5d58e4 100644 --- a/frontend/src/app/component/tweets/tweets.component.ts +++ b/frontend/src/app/component/tweets/tweets.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit } from '@angular/core'; +import {Component, OnInit} from '@angular/core'; import {AuthService} from '../../services/auth.service'; import {HttpClient, HttpHeaders} from '@angular/common/http'; @@ -13,29 +13,32 @@ class Tweet { }) export class TweetsComponent implements OnInit { - tweets:Tweet[] = []; + tweets: Tweet[] = []; constructor(private http: HttpClient, - private authService: AuthService) { } + private authService: AuthService) { + } ngOnInit(): void { } loadMore() { - const headerDict = { - 'Authorization': 'Bearer ' + this.authService.getToken(), - }; - - - - return this.http.get('http://localhost:8000/api/login', - { - headers: new HttpHeaders(headerDict), - observe: 'response', - }) - .subscribe(data => { console.log(data); alert('Returned with code: ' + data['status']); }); - } + const headerDict = { + 'Authorization': 'Bearer ' + this.authService.getToken(), + }; + return this.http.get('http://localhost:8000/api/login', + { + headers: new HttpHeaders(headerDict), + observe: 'response', + }) + .subscribe(data => { + console.log(data); + alert('Returned with code: ' + data['status']); + }); } + + +} diff --git a/frontend/src/app/interfaces/interface.ts b/frontend/src/app/interfaces/interface.ts index 60cbb9d..9bd7478 100644 --- a/frontend/src/app/interfaces/interface.ts +++ b/frontend/src/app/interfaces/interface.ts @@ -9,9 +9,9 @@ export interface WSEvents { export interface Tweet { - icon: any - text: string - date_time: DateTimeFormat - url: String + icon: any; + text: string; + date_time: DateTimeFormat; + url: String; }