Merge branch 'feature/vote' of https://github.com/tidusjar/ombi into feature/vote

pull/2556/head
TidusJar 6 years ago
commit 9cf8ea7a6f

@ -1,6 +1,8 @@
using System.Linq;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Ombi.Core.Models;
using Ombi.Core.Models.UI;
using Ombi.Store.Entities;
namespace Ombi.Core.Engine
@ -12,5 +14,6 @@ namespace Ombi.Core.Engine
IQueryable<Votes> GetVotes(int requestId, RequestType requestType);
Task RemoveCurrentVote(Votes currentVote);
Task<VoteEngineResult> UpVote(int requestId, RequestType requestType);
Task<List<VoteViewModel>> GetMovieViewModel();
}
}

@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Security.Principal;
using System.Text;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Ombi.Core.Authentication;
@ -10,6 +11,7 @@ using Ombi.Core.Models;
using Ombi.Core.Models.UI;
using Ombi.Core.Rule.Interfaces;
using Ombi.Core.Settings;
using Ombi.Schedule.Jobs.Ombi;
using Ombi.Settings.Settings.Models;
using Ombi.Store.Entities;
using Ombi.Store.Repository;
@ -38,6 +40,8 @@ namespace Ombi.Core.Engine
{
var vm = new List<VoteViewModel>();
var movieRequests = await _movieRequestEngine.GetRequests();
var tvRequestsTask = _tvRequestEngine.GetRequests();
var musicRequestsTask = _musicRequestEngine.GetRequests();
foreach (var r in movieRequests)
{
// Make model
@ -51,12 +55,62 @@ namespace Ombi.Core.Engine
RequestId = r.Id,
RequestType = RequestType.Movie,
Title = r.Title,
Image = r.PosterPath,
Background = r.Background,
Image = $"https://image.tmdb.org/t/p/w500/{r.PosterPath}",
Background = $"https://image.tmdb.org/t/p/w1280{r.Background}",
Description = r.Overview
});
}
foreach (var r in await musicRequestsTask)
{
// Make model
var votes = GetVotes(r.Id, RequestType.Album);
var upVotes = await votes.Where(x => x.VoteType == VoteType.Upvote).CountAsync();
var downVotes = await votes.Where(x => x.VoteType == VoteType.Downvote).CountAsync();
vm.Add(new VoteViewModel
{
Upvotes = upVotes,
Downvotes = downVotes,
RequestId = r.Id,
RequestType = RequestType.Album,
Title = r.Title,
Image = r.Cover,
Background = r.Cover,
Description = r.ArtistName
});
}
foreach (var r in await tvRequestsTask)
{
// Make model
var votes = GetVotes(r.Id, RequestType.TvShow);
var upVotes = await votes.Where(x => x.VoteType == VoteType.Upvote).CountAsync();
var downVotes = await votes.Where(x => x.VoteType == VoteType.Downvote).CountAsync();
var finalsb = new StringBuilder();
foreach (var childRequests in r.ChildRequests)
{
foreach (var epInformation in childRequests.SeasonRequests.OrderBy(x => x.SeasonNumber))
{
var orderedEpisodes = epInformation.Episodes.OrderBy(x => x.EpisodeNumber).ToList();
var episodeString = NewsletterJob.BuildEpisodeList(orderedEpisodes.Select(x => x.EpisodeNumber));
finalsb.Append($"Season: {epInformation.SeasonNumber} - Episodes: {episodeString}");
finalsb.Append("<br />");
}
}
vm.Add(new VoteViewModel
{
Upvotes = upVotes,
Downvotes = downVotes,
RequestId = r.Id,
RequestType = RequestType.TvShow,
Title = r.Title,
Image = r.PosterPath,
Background = r.Background,
Description = finalsb.ToString()
});
}
return vm;
}

