Здравствуйте, Хабропользователи!
В свете недавно появившейся статьи я хотел бы рассказать Вам историю появления программы для мониторинга количества залогированного времени на oDesk от зарождения идеи до готового приложения.
По роду своей деятельности я чаще всего занимаюсь реализацией web-приложений на базе ASP.NET. Но в то же время, в качестве хобби, иногда пишу desktop-приложения. Обычно это небольшие программки для решения узких задач.
При работе с клиентами мы часто используем oDesk и, в частности, я, как разработчик, пользуюсь приложением oDesk Team Room для логирования процесса работы. И всё бы ничего — время идёт, проект растёт, клиент доволен — но я часто ловил себя на мысли что мне не хватает возможности просмотра залогированного времени для того, чтобы точно знать, сколько времени я потратил на контракт. Иногда это необходимо, чтобы удовлетворить специфические просьбы клиентов типа «на следующей неделе потратьте на меня не более 25 часов» и тому подобные, а иногда и просто для себя, для статистики. Да, конечно, у oDesk есть web-интерфейс, где всю эту информацию можно получить, но мне показалось этого мало, так как мне нужно либо постоянно держать открытой страницу oDesk, либо периодически на неё заходить, только лишь ради того, чтобы посмотреть, сколько времени залогировано к данному моменту.
Этап 1 – идея
В один из вечеров во время просмотра почты мне пришла идея «упростить себе жизнь» при работе с oDesk. Первым делом я решил составить список того, что я хочу получить от приложения. Он был небольшой, но на тот момент полностью меня устраивал:
- Возможность просмотра залогированного времени за день, неделю и месяц;
- Автоматическое обновление показателей каждые 10 минут;
- Ничего лишнего, мне не нужна «программа для всего»;
- Минималистичный интерфейс («Минимум действий – максимум функционала»);
- Не тратить много времени на реализацию, готовое решение нужно было как можно скорее.
Далее, я решил выбрать средства разработки. В силу профессиональной деятельности я чаще всего использую C# и, кроме того, давно хотел сделать win-приложение с использованием технологии WPF. Почему именно WPF? Всего по одной, но весомой на тот момент для меня причине — это приложения «для себя» (я не предполагал, что им будет ещё кто-либо пользоваться) и мне давно хотелось попробовать WPF и, в частности, XAML «вживую».
Этап 2 – анализ и реализация
Первым делом я решил выяснить, как происходит авторизация на oDesk. При беглом просмотре страницы авторизации я сразу заметил 3 достаточно стандартных тэга:
<form enctype="multipart/form-data" id="login_frm" name="" onsubmit="trim_all( this); disable_buttons(); " action="/login.php" method="post" accept-charset="utf-8">
<input id="login" type="text" maxlength="512" size="33" tabindex="1" value="" name="login" placeholder="user name" style="margin-bottom: 10px;">
<input id="password" type="password" maxlength="512" size="33" tabindex="2" value="" name="password" placeholder="password">
Этого вполне достаточно, чтобы залогиниться. Для авторизации и дальнейшей работы с oDesk я решил воспользоваться классом HttpClient. Это стандартный класс из .NET 4.5, но, тем не менее, может использоваться в проектах на .NET 4.0. Для авторизации создаётся форма и посредством Post-запроса отправляется на сервер:
MultipartFormDataContent data = new MultipartFormDataContent();
data.Add(new ByteArrayContent(Encoding.UTF8.GetBytes(this._login)), "login");
data.Add(new ByteArrayContent(Encoding.UTF8.GetBytes(this._password)), "password");
data.Add(new ByteArrayContent(Encoding.UTF8.GetBytes("login")), "action");
HttpResponseMessage response = this._client.Post("https://www.odesk.com/login.php", data);
Далее, необходимо получить список активных контрактов залогиневшегося пользователя. Наиболее простым решением, как мне показалось на тот момент, было взять их со страницы https://www.odesk.com/team/scripts/login?initial=1&after_login_location=http%3A%2F%2Fwww.odesk.com%2Fteam%2Fscripts%2Freport.
Для этого был написан следующий небольшой метод:
this._companies = new List<String>();
HttpResponseMessage response = this._client.Get("https://www.odesk.com/team/scripts/login?initial=1&after_login_location=http%3A%2F%2Fwww.odesk.com%2Fteam%2Fscripts%2Freport");
Regex htmlCompaniesRegex = new Regex(@"(?<=<select name='selected_company'>)(w|W)+(?=</select>)");
String htmlCompanies = htmlCompaniesRegex.Match(response.Content.ReadAsString()).Value;
Regex companiesRegex = new Regex("(?<=<option value=")(\w|\W)+?(?=">)");
foreach (Match company in companiesRegex.Matches(htmlCompanies))
{
(this._companies as List<String>).Add(company.Value);
}
Ничего сложного, просто выбрал значения, используя два регулярных выражения.
Ну и в завершении нужно было получить непосредственные значения счётчиков. Посмотрев внимательно на страницу https://www.odesk.com/team/scripts/report, я обнаружил полезную ссылку на скачивание лога в виде csv:
https://www.odesk.com/team/scripts/report?company_id={имя_контракта}&user_id={логин}&vs_users=&include_offline=1&include_overtime=0&include_online=1&include_memos=1&type=CSV&date={дата}&start_date={дата_начала_выборки}&end_date={дата_конца_выборки}&range=custom
Это как раз то, что нужно. Далее, был написал следующий небольшой код:
private Boolean TryGetWorkedTime(String company, DateTime from, DateTime to, out TimeSpan workedTime)
{
workedTime = TimeSpan.FromMinutes(0);
Boolean success = true;
try
{
String timeUrl = "https://www.odesk.com/team/scripts/report?company_id={0}&user_id={1}&vs_users=&include_offline=1&include_overtime=0&include_online=1&include_memos=1&type=CSV&date={2:MM/dd/yy}&start_date={3:MM/dd/yy}&end_date={4:MM/dd/yy}&range=custom";
HttpResponseMessage response = this._client.Get(String.Format(timeUrl, company, this._login, to, from, to));
CsvReader reader = new CsvReader(new StreamReader(response.Content.ContentReadStream));
while (reader.Read())
{
workedTime += reader.GetField<TimeSpan>(2);
}
}
catch (Exception e)
{
workedTime = TimeSpan.MinValue;
success = false;
}
return success;
}
А следом я дописал метод для получения суммарного времени по всем контрактам:
public Boolean TryGetFullWorkedTime(DateTime date, ResultType type, out TimeSpan workedTime)
{
TimeSpan result = TimeSpan.FromSeconds(0);
Boolean success = true;
DateTime to = date;
DateTime from = date;
if (type == ResultType.Week)
{
from = from.AddDays(-(from.DayOfWeek == DayOfWeek.Sunday ? 6 : ((Int32)from.DayOfWeek - 1)));
}
if (type == ResultType.Month)
{
from = new DateTime(from.Year, from.Month, 1);
}
Object synchroPoint = new Object();
// get counter for each company (in parallel).
if (this.Companies != null && this.Companies.Any())
{
Parallel.ForEach(this.Companies.Except(this.IgnoredCompanies), company =>
{
TimeSpan time;
if (this.TryGetWorkedTime(company, from, to, out time))
{
lock (synchroPoint)
{
result += time;
}
}
else
{
success = false;
result = TimeSpan.MinValue;
}
});
}
else
{
success = false;
result = TimeSpan.MinValue;
}
workedTime = result;
return success;
}
Итак, после всего описаного у меня были все необходимые методы, чтобы сделать готовое приложение. Начал я с простенького интерфейса:
Это не оригинальный скриншот. Жалею, что не сделал его тогда на память. Но эта репродукция очень похожа и неплохо передаёт унылость оригинала. Продолжить я решил уже на следующий день.
Утром следующего дня я подумал над тем, «зачем чаще всего я буду смотреть на это приложение?», и ответ пришёл как-то сам собой: «чтобы следить за показателем времени за день». Исходя из этого, я решил сделать этот показатель более крупным по отношению к остальным. Получилось примерно следующее:
Конечно уже лучше, но всё равно не покидало чувство, что чего-то не хватает. В итоге, после ещё некоторого времени раздумий и зарисовок я пришёл к такому варианту:
Этот вариан нравился мне уже больше. К тому же, он очень хорошо вписывался в концепцию Metro UI Windows 8 UI, которая мне очень нравится.
После того, как окончательный вариант дизайна интерфейса был утверждён, я просто перевёл его на XAML и дописал недостающую логику по взаимодействию. В результате получилась программа, способная сама авторизоваться и периодически обновлять показатели. Это уже было практически то, что мне необходимо.
Этап 3 – Начало использования
Через пару дней я решил, что получившуюся софтину можно показать другим коллегам и услышать их отзывы, замечания, пожелания и предложения. К своему большому удивлению, им идея понравилась, и я просто был засыпан горой различных предложений по её улучшению. Некоторые из них я решил реализовать, но в тоже время много предложений были не приняты из-за того, что предполагали сделать из небольшой утилитки «монстра», который умеет всё. Но я не стал отступать от принципа, который заложил в самом начале, – «ничего лишнего».
После ещё нескольких дней доработок были реализованы некоторые из предложенных идей:
- Темы оформления;
- ProgressBar для отображения процесса обновления показаний;
- Слегка изменён алгоритм поведения при разрыве Интернет-соединения;
- Статистика по предыдущим месяцам (LMB double click);
- Игнор-список;
- Возможность масштабирования (Ctrl + Scroll).
В итоге после всей проделанной работы получилось нечто следующее:
Последними «штрихами» были выбор имени и деплой на локальный сервер. В качестве имени я не придумал ничего лучше (да и думать над именем уже не хотелось), чем oDesktop. Но, тем не менее, мне кажется, это название вполне содержательное.
Завершение
После недолгой разработки я получил вполне удобную (по крайней мере, для меня точно) программку, которая успешно справляется с возложенной на неё функцией уже на протяжении полугода. Судя по отзывам других людей, многим она также пришлась по душе и они пользуются ей. Но, к сожалению, угодить всем не возможно. Есть и те, кому она не понравилась… Не знаю, чем. Они не объясняли. Кроме того, я попробовал на практике WPF.
Сейчас я решил поделиться этой небольшой софтинкой с общественностью, правда пока без исходных кодов. Пока они не в том состоянии, чтобы их кому-либо показывать, впоследствии – будут выложены на гит.
Сейчас появилась идея реализовать аналогичное приложение для Windows 8 это будет, на мой взгляд, неплохим продолжением данного «домашнего» just-for-fun проекта.
Спасибо за внимание!
Автор: f2f2