public async Task<Certificate> AddCertificate(Guid templateId, string userId, string instructorId, Dictionary<string, string> parameters, bool isPreview=false)
		{
			var certificate = new Certificate
			{
				Id = Guid.NewGuid(),
				TemplateId = templateId,
				UserId = userId,
				InstructorId = instructorId,
				Parameters = JsonConvert.SerializeObject(parameters),
				Timestamp = DateTime.Now,
				IsPreview = isPreview,
			};
			db.Certificates.Add(certificate);
			await db.SaveChangesAsync();
			return certificate;
		}
		private string ReplaceCodeReviewsBuiltinParameters(string content, Certificate certificate, Course course)
		{
			var codeReviewsCount = slideCheckingsRepo.GetUsersPassedManualExerciseCheckings(course.Id, certificate.UserId).Count();
			var exercisesMaxReviewScores = course.Slides
				.OfType<ExerciseSlide>().
				ToDictionary(s => s.Id, s => s.Exercise.MaxReviewScore);
			var codeReviewsFullCount = slideCheckingsRepo
				.GetUsersPassedManualExerciseCheckings(course.Id, certificate.UserId)
				.Count(s => s.Score == exercisesMaxReviewScores.GetOrDefault(s.SlideId, -1));

			content = SubstituteOneParameter(content, "codereviews.passed", codeReviewsCount.ToString());
			content = SubstituteOneParameter(content, "codereviews.passed_maxscore", codeReviewsFullCount.ToString());
			return content;
		}
		public async Task RemoveCertificate(Certificate certificate)
		{
			certificate.IsDeleted = true;
			await db.SaveChangesAsync();
		}
		private string ReplaceQuizzesBuiltinParameters(string content, Certificate certificate, Course course)
		{
			var passedQuizzesCount = userQuizzesRepo.GetIdOfQuizPassedSlides(course.Id, certificate.UserId).Count;
			var scoredMaximumQuizzesCount = userQuizzesRepo.GetIdOfQuizSlidesScoredMaximum(course.Id, certificate.UserId).Count;

			content = SubstituteOneParameter(content, "quizzes.passed", passedQuizzesCount.ToString());
			content = SubstituteOneParameter(content, "quizzes.passed_maxscore", scoredMaximumQuizzesCount.ToString());
			return content;
		}
		private string ReplaceBasicBuiltinParameters(string content, Certificate certificate, Course course, string certificateUrl)
		{
			content = SubstituteOneParameter(content, "user.first_name", certificate.User.FirstName);
			content = SubstituteOneParameter(content, "user.last_name", certificate.User.LastName);
			content = SubstituteOneParameter(content, "user.name", certificate.User.VisibleName);

			content = SubstituteOneParameter(content, "instructor.first_name", certificate.Instructor.FirstName);
			content = SubstituteOneParameter(content, "instructor.last_name", certificate.Instructor.LastName);
			content = SubstituteOneParameter(content, "instructor.name", certificate.Instructor.VisibleName);

			content = SubstituteOneParameter(content, "course.id", course.Id);
			content = SubstituteOneParameter(content, "course.title", course.Title);

			content = SubstituteOneParameter(content, "date", certificate.Timestamp.ToLongDateString());
			content = SubstituteOneParameter(content, "date.day", certificate.Timestamp.Day.ToString());
			content = SubstituteOneParameter(content, "date.month", certificate.Timestamp.Month.ToString("D2"));
			content = SubstituteOneParameter(content, "date.year", certificate.Timestamp.Year.ToString());

			content = SubstituteOneParameter(content, "certificate.id", certificate.Id.ToString());
			content = SubstituteOneParameter(content, "certificate.url", certificateUrl);

			return content;
		}
		private string SubstituteBuiltinParameters(string content, Certificate certificate, Course course, string certificateUrl)
		{
			content = ReplaceBasicBuiltinParameters(content, certificate, course, certificateUrl);

			/* Replace %score% for total course score */
			var userScore = visitsRepo.GetScoresForSlides(course.Id, certificate.UserId).Sum(p => p.Value);
			content = SubstituteOneParameter(content, "score", userScore.ToString());

			/* Replace %codereviews.*% */
			content = ReplaceCodeReviewsBuiltinParameters(content, certificate, course);
			/* Replace %quizzes.*% */
			content = ReplaceQuizzesBuiltinParameters(content, certificate, course);

			var acceptedSolutionsCount = userSolutionsRepo.GetAllAcceptedSubmissionsByUser(course.Id, course.Slides.Select(s => s.Id), certificate.UserId).DistinctBy(s => s.SlideId).Count();
			content = SubstituteOneParameter(content, "exercises.accepted", acceptedSolutionsCount.ToString());

			return content;
		}
		private string SubstituteParameters(string content, Certificate certificate, Course course, string certificateUrl)
		{
			var parameters = JsonConvert.DeserializeObject<Dictionary<string, string>>(certificate.Parameters);
			foreach (var kv in parameters)
			{
				content = SubstituteOneParameter(content, kv.Key, kv.Value);
			}

			content = SubstituteBuiltinParameters(content, certificate, course, certificateUrl);

			return content;
		}
		public string RenderCertificate(Certificate certificate, Course course, string certificateUrl)
		{
			var templateDirectory = GetTemplateDirectory(certificate.Template);
			var indexFile = templateDirectory.GetFile(TemplateIndexFile);
			var content = File.ReadAllText(indexFile.FullName);

			return SubstituteParameters(content, certificate, course, certificateUrl);
		}