@ -1,11 +1,6 @@
using System.Collections.Generic;
using Moq;
using NUnit.Framework;
using Ombi.Core.Settings;
using Ombi.Schedule.Jobs.Ombi;
using Ombi.Settings.Settings.Models;
using Ombi.Settings.Settings.Models.Notifications;
using Ombi.Store.Entities;
using static Ombi.Schedule.Jobs.Ombi.NewsletterJob;
namespace Ombi.Schedule.Tests
{
@ -15,17 +10,12 @@ namespace Ombi.Schedule.Tests
[TestCaseSource(nameof(EpisodeListData))]
public string BuildEpisodeListTest(List<int> episodes)
{
var emailSettings = new Mock<ISettingsService<EmailNotificationSettings>>();
var customziation = new Mock<ISettingsService<CustomizationSettings>>();
var newsletterSettings = new Mock<ISettingsService<NewsletterSettings>>();
var newsletter = new NewsletterJob(null, null, null, null, null, null, customziation.Object, emailSettings.Object, null, null, newsletterSettings.Object, null, null, null, null);
var ep = new List<int>();
foreach (var i in episodes)
{
ep.Add(i);
}
var result = newsletter.BuildEpisodeList(ep);
var result = BuildEpisodeList(ep);
return result;
}

@ -654,7 +654,7 @@ namespace Ombi.Schedule.Jobs.Ombi
AddInfoTable(sb);
var title = "";
if (!String.IsNullOrEmpty(info.premiered) && info.premiered.Length > 4)
if (!string.IsNullOrEmpty(info.premiered) && info.premiered.Length > 4)
{
title = $"{t.Title} ({info.premiered.Remove(4)})";
} else
@ -715,7 +715,7 @@ namespace Ombi.Schedule.Jobs.Ombi
}
}
public string BuildEpisodeList(IEnumerable<int> orderedEpisodes)
public static string BuildEpisodeList(IEnumerable<int> orderedEpisodes)
{
var epSb = new StringBuilder();
var previousEpisodes = new List<int>();

File diff suppressed because it is too large Load Diff

@ -0,0 +1,46 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
namespace Ombi.Store.Migrations
{
public partial class Votes : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Votes",
columns: table => new
{
Id = table.Column<int>(nullable: false)
.Annotation("Sqlite:Autoincrement", true),
RequestId = table.Column<int>(nullable: false),
VoteType = table.Column<int>(nullable: false),
RequestType = table.Column<int>(nullable: false),
UserId = table.Column<string>(nullable: true),
Date = table.Column<DateTime>(nullable: false),
Deleted = table.Column<bool>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Votes", x => x.Id);
table.ForeignKey(
name: "FK_Votes_AspNetUsers_UserId",
column: x => x.UserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateIndex(
name: "IX_Votes_UserId",
table: "Votes",
column: "UserId");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Votes");
}
}
}

@ -916,6 +916,30 @@ namespace Ombi.Store.Migrations
b.ToTable("UserQualityProfiles");
});
modelBuilder.Entity("Ombi.Store.Entities.Votes", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<DateTime>("Date");
b.Property<bool>("Deleted");
b.Property<int>("RequestId");
b.Property<int>("RequestType");
b.Property<string>("UserId");
b.Property<int>("VoteType");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("Votes");
});
modelBuilder.Entity("Ombi.Store.Repository.Requests.EpisodeRequests", b =>
{
b.Property<int>("Id")
@ -1128,6 +1152,13 @@ namespace Ombi.Store.Migrations
.HasForeignKey("UserId");
});
modelBuilder.Entity("Ombi.Store.Entities.Votes", b =>
{
b.HasOne("Ombi.Store.Entities.OmbiUser", "User")
.WithMany()
.HasForeignKey("UserId");
});
modelBuilder.Entity("Ombi.Store.Repository.Requests.EpisodeRequests", b =>
{
b.HasOne("Ombi.Store.Repository.Requests.SeasonRequests", "Season")

@ -15,7 +15,7 @@ import { TranslateLoader, TranslateModule } from "@ngx-translate/core";
import { TranslateHttpLoader } from "@ngx-translate/http-loader";
import { CookieService } from "ng2-cookies";
import { GrowlModule } from "primeng/components/growl/growl";
import { ButtonModule, CaptchaModule, ConfirmationService, ConfirmDialogModule, DataTableModule, DialogModule, SharedModule, SidebarModule, TooltipModule } from "primeng/primeng";
import { ButtonModule, CaptchaModule, ConfirmationService, ConfirmDialogModule, DataTableModule, DialogModule, OverlayPanelModule, SharedModule, SidebarModule, TooltipModule } from "primeng/primeng";
// Components
import { AppComponent } from "./app.component";
@ -55,6 +55,7 @@ const routes: Routes = [
{ loadChildren: "./requests/requests.module#RequestsModule", path: "requests" },
{ loadChildren: "./search/search.module#SearchModule", path: "search" },
{ loadChildren: "./recentlyAdded/recentlyAdded.module#RecentlyAddedModule", path: "recentlyadded" },
{ loadChildren: "./vote/vote.module#VoteModule", path: "vote" },
];
// AoT requires an exported function for factories
@ -97,6 +98,7 @@ export function JwtTokenGetter() {
CaptchaModule,
TooltipModule,
ConfirmDialogModule,
OverlayPanelModule,
CommonModule,
JwtModule.forRoot({
config: {

@ -3,6 +3,7 @@
export enum RequestType {
movie = 1,
tvShow = 2,
}
// NEW WORLD

@ -0,0 +1,23 @@
export interface IVoteViewModel {
requestId: number;
requestType: RequestTypes;
image: string;
background: string;
upvotes: number;
downvotes: number;
title: string;
description: string;
}
export interface IVoteEngineResult {
result: boolean;
message: string;
isError: boolean;
errorMessage: string;
}
export enum RequestTypes {
TvShow = 0,
Movie = 1,
Album = 2,
}

@ -16,3 +16,4 @@ export * from "./IIssues";
export * from "./IRecentlyAdded";
export * from "./ILidarr";
export * from "./ISearchMusicResult";
export * from "./IVote";

@ -14,3 +14,4 @@ export * from "./issues.service";
export * from "./mobile.service";
export * from "./notificationMessage.service";
export * from "./recentlyAdded.service";
export * from "./vote.service";

@ -0,0 +1,36 @@
import { PlatformLocation } from "@angular/common";
import { HttpClient } from "@angular/common/http";
import { Injectable } from "@angular/core";
import { IVoteEngineResult, IVoteViewModel } from "../interfaces";
import { ServiceHelpers } from "./service.helpers";
@Injectable()
export class VoteService extends ServiceHelpers {
constructor(public http: HttpClient, public platformLocation: PlatformLocation) {
super(http, "/api/v1/Vote/", platformLocation);
}
public async getModel(): Promise<IVoteViewModel[]> {
return await this.http.get<IVoteViewModel[]>(`${this.url}`, {headers: this.headers}).toPromise();
}
public async upvoteMovie(requestId: number): Promise<IVoteEngineResult> {
return await this.http.post<IVoteEngineResult>(`${this.url}up/movie/${requestId}`, {headers: this.headers}).toPromise();
}
public async upvoteTv(requestId: number): Promise<IVoteEngineResult> {
return await this.http.post<IVoteEngineResult>(`${this.url}up/tv/${requestId}`, {headers: this.headers}).toPromise();
}
public async upvoteAlbum(requestId: number): Promise<IVoteEngineResult> {
return await this.http.post<IVoteEngineResult>(`${this.url}up/album/${requestId}`, {headers: this.headers}).toPromise();
}
public async downvoteMovie(requestId: number): Promise<IVoteEngineResult> {
return await this.http.post<IVoteEngineResult>(`${this.url}down/movie/${requestId}`, {headers: this.headers}).toPromise();
}
public async downvoteTv(requestId: number): Promise<IVoteEngineResult> {
return await this.http.post<IVoteEngineResult>(`${this.url}down/tv/${requestId}`, {headers: this.headers}).toPromise();
}
public async downvoteAlbum(requestId: number): Promise<IVoteEngineResult> {
return await this.http.post<IVoteEngineResult>(`${this.url}down/album/${requestId}`, {headers: this.headers}).toPromise();
}
}

@ -0,0 +1,35 @@
<h1>Vote</h1>
<div *ngIf="viewModel">
<table class="table table-striped table-hover table-responsive table-condensed">
<thead>
<tr>
<td></td>
<td></td>
<td>Title</td>
<td>Description</td>
</tr>
</thead>
<tbody>
<tr *ngFor="let vm of viewModel">
<td class="vcenter">
<button class="btn btn-info-outline" (click)="upvote(vm)"><i class="fa fa-arrow-up" aria-hidden="true"></i></button>
<button class="btn btn-info-outline" (click)="downvote(vm)"><i class="fa fa-arrow-down" aria-hidden="true"></i></button>
</td>
<td style="width: 10%"> <img *ngIf="vm.image" class="img-responsive poster" style="max-width: 100%;
height: auto;
width: 100%;"
(click)="toggle($event, vm.image)" src="{{vm.image}}" alt="poster"></td>
<td class="vcenter">{{vm.title}}</td>
<td class="vcenter" [innerHTML]="vm.description"></td>
</tr>
</tbody>
</table>
</div>
<p-overlayPanel #op [dismissable]="true" [styleClass]="'hideBackground'">
<img class="img-responsive poster" width="70%" src="{{panelImage}}" alt="poster">
</p-overlayPanel>

@ -0,0 +1,12 @@
.vcenter {
vertical-align: middle;
float: none;
}
.hideBackground {
border: 0px solid transparent !important;
background: transparent !important;
-webkit-box-shadow: 0 0px 0px 0 transparent !important;
-moz-box-shadow: 0 0px 0px 0 transparent !important;
box-shadow: 0 0px 0px 0 transparent !important;
}

@ -0,0 +1,70 @@
import { Component, OnInit, ViewChild } from "@angular/core";
import { OverlayPanel } from "primeng/primeng";
import { NotificationService, VoteService } from "../services";
import { IVoteEngineResult, IVoteViewModel, RequestTypes } from "../interfaces";
@Component({
templateUrl: "vote.component.html",
styleUrls: ["vote.component.scss"],
})
export class VoteComponent implements OnInit {
public viewModel: IVoteViewModel[];
public panelImage: string;
@ViewChild("op") public overlayPanel: OverlayPanel;
constructor(private voteService: VoteService, private notificationSerivce: NotificationService) { }
public async ngOnInit() {
this.viewModel = await this.voteService.getModel();
}
public toggle(event: any, image: string) {
this.panelImage = image;
this.overlayPanel.toggle(event);
}
public async upvote(vm: IVoteViewModel) {
let result: IVoteEngineResult = {errorMessage:"", isError: false, message:"",result:false};
switch(vm.requestType) {
case RequestTypes.Album:
result = await this.voteService.upvoteAlbum(vm.requestId);
break;
case RequestTypes.Movie:
result = await this.voteService.upvoteMovie(vm.requestId);
break;
case RequestTypes.TvShow:
result = await this.voteService.upvoteTv(vm.requestId);
break;
}
if(result.isError) {
this.notificationSerivce.error(result.errorMessage);
} else {
this.notificationSerivce.success("Voted!");
}
}
public async downvote(vm: IVoteViewModel) {
let result: IVoteEngineResult = {errorMessage:"", isError: false, message:"",result:false};
switch(vm.requestType) {
case RequestTypes.Album:
result = await this.voteService.downvoteAlbum(vm.requestId);
break;
case RequestTypes.Movie:
result = await this.voteService.downvoteMovie(vm.requestId);
break;
case RequestTypes.TvShow:
result = await this.voteService.downvoteTv(vm.requestId);
break;
}
if(result.isError) {
this.notificationSerivce.error(result.errorMessage);
} else {
this.notificationSerivce.success("Voted!");
}
}
}

@ -0,0 +1,41 @@
import { NgModule } from "@angular/core";
import { RouterModule, Routes } from "@angular/router";
import { NgbModule } from "@ng-bootstrap/ng-bootstrap";
import { OrderModule } from "ngx-order-pipe";
import { OverlayPanelModule, SharedModule, TabViewModule } from "primeng/primeng";
import { VoteService } from "../services";
import { AuthGuard } from "../auth/auth.guard";
import { SharedModule as OmbiShared } from "../shared/shared.module";
import { VoteComponent } from "./vote.component";
const routes: Routes = [
{ path: "", component: VoteComponent, canActivate: [AuthGuard] },
];
@NgModule({
imports: [
RouterModule.forChild(routes),
NgbModule.forRoot(),
SharedModule,
OrderModule,
OmbiShared,
TabViewModule,
OverlayPanelModule,
],
declarations: [
VoteComponent,
],
exports: [
RouterModule,
],
providers: [
VoteService,
],
})
export class VoteModule { }

@ -1007,5 +1007,4 @@ a > h4:hover {
.album-cover {
width:300px;
}
}

@ -4,6 +4,8 @@ using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;
using Ombi.Core.Engine;
using Ombi.Core.Models;
using Ombi.Core.Models.UI;
using Ombi.Store.Entities;
namespace Ombi.Controllers
@ -20,6 +22,69 @@ namespace Ombi.Controllers
private readonly IVoteEngine _engine;
/// <summary>
/// Returns the viewmodel to render on the UI
/// </summary>
[HttpGet]
public Task<List<VoteViewModel>> GetView()
{
return _engine.GetMovieViewModel();
}
/// <summary>
/// Upvotes a movie
/// </summary>
[HttpPost("up/movie/{requestId:int}")]
public Task<VoteEngineResult> UpvoteMovie(int requestId)
{
return _engine.UpVote(requestId, RequestType.Movie);
}
/// <summary>
/// Upvotes a tv show
/// </summary>
[HttpPost("up/tv/{requestId:int}")]
public Task<VoteEngineResult> UpvoteTv(int requestId)
{
return _engine.UpVote(requestId, RequestType.TvShow);
}
/// <summary>
/// Upvotes a album
/// </summary>
[HttpPost("up/album/{requestId:int}")]
public Task<VoteEngineResult> UpvoteAlbum(int requestId)
{
return _engine.UpVote(requestId, RequestType.Album);
}
/// <summary>
/// Downvotes a movie
/// </summary>
[HttpPost("down/movie/{requestId:int}")]
public Task<VoteEngineResult> DownvoteMovie(int requestId)
{
return _engine.DownVote(requestId, RequestType.Movie);
}
/// <summary>
/// Downvotes a tv show
/// </summary>
[HttpPost("down/tv/{requestId:int}")]
public Task<VoteEngineResult> DownvoteTv(int requestId)
{
return _engine.DownVote(requestId, RequestType.TvShow);
}
/// <summary>
/// Downvotes a album
/// </summary>
[HttpPost("down/album/{requestId:int}")]
public Task<VoteEngineResult> DownvoteAlbum(int requestId)
{
return _engine.DownVote(requestId, RequestType.Album);
}
/// <summary>
/// Get's all the votes for the request id
/// </summary>

Loading…
Cancel
Save