mirror of
https://github.com/Radarr/Radarr.git
synced 2024-09-17 15:02:34 +02:00
Series/Index started in backbone
This commit is contained in:
parent
ace7910f2a
commit
eb18d1c4a1
@ -1,4 +1,5 @@
|
||||
using System.Linq;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using AutoMapper;
|
||||
using Autofac;
|
||||
using NLog;
|
||||
@ -9,7 +10,9 @@
|
||||
using NzbDrone.Api.QualityProfiles;
|
||||
using NzbDrone.Api.QualityType;
|
||||
using NzbDrone.Api.Resolvers;
|
||||
using NzbDrone.Api.Series;
|
||||
using NzbDrone.Core;
|
||||
using NzbDrone.Core.Helpers;
|
||||
using NzbDrone.Core.Repository.Quality;
|
||||
|
||||
namespace NzbDrone.Api
|
||||
@ -32,25 +35,30 @@ protected override void ApplicationStartup(ILifetimeScope container, IPipelines
|
||||
public static void InitializeAutomapper()
|
||||
{
|
||||
//QualityProfiles
|
||||
Mapper.CreateMap<QualityProfileModel, QualityProfile>()
|
||||
.ForMember(dest => dest.QualityProfileId, opt => opt.MapFrom(src => src.Id))
|
||||
.ForMember(dest => dest.Allowed,
|
||||
opt => opt.ResolveUsing<QualitiesToAllowedResolver>().FromMember(src => src.Qualities));
|
||||
|
||||
Mapper.CreateMap<QualityProfile, QualityProfileModel>()
|
||||
.ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.QualityProfileId))
|
||||
.ForMember(dest => dest.Qualities,
|
||||
opt => opt.ResolveUsing<AllowedToQualitiesResolver>().FromMember(src => src.Allowed));
|
||||
|
||||
Mapper.CreateMap<QualityProfileModel, QualityProfile>()
|
||||
.ForMember(dest => dest.QualityProfileId, opt => opt.MapFrom(src => src.Id))
|
||||
.ForMember(dest => dest.Allowed,
|
||||
opt => opt.ResolveUsing<QualitiesToAllowedResolver>().FromMember(src => src.Qualities));
|
||||
|
||||
Mapper.CreateMap<QualityTypes, QualityProfileType>()
|
||||
.ForMember(dest => dest.Allowed, opt => opt.Ignore());
|
||||
|
||||
//QualityTypes
|
||||
Mapper.CreateMap<Core.Repository.Quality.QualityType, QualityTypeModel>()
|
||||
.ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.QualityTypeId));
|
||||
|
||||
Mapper.CreateMap<QualityTypeModel, Core.Repository.Quality.QualityType>()
|
||||
.ForMember(dest => dest.QualityTypeId, opt => opt.MapFrom(src => src.Id));
|
||||
|
||||
Mapper.CreateMap<Core.Repository.Quality.QualityType, QualityTypeModel>()
|
||||
.ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.QualityTypeId));
|
||||
//Series
|
||||
Mapper.CreateMap<Core.Repository.Series, SeriesModel>()
|
||||
.ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.SeriesId));
|
||||
//.ForMember(dest => dest.BacklogSetting, opt => opt.MapFrom(src => Convert.ToInt32(src.BacklogSetting)));
|
||||
}
|
||||
|
||||
protected override ILifetimeScope GetApplicationContainer()
|
||||
|
@ -96,6 +96,7 @@
|
||||
<Compile Include="Extentions\NancyJsonSerializer.cs" />
|
||||
<Compile Include="Extentions\Serializer.cs" />
|
||||
<Compile Include="RootFolders\RootFolderModule.cs" />
|
||||
<Compile Include="Series\SeriesModel.cs" />
|
||||
<Compile Include="Series\SeriesModule.cs" />
|
||||
<Compile Include="Series\SeriesLookupModule.cs" />
|
||||
<Compile Include="ErrorManagment\ApiException.cs" />
|
||||
|
46
NzbDrone.Api/Series/SeriesModel.cs
Normal file
46
NzbDrone.Api/Series/SeriesModel.cs
Normal file
@ -0,0 +1,46 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using NzbDrone.Core.Model;
|
||||
|
||||
namespace NzbDrone.Api.Series
|
||||
{
|
||||
public class SeriesModel
|
||||
{
|
||||
public Int32 Id { get; set; }
|
||||
|
||||
//Todo: Sorters should be done completely on the client
|
||||
//Todo: Is there an easy way to keep IgnoreArticlesWhenSorting in sync between, Series, History, Missing?
|
||||
|
||||
//View Only
|
||||
public String Title { get; set; }
|
||||
public Int32 SeasonsCount { get; set; }
|
||||
public Int32 EpisodeCount { get; set; }
|
||||
public Int32 EpisodeFileCount { get; set; }
|
||||
public String Status { get; set; }
|
||||
public String AirsDayOfWeek { get; set; }
|
||||
public String QualityProfileName { get; set; }
|
||||
public String Overview { get; set; }
|
||||
public Int32 Episodes { get; set; }
|
||||
public Boolean HasBanner { get; set; }
|
||||
public DateTime NextAiring { get; set; }
|
||||
public String Details { get; set; }
|
||||
public String Network { get; set; }
|
||||
public String AirTime { get; set; }
|
||||
public String Language { get; set; }
|
||||
|
||||
public Int32 SeasonCount { get; set; }
|
||||
public Int32 UtcOffset { get; set; }
|
||||
|
||||
//View & Edit
|
||||
public String Path { get; set; }
|
||||
public Int32 QualityProfileId { get; set; }
|
||||
|
||||
//Editing Only
|
||||
public Boolean SeasonFolder { get; set; }
|
||||
public Boolean Monitored { get; set; }
|
||||
public BacklogSettingType BacklogSetting { get; set; }
|
||||
public DateTime? CustomStartDate { get; set; }
|
||||
}
|
||||
}
|
@ -1,10 +1,14 @@
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AutoMapper;
|
||||
using FluentValidation;
|
||||
using Nancy;
|
||||
using NzbDrone.Api.Extentions;
|
||||
using NzbDrone.Api.QualityProfiles;
|
||||
using NzbDrone.Common;
|
||||
using NzbDrone.Core.Jobs;
|
||||
using NzbDrone.Core.Providers;
|
||||
using NzbDrone.Core.Providers.Core;
|
||||
|
||||
namespace NzbDrone.Api.Series
|
||||
{
|
||||
@ -12,15 +16,26 @@ public class SeriesModule : NzbDroneApiModule
|
||||
{
|
||||
private readonly SeriesProvider _seriesProvider;
|
||||
private readonly JobProvider _jobProvider;
|
||||
private readonly ConfigProvider _configProvider;
|
||||
|
||||
public SeriesModule(SeriesProvider seriesProvider, JobProvider jobProvider)
|
||||
public SeriesModule(SeriesProvider seriesProvider, JobProvider jobProvider,
|
||||
ConfigProvider configProvider)
|
||||
: base("/Series")
|
||||
{
|
||||
_seriesProvider = seriesProvider;
|
||||
_jobProvider = jobProvider;
|
||||
_configProvider = configProvider;
|
||||
Get["/"] = x => AllSeries();
|
||||
Post["/"] = x => AddSeries();
|
||||
}
|
||||
|
||||
private Response AllSeries()
|
||||
{
|
||||
var series = _seriesProvider.GetAllSeriesWithEpisodeCount().ToList();
|
||||
var seriesModels = Mapper.Map<List<Core.Repository.Series>, List<SeriesModel>>(series);
|
||||
|
||||
return seriesModels.AsResponse();
|
||||
}
|
||||
|
||||
private Response AddSeries()
|
||||
{
|
||||
@ -29,8 +44,7 @@ private Response AddSeries()
|
||||
//Todo: Alert the user if this series already exists
|
||||
//Todo: We need to create the folder if the user is adding a new series
|
||||
//(we can just create the folder and it won't blow up if it already exists)
|
||||
//We also need to remove any special characters from the filename before attempting to create it
|
||||
|
||||
//We also need to remove any special characters from the filename before attempting to create it
|
||||
|
||||
_seriesProvider.AddSeries("", request.Path, request.SeriesId, request.QualityProfileId, null);
|
||||
_jobProvider.QueueJob(typeof(ImportNewSeriesJob));
|
||||
@ -39,7 +53,6 @@ private Response AddSeries()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public class SeriesValidator : AbstractValidator<Core.Repository.Series>
|
||||
{
|
||||
private readonly DiskProvider _diskProvider;
|
||||
@ -58,7 +71,5 @@ public SeriesValidator()
|
||||
RuleFor(s => s.QualityProfileId).GreaterThan(0);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
@ -58,7 +58,7 @@ public virtual IList<Series> GetAllSeriesWithEpisodeCount()
|
||||
var series = _database
|
||||
.Fetch<Series, QualityProfile>(@"SELECT Series.SeriesId, Series.Title, Series.CleanTitle, Series.Status, Series.Overview, Series.AirsDayOfWeek, Series.AirTimes,
|
||||
Series.Language, Series.Path, Series.Monitored, Series.QualityProfileId, Series.SeasonFolder, Series.BacklogSetting, Series.Network,
|
||||
SUM(CASE WHEN Ignored = 0 AND Airdate <= @0 THEN 1 ELSE 0 END) AS EpisodeCount,
|
||||
Series.UtcOffset, Series.CustomStartDate, SUM(CASE WHEN Ignored = 0 AND Airdate <= @0 THEN 1 ELSE 0 END) AS EpisodeCount,
|
||||
SUM(CASE WHEN Episodes.Ignored = 0 AND Episodes.EpisodeFileId > 0 AND Episodes.AirDate <= @0 THEN 1 ELSE 0 END) as EpisodeFileCount,
|
||||
MAX(Episodes.SeasonNumber) as SeasonCount, MIN(CASE WHEN AirDate < @0 OR Ignored = 1 THEN NULL ELSE AirDate END) as NextAiring,
|
||||
QualityProfiles.QualityProfileId, QualityProfiles.Name, QualityProfiles.Cutoff, QualityProfiles.SonicAllowed
|
||||
@ -68,7 +68,8 @@ FROM Series
|
||||
WHERE Series.LastInfoSync IS NOT NULL
|
||||
GROUP BY Series.SeriesId, Series.Title, Series.CleanTitle, Series.Status, Series.Overview, Series.AirsDayOfWeek, Series.AirTimes,
|
||||
Series.Language, Series.Path, Series.Monitored, Series.QualityProfileId, Series.SeasonFolder, Series.BacklogSetting, Series.Network,
|
||||
QualityProfiles.QualityProfileId, QualityProfiles.Name, QualityProfiles.Cutoff, QualityProfiles.SonicAllowed", DateTime.Today);
|
||||
Series.UtcOffset, Series.CustomStartDate,
|
||||
QualityProfiles.QualityProfileId, QualityProfiles.Name, QualityProfiles.Cutoff, QualityProfiles.SonicAllowed",DateTime.Today);
|
||||
|
||||
return series;
|
||||
}
|
||||
@ -99,7 +100,7 @@ public virtual Series UpdateSeriesInfo(int seriesId)
|
||||
|
||||
series.SeriesId = tvDbSeries.Id;
|
||||
series.Title = tvDbSeries.SeriesName;
|
||||
series.AirTimes = CleanAirsTime(tvDbSeries.AirsTime);
|
||||
series.AirTime = CleanAirsTime(tvDbSeries.AirsTime);
|
||||
series.AirsDayOfWeek = tvDbSeries.AirsDayOfWeek;
|
||||
series.Overview = tvDbSeries.Overview;
|
||||
series.Status = tvDbSeries.Status;
|
||||
|
@ -26,7 +26,8 @@ public class Series
|
||||
[DisplayName("Air on")]
|
||||
public DayOfWeek? AirsDayOfWeek { get; set; }
|
||||
|
||||
public String AirTimes { get; set; }
|
||||
[Column("AirTimes")]
|
||||
public String AirTime { get; set; }
|
||||
|
||||
public string Language { get; set; }
|
||||
|
||||
|
@ -237,7 +237,7 @@ private List<SeriesModel> GetSeriesModels(IList<Series> seriesInDb)
|
||||
EpisodeFileCount = s.EpisodeFileCount,
|
||||
NextAiring = s.NextAiring == null ? String.Empty : s.NextAiring.Value.ToBestDateString(),
|
||||
NextAiringSorter = s.NextAiring == null ? new DateTime(9999, 12, 31).ToString("o", CultureInfo.InvariantCulture) : s.NextAiring.Value.ToString("o", CultureInfo.InvariantCulture),
|
||||
AirTime = s.AirTimes,
|
||||
AirTime = s.AirTime,
|
||||
CustomStartDate = s.CustomStartDate.HasValue ? s.CustomStartDate.Value.ToString("yyyy-MM-dd") : String.Empty
|
||||
}).ToList();
|
||||
|
||||
|
@ -52,9 +52,9 @@ private List<UpcomingEpisodeModel> GetUpcomingEpisodeModels(List<Episode> episod
|
||||
EpisodeNumbering = String.Format("{0}x{1:00}", u.SeasonNumber, u.EpisodeNumber),
|
||||
Title = u.Title,
|
||||
Overview = u.Overview,
|
||||
AirDateTime = GetDateTime(u.AirDate.Value, u.Series.AirTimes),
|
||||
AirDateTime = GetDateTime(u.AirDate.Value, u.Series.AirTime),
|
||||
AirDate = u.AirDate.Value.ToBestDateString(),
|
||||
AirTime = String.IsNullOrEmpty(u.Series.AirTimes) ? "?" : Convert.ToDateTime(u.Series.AirTimes).ToShortTimeString(),
|
||||
AirTime = String.IsNullOrEmpty(u.Series.AirTime) ? "?" : Convert.ToDateTime(u.Series.AirTime).ToShortTimeString(),
|
||||
Status = u.Status.ToString()
|
||||
}).OrderBy(e => e.AirDateTime).ToList();
|
||||
}
|
||||
|
@ -409,6 +409,12 @@
|
||||
<Content Include="_backboneApp\JsLibraries\backbone.js" />
|
||||
<Content Include="_backboneApp\JsLibraries\backbone.marionette.js" />
|
||||
<Content Include="_backboneApp\AddSeries\addSeriesLayoutTemplate.html" />
|
||||
<Content Include="_backboneApp\Series\Index\SeriesCollectionTemplate.html" />
|
||||
<Content Include="_backboneApp\Series\Index\IndexLayoutTemplate.html" />
|
||||
<Content Include="_backboneApp\Series\Index\IndexLayout.js" />
|
||||
<Content Include="_backboneApp\Series\Index\SeriesItemTemplate.html" />
|
||||
<Content Include="_backboneApp\Series\Index\SeriesItemView.js" />
|
||||
<Content Include="_backboneApp\Series\SeriesCollection.js" />
|
||||
<Content Include="_backboneApp\Series\SeriesModel.js" />
|
||||
<Content Include="_backboneApp\Shared\AutoComplete.js" />
|
||||
<Content Include="_backboneApp\Shared\NotificationModel.js" />
|
||||
|
@ -56,8 +56,6 @@ NzbDrone.AddSeries.SearchItemView = Backbone.Marionette.ItemView.extend({
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
|
||||
NzbDrone.AddSeries.SearchResultView = Backbone.Marionette.CollectionView.extend({
|
||||
|
@ -30,6 +30,7 @@ public void Configure(BundleCollection bundles)
|
||||
bundles.Add<ScriptBundle>("~/_backboneApp/JsLibraries/backbone.js");
|
||||
|
||||
bundles.Add<ScriptBundle>(NZBDRONE, new[]{
|
||||
APP_PATH + "\\Series\\Index\\IndexLayout.js",
|
||||
APP_PATH + "\\AddSeries\\AddSeriesLayout.js",
|
||||
APP_PATH + "\\Shared\\NotificationView.js",
|
||||
|
||||
|
33
NzbDrone.Web/_backboneApp/Series/Index/IndexLayout.js
Normal file
33
NzbDrone.Web/_backboneApp/Series/Index/IndexLayout.js
Normal file
@ -0,0 +1,33 @@
|
||||
'use strict;'
|
||||
/// <reference path="../../app.js" />
|
||||
/// <reference path="../SeriesCollection.js" />
|
||||
/// <reference path="SeriesItemView.js" />
|
||||
|
||||
NzbDrone.Series.IndexLayout = Backbone.Marionette.Layout.extend({
|
||||
template: 'Series/Index/IndexLayoutTemplate',
|
||||
route: 'Series/index',
|
||||
|
||||
ui: {
|
||||
edit: '.edit-series',
|
||||
delele: '.delete-series'
|
||||
},
|
||||
|
||||
regions: {
|
||||
main: '#series',
|
||||
},
|
||||
|
||||
collection: new NzbDrone.Series.SeriesCollection(),
|
||||
|
||||
initialize: function (options) {
|
||||
|
||||
},
|
||||
|
||||
onRender: function () {
|
||||
console.log('binding auto complete');
|
||||
|
||||
this.collection.fetch();
|
||||
//Show things
|
||||
|
||||
this.main.show(new NzbDrone.Series.Index.SeriesCollectionView({ collection: this.collection }));
|
||||
},
|
||||
});
|
@ -0,0 +1 @@
|
||||
<div id="series" class="result-list span18 offset1"></div>
|
@ -0,0 +1,13 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Title</th>
|
||||
<th>Seasons</th>
|
||||
<th>Quality</th>
|
||||
<th>Network</th>
|
||||
<th>Next Airing</th>
|
||||
<th>Episodes</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
@ -0,0 +1,15 @@
|
||||
<td name="status"></td>
|
||||
<td name="title"></td>
|
||||
<td name="seasonCount"></td>
|
||||
<td name="qualityProfileName"></td>
|
||||
<td name="network"></td>
|
||||
<td name="nextAiring"></td>
|
||||
<td name="EpisodeCount">
|
||||
{{#if episodeFileCount}}
|
||||
{{episodeFileCount}}
|
||||
{{else}}
|
||||
0
|
||||
{{/if}}
|
||||
/ {{episodeCount}}
|
||||
<td>
|
||||
<td>Edit/Delete</td>
|
30
NzbDrone.Web/_backboneApp/Series/Index/SeriesItemView.js
Normal file
30
NzbDrone.Web/_backboneApp/Series/Index/SeriesItemView.js
Normal file
@ -0,0 +1,30 @@
|
||||
'use strict';
|
||||
/*global NzbDrone, Backbone*/
|
||||
/// <reference path="../../app.js" />
|
||||
/// <reference path="../SeriesModel.js" />
|
||||
/// <reference path="../SeriesCollection.js" />
|
||||
|
||||
NzbDrone.Series.Index.SeriesItemView = Backbone.Marionette.ItemView.extend({
|
||||
template: 'Series/Index/SeriesItemTemplate',
|
||||
tagName: 'tr',
|
||||
|
||||
events: {
|
||||
'click .x-remove': 'removeSeries',
|
||||
},
|
||||
|
||||
onRender: function () {
|
||||
NzbDrone.ModelBinder.bind(this.model, this.el);
|
||||
},
|
||||
|
||||
removeSeries: function () {
|
||||
this.model.destroy({ wait: true });
|
||||
this.model.collection.remove(this.model);
|
||||
},
|
||||
});
|
||||
|
||||
NzbDrone.Series.Index.SeriesCollectionView = Backbone.Marionette.CompositeView.extend({
|
||||
itemView: NzbDrone.Series.Index.SeriesItemView,
|
||||
template: 'Series/Index/SeriesCollectionTemplate',
|
||||
tagName: 'table',
|
||||
className: 'table table-hover',
|
||||
});
|
7
NzbDrone.Web/_backboneApp/Series/SeriesCollection.js
Normal file
7
NzbDrone.Web/_backboneApp/Series/SeriesCollection.js
Normal file
@ -0,0 +1,7 @@
|
||||
/// <reference path="../app.js" />
|
||||
/// <reference path="SeriesModel.js" />
|
||||
|
||||
NzbDrone.Series.SeriesCollection = Backbone.Collection.extend({
|
||||
url: NzbDrone.Constants.ApiRoot + '/series',
|
||||
model: NzbDrone.Series.SeriesModel,
|
||||
});
|
@ -1,10 +1,3 @@
|
||||
NzbDrone.Series.SeriesModel = Backbone.Model.extend({
|
||||
url: NzbDrone.Constants.ApiRoot + '/series'
|
||||
|
||||
});
|
||||
|
||||
|
||||
NzbDrone.Series.SeriesCollection = Backbone.Collection.extend({
|
||||
model: NzbDrone.Series.SeriesModel,
|
||||
url: NzbDrone.Constants.ApiRoot + '/series',
|
||||
});
|
||||
url: NzbDrone.Constants.ApiRoot + '/series'
|
||||
});
|
@ -18,6 +18,7 @@ if (typeof console === undefined) {
|
||||
|
||||
NzbDrone = new Backbone.Marionette.Application();
|
||||
NzbDrone.Series = {};
|
||||
NzbDrone.Series.Index = {};
|
||||
NzbDrone.AddSeries = {};
|
||||
NzbDrone.AddSeries.New = {};
|
||||
NzbDrone.AddSeries.Existing = {};
|
||||
@ -33,7 +34,6 @@ _.templateSettings = {
|
||||
|
||||
NzbDrone.ModelBinder = new Backbone.ModelBinder();
|
||||
|
||||
|
||||
NzbDrone.Constants = {
|
||||
ApiRoot: '/api'
|
||||
};
|
||||
@ -42,38 +42,37 @@ NzbDrone.Events = {
|
||||
DisplayInMainRegion: 'DisplayInMainRegion'
|
||||
};
|
||||
|
||||
|
||||
NzbDrone.Controller = Backbone.Marionette.Controller.extend({
|
||||
|
||||
addSeries: function (action, query) {
|
||||
NzbDrone.mainRegion.show(new NzbDrone.AddSeries.AddSeriesLayout(this, action, query));
|
||||
},
|
||||
|
||||
series: function (action, query) {
|
||||
NzbDrone.mainRegion.show(new NzbDrone.Series.IndexLayout(this, action, query));
|
||||
},
|
||||
|
||||
notFound: function () {
|
||||
alert('route not found');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
NzbDrone.Router = Backbone.Marionette.AppRouter.extend({
|
||||
|
||||
controller: new NzbDrone.Controller(),
|
||||
// "someMethod" must exist at controller.someMethod
|
||||
appRoutes: {
|
||||
'series/index': 'series',
|
||||
'series/add': 'addSeries',
|
||||
'series/add/:action(/:query)': 'addSeries',
|
||||
':whatever': 'notFound'
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
NzbDrone.addInitializer(function (options) {
|
||||
|
||||
console.log('starting application');
|
||||
|
||||
|
||||
NzbDrone.addRegions({
|
||||
mainRegion: '#main-region',
|
||||
notificationRegion: '#notification-region'
|
||||
@ -81,6 +80,4 @@ NzbDrone.addInitializer(function (options) {
|
||||
|
||||
NzbDrone.Router = new NzbDrone.Router();
|
||||
Backbone.history.start();
|
||||
|
||||
|
||||
});
|
@ -1,6 +1,6 @@
|
||||
<SolutionConfiguration>
|
||||
<FileVersion>1</FileVersion>
|
||||
<AutoEnableOnStartup>True</AutoEnableOnStartup>
|
||||
<AutoEnableOnStartup>False</AutoEnableOnStartup>
|
||||
<AllowParallelTestExecution>true</AllowParallelTestExecution>
|
||||
<FrameworkUtilisationTypeForNUnit>UseDynamicAnalysis</FrameworkUtilisationTypeForNUnit>
|
||||
<FrameworkUtilisationTypeForGallio>Disabled</FrameworkUtilisationTypeForGallio>
|
||||
|
Loading…
Reference in New Issue
Block a user