public async Task <IHttpActionResult> PostFinishReview(int reviewId) { var userId = this.GetUserId(); // Get ExerciseId and FinishTime back. var output = (await DapperHelper.QueryResilientlyAsync <dynamic>( "dbo.exeFinishReview", new { ReviewId = reviewId, UserId = userId, }, CommandType.StoredProcedure)) .Single(); var partitionKey = ReviewPiece.GetPartitionKey(output.ExerciseId); // Revoke write access from the reviewer. var reviewerEntity = new ReviewPiece() { PartitionKey = partitionKey, RowKey = ReviewPiece.GetRowKey(reviewId, ReviewPiece.PieceTypes.Editor, userId), ETag = "*", }; var operation = TableOperation.Delete(reviewerEntity); var table = AzureStorageUtils.GetCloudTable(AzureStorageUtils.TableNames.ReviewPieces); await table.ExecuteAsync(operation); var finishTime = DateTime.SpecifyKind(output.FinishTime, DateTimeKind.Utc); // Notify the watching exercise author // this.GetAuthorConnections(partitionKey).ReviewFinished(reviewId, finishTime); return(Ok(new { FinishTime = finishTime })); }
public async Task <IHttpActionResult> PostStartReview(int reviewId) { var userId = this.GetUserId(); // Get ExerciseId, AuthorUserId, StartTime back. var output = (await DapperHelper.QueryResilientlyAsync <dynamic>("dbo.exeStartReview", new { UserId = userId, ReviewId = reviewId, }, CommandType.StoredProcedure)) .Single(); /* Write entities which will allow the reviewer to access for reading and writing and the author for reading. * We will simply check the presence of one of these records as we read or write the entities. * The write entity will be deleted on review finish. */ var partitionKey = ReviewPiece.GetPartitionKey(output.ExerciseId); var viewerEntity = new ReviewPiece() { PartitionKey = partitionKey, RowKey = ReviewPiece.GetRowKey(reviewId, ReviewPiece.PieceTypes.Viewer, userId), }; var editorEntity = new ReviewPiece() { PartitionKey = partitionKey, RowKey = ReviewPiece.GetRowKey(reviewId, ReviewPiece.PieceTypes.Editor, userId), }; var authorEntity = new ReviewPiece() { PartitionKey = partitionKey, RowKey = ReviewPiece.GetRowKey(reviewId, ReviewPiece.PieceTypes.Viewer, output.AuthorUserId), }; var batchOperation = new TableBatchOperation(); batchOperation.InsertOrReplace(viewerEntity); batchOperation.InsertOrReplace(editorEntity); if (userId != output.AuthorUserId) { batchOperation.InsertOrReplace(authorEntity); } var table = AzureStorageUtils.GetCloudTable(AzureStorageUtils.TableNames.ReviewPieces); await table.ExecuteBatchAsync(batchOperation); var startTime = DateTime.SpecifyKind(output.StartTime, DateTimeKind.Utc); // SignalR this.GetAuthorConnections(partitionKey).ReviewStarted(reviewId, startTime, output.ReviewerUserId, output.ReviewerName); return(StatusCode(HttpStatusCode.NoContent)); }
public async Task <IHttpActionResult> GetMessageText(string extId = null) { string text = null; if (!String.IsNullOrWhiteSpace(extId)) { var table = AzureStorageUtils.GetCloudTable(AzureStorageUtils.TableNames.UserMessages); var operation = TableOperation.Retrieve <UserMessageEntity>(extId, String.Empty); var result = await table.ExecuteAsync(operation); var entity = result.Result as UserMessageEntity; text = entity != null ? entity.Text : null; } return(new RawStringResult(this, text, RawStringResult.TextMediaType.PlainText)); }
public async Task <IHttpActionResult> DeletePiece(string partitionKey, string rowKey) { // Check access rights. var reviewId = ReviewPiece.GetReviewId(rowKey); var table = AzureStorageUtils.GetCloudTable(AzureStorageUtils.TableNames.ReviewPieces); var userIsEditor = await UserIsEditor(partitionKey, reviewId, table); if (userIsEditor) { var entity = new ReviewPiece() { PartitionKey = partitionKey, RowKey = rowKey, ETag = "*", }; var deleteOperation = TableOperation.Delete(entity); // If the piece is not yet saved, we will get "The remote server returned an error: (404) Not Found." // The approved solution from MS is to try to retrieve the entity first. Catching exception is a hack which is appropriate for a single entity, it is not compatible with a Batch operation. try { await table.ExecuteAsync(deleteOperation); } catch (StorageException ex) { if (ex.RequestInformation.HttpStatusCode != (int)HttpStatusCode.NotFound) { throw; } } // Notify the exercise author in real-time. var pieceType = ReviewPiece.GetType(rowKey); var pieceId = ReviewPiece.GetPieceId(rowKey); this.GetAuthorConnections(partitionKey).PieceDeleted(reviewId, pieceType, pieceId); } return(StatusCode(userIsEditor ? HttpStatusCode.NoContent : HttpStatusCode.BadRequest)); }
// GET: /history public async Task <ActionResult> History() { // Send all the days there are records for. We will enable/disable days in the calendar on the page accordingly. RowKeys in the table are "inverted" local time. var days = new List <string>(); if (this.IsAuthenticated()) { var userId = this.GetUserId(); var partitionKey = KeyUtils.IntToKey(userId); var filterPartition = TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, partitionKey); var query = new TableQuery <TableEntity>().Where(filterPartition); var table = AzureStorageUtils.GetCloudTable(AzureStorageUtils.TableNames.LibraryHistory); TableQuerySegment <TableEntity> currentSegment = null; while (currentSegment == null || currentSegment.ContinuationToken != null) { currentSegment = await table.ExecuteQuerySegmentedAsync <TableEntity>( query, currentSegment != null?currentSegment.ContinuationToken : null ); // Format 2014-01-21 as "140121" var localDays = currentSegment.Results .GroupBy(i => i.RowKey.Substring(0, 6)) .Select(i => KeyUtils.InvertedKeyToLocalTime(i.Key, 3, "", "d2").Substring(2)) ; days.AddRange(localDays); } } //var daysParam = days.Distinct(); //ViewBag.DaysParamJson = JsonUtils.SerializeAsJson(daysParam); ViewBag.DaysParam = days.Distinct(); return(View()); }
public async Task <IHttpActionResult> PutReviewPieces([FromBody] IEnumerable <ReviewPiece> pieces) { // All pieces must belong to the same exercise and review. var partitionKey = pieces .Select(i => i.PartitionKey) .GroupBy(i => i) .Select(i => i.Key) .Single(); var reviewId = pieces .Select(i => ReviewPiece.GetReviewId(i.RowKey)) .GroupBy(i => i) .Select(i => i.Key) .Single(); // Ensure that the user is the actual reviewer. Check the presense of the access entry for the user. All pieces must belong to the same exercise and review. var table = AzureStorageUtils.GetCloudTable(AzureStorageUtils.TableNames.ReviewPieces); var userIsEditor = await UserIsEditor(partitionKey, reviewId, table); if (userIsEditor) { var batchOperation = new TableBatchOperation(); foreach (var piece in pieces) { batchOperation.InsertOrReplace(piece); } await table.ExecuteBatchAsync(batchOperation); } // Notify the exercise author in real-time. var piecesArr = pieces.Select(i => i.Json).ToArray(); this.GetAuthorConnections(partitionKey).PiecesChanged(piecesArr); return(StatusCode(userIsEditor ? HttpStatusCode.NoContent : HttpStatusCode.BadRequest)); }
public async Task <IHttpActionResult> GetCollection(string q) { if (String.IsNullOrWhiteSpace(q)) { return(BadRequest("Empty query.")); } var collectionName = GeneralUtils.SanitizeSpaceSeparatedWords(q); // Try to find the picture collection for the query in the internal Table storage. var table = AzureStorageUtils.GetCloudTable(AzureStorageUtils.TableNames.GamePicapick); var operation = TableOperation.Retrieve <GamePickapicEntity>(collectionName, String.Empty); var entity = (await table.ExecuteAsync(operation)).Result as GamePickapicEntity; string json = entity != null ? entity.Json : null; // If the data not found in our internal Table, query the YouTube API. if (json == null) { // We do not use the Google API Client library to materialize result as a POCO. Anyway the API itself is RESTful, and JSON can be parsed easily. Avoid overhead, overkill, bloatware etc. // +https://developers.google.com/apis-explorer/#p/youtube/v3/youtube.search.list var youtubeParams = HttpUtility.ParseQueryString(String.Empty); // returns System.Web.HttpValueCollection: System.Collections.Specialized.NameValueCollection youtubeParams["key"] = ConfigurationManager.AppSettings["YoutubeApiKey"]; youtubeParams["part"] = "snippet"; youtubeParams["type"] = "video"; youtubeParams["maxResults"] = "50"; youtubeParams["q"] = collectionName; youtubeParams["fields"] = "items(id,snippet(thumbnails(high)))"; var youtubeQueryString = youtubeParams.ToString(); var url = "https://www.googleapis.com/youtube/v3/search?" + youtubeQueryString; var handler = new HttpClientHandler(); if (handler.SupportsAutomaticDecompression) { handler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate; } string response = null; using (var client = new HttpClient(handler)) { // If User-Agent is not sent, the server ignores "Accept-Encoding: gzip, deflate" and does not compress the response. The observed compression is 10kB -> 1kB. client.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", "google-api-dotnet-client/1.8.1.31685 (gzip)"); response = await client.GetStringAsync(url); } var urls = JObject.Parse(response) .GetValue("items") .Children() .Select(i => i["snippet"]["thumbnails"]["high"]["url"].Value <string>()) .OrderBy(i => Guid.NewGuid()) // Shuffle .ToArray(); var controlNumber = Math.Abs(String.Join(String.Empty, urls).GetHashCode()) % 100; json = JsonUtils.SerializeAsJson(new { CollectionName = collectionName, ControlNumber = controlNumber, Urls = urls, }); entity = new GamePickapicEntity(collectionName, json); await AzureStorageUtils.InsertEntityAsync(AzureStorageUtils.TableNames.GamePicapick, entity); } return(new RawStringResult(this, json, RawStringResult.TextMediaType.Json)); }
public async Task <ActionResult> Upload(HttpPostedFileBase mp3File, HttpPostedFileBase xmlFile, string userdir = "", string title = null, string learnerSkype = null) { int exerciseId = 0; int newUserId = 0; var userId = this.GetUserId(); // Save the recording if (mp3File != null) { ViewBag.mp3FileName = mp3File.FileName; // We can receive garbage from the wild web. try { using (var stream = mp3File.InputStream) { var duration = RecordingUtils.GetMp3Duration(stream); if (duration > 0) { var recordingTitle = !String.IsNullOrEmpty(title) ? title : Path.GetFileNameWithoutExtension(mp3File.FileName); exerciseId = await ExerciseUtils.SaveRecording( stream, userId, ArtifactType.Mp3, ServiceType.IeltsSpeaking, duration, recordingTitle ); } } } catch { } } // If the recording is being uploaded by a teacher, make it owned by the learner, create a review, and save remark spots if an accompanying XML file is provided. if (exerciseId != 0 && this.GetUserIsTeacher()) { // We may succeed or fail with updating the user of the exercise depending on whether the provided Skype name is found and is unambiguous. // Continue anyway with either old or new user. if (!String.IsNullOrEmpty(learnerSkype)) { newUserId = (await DapperHelper.QueryResilientlyAsync <int?>("dbo.exeTryChangeExerciseAuthor", new { ExerciseId = exerciseId, UserId = userId, SkypeName = learnerSkype, }, CommandType.StoredProcedure)) .SingleOrDefault() .GetValueOrDefault(); } IEnumerable <int> remarkSpots = new int[] { }; // If there is a remark spot collection XML file produced by the teacher coming with the recording. if (xmlFile != null) { // We can receive garbage from the wild web. try { XElement root; using (var stream = xmlFile.InputStream) { root = XElement.Load(stream); } var startedText = root.Attribute("time").Value; var started = DateTime.Parse(startedText, null, DateTimeStyles.RoundtripKind); remarkSpots = root .Elements("RemarkSpot") .Select(i => { var spot = DateTime.Parse(i.Attribute("time").Value, null, DateTimeStyles.RoundtripKind); return(Convert.ToInt32((spot - started).TotalMilliseconds)); }) .ToList(); } catch { } } // We have got remark spots. Create a review. if (remarkSpots.Any()) { var reviewId = (await DapperHelper.QueryResilientlyAsync <int>("dbo.exeCreateUploadedReview", new { ExerciseId = exerciseId, UserId = userId, }, CommandType.StoredProcedure )) .SingleOrDefault(); // Save remarks. if (reviewId != 0) { var pieces = remarkSpots .OrderBy(i => i) .Distinct() .Select((i, index) => { var finish = Math.Max(0, i - 1000); // We allow for a 1 second delay between the actual learner's mistake and the teacher's action. var start = Math.Max(0, finish - 2000); // Assume the spot is 2 second long. var remark = new RemarkSpot { ReviewId = reviewId, Type = ReviewPiece.PieceTypes.Remark, Id = index, Start = start, Finish = finish, }; return(new ReviewPiece { PartitionKey = ReviewPiece.GetPartitionKey(exerciseId), RowKey = ReviewPiece.GetRowKey(reviewId, ReviewPiece.PieceTypes.Remark, index), Json = JsonUtils.SerializeAsJson(remark), }); }); var batchOperation = new TableBatchOperation(); foreach (var piece in pieces) { batchOperation.InsertOrReplace(piece); } // Write entities which will allow the reviewer to access remarks for reading and writing. We will simply check the presence of one of these records as we read or write the entities. // The write entity will be deleted on review finish. var viewerEntity = new ReviewPiece() { PartitionKey = ReviewPiece.GetPartitionKey(exerciseId), RowKey = ReviewPiece.GetRowKey(reviewId, ReviewPiece.PieceTypes.Viewer, userId), }; batchOperation.InsertOrReplace(viewerEntity); var editorEntity = new ReviewPiece() { PartitionKey = ReviewPiece.GetPartitionKey(exerciseId), RowKey = ReviewPiece.GetRowKey(reviewId, ReviewPiece.PieceTypes.Editor, userId), }; batchOperation.InsertOrReplace(editorEntity); var table = AzureStorageUtils.GetCloudTable(AzureStorageUtils.TableNames.ReviewPieces); await table.ExecuteBatchAsync(batchOperation); // Redirect to the reviews page. return(RedirectToAction("Edit", "Reviews", new { id = reviewId.ToString() })); } } // end of if (remarkSpots.Any()) } // end of if (exerciseId && this.GetIsTeacher()) // if (exerciseId != 0) { return(newUserId == 0 ? RedirectToAction("Index") : RedirectToAction("Index", "Reviews")); } ViewBag.Success = exerciseId != 0; ViewBag.UserDir = userdir; return(View()); } // end of Upload()
public async Task <IHttpActionResult> PutSave([FromUri] string uuid, [FromBody] JObject values) { string redirectToUrl = null; var learnerSkype = (string)values["learnerSkype"]; var titleValue = (string)values["title"]; var comment = (string)values["comment"]; var remarkSpots = ((JArray)values["remarkSpots"]).Select(i => (int)i); // milliseconds var userId = this.GetUserId(); //-- Call the remote transcoding service. var host = ConfigurationManager.AppSettings["RecorderHost"]; var urlFormat = "http://{0}/api/recordings/from_freeswitch/?uuid={1}&userId={2}"; var url = String.Format(urlFormat, host, uuid, userId); HttpClient client = new HttpClient(); HttpResponseMessage response = await client.GetAsync(url); if (!response.IsSuccessStatusCode) { throw new HttpException("from_freeswitch"); } // Error is returned as HTML. Then we get error here: No MediaTypeFormatter is available to read an object of type 'RecordingInfo' from content with media type 'text/html'. var recordingDetails = await response.Content.ReadAsAsync <RecordingDetails>(); // Make sure the duration is known. If the transcoder has failed to parse the ffmpeg logs, it returns DurationMsec = 0. if (recordingDetails.TotalDuration == 0) { // Read the blob and try to determine the duration directly. recordingDetails.TotalDuration = await RecordingUtils.GetMp3Duration(AzureStorageUtils.ContainerNames.Artifacts, recordingDetails.BlobName); } var title = !String.IsNullOrEmpty(titleValue) ? titleValue : Path.GetFileNameWithoutExtension(recordingDetails.BlobName); //-- Create a database record for the exercise. var exerciseId = await ExerciseUtils.CreateExercise(recordingDetails.BlobName, userId, ServiceType.IeltsSpeaking, ArtifactType.Mp3, recordingDetails.TotalDuration, title, null, comment); // If the recording is being uploaded by a teacher, make it owned by the learner, create a review, and save remark spots if an accompanying XML file is provided. if (exerciseId != 0 && this.GetUserIsTeacher()) { //-- Change the exercise author. // We may succeed or fail with updating the user of the exercise depending on whether the provided Skype name is found and is unambiguous. // Continue anyway with either old or new user. var newUserId = (await DapperHelper.QueryResilientlyAsync <int>("dbo.exeTryChangeExerciseAuthor", new { ExerciseId = exerciseId, UserId = userId, SkypeName = learnerSkype, }, CommandType.StoredProcedure)) .SingleOrDefault(); //-- Create a review. We do not charge the learner for a review initiated by the teacher. var reviewId = (await DapperHelper.QueryResilientlyAsync <int>("dbo.exeCreateUploadedReview", new { ExerciseId = exerciseId, UserId = userId, }, CommandType.StoredProcedure)) .SingleOrDefault(); //-- We have got remark spots. Save remarks. if (reviewId != 0 && remarkSpots.Any()) { var pieces = remarkSpots .OrderBy(i => i) .Distinct() .Select((i, index) => { var finish = Math.Max(0, i - AssumedTeacherReactionDelayMSec); var start = Math.Max(0, finish - DefaultSpotLengthMSec); var remark = new RemarkSpot { ReviewId = reviewId, Type = ReviewPiece.PieceTypes.Remark, Id = index, Start = start, Finish = finish, }; return(new ReviewPiece { // This PartitionKey calculation is redundunt, but the overhead is neglectable. PartitionKey = ReviewPiece.GetPartitionKey(exerciseId), RowKey = ReviewPiece.GetRowKey(reviewId, ReviewPiece.PieceTypes.Remark, index), Json = JsonUtils.SerializeAsJson(remark), }); }); var batchOperation = new TableBatchOperation(); foreach (var piece in pieces) { batchOperation.InsertOrReplace(piece); } //-- Write entities which will allow the reviewer to access remarks for reading and writing. We will simply check the presence of one of these records as we read or write the entities. // The write entity will be deleted on review finish. var viewerEntity = new ReviewPiece() { PartitionKey = ReviewPiece.GetPartitionKey(exerciseId), RowKey = ReviewPiece.GetRowKey(reviewId, ReviewPiece.PieceTypes.Viewer, userId), }; batchOperation.InsertOrReplace(viewerEntity); var editorEntity = new ReviewPiece() { PartitionKey = ReviewPiece.GetPartitionKey(exerciseId), RowKey = ReviewPiece.GetRowKey(reviewId, ReviewPiece.PieceTypes.Editor, userId), }; batchOperation.InsertOrReplace(editorEntity); var table = AzureStorageUtils.GetCloudTable(AzureStorageUtils.TableNames.ReviewPieces); await table.ExecuteBatchAsync(batchOperation); } // end of if (reviewId != 0 && remarkSpots.Any()) //-- Return the link to the review page. The client will redirect. redirectToUrl = Url.Link("Default", new { Controller = "Reviews", Action = reviewId != 0 ? "Edit" : "Index", id = reviewId.ToString() }); } // end of if (exerciseId && this.GetIsTeacher()) return(Created(redirectToUrl, redirectToUrl)); }