From calm golden sunsets reflecting off the Pacific to giant waves crashing violently into the sea, the island of Oahu is a story of contrasts. The most visited of the Hawaiian islands, thanks to its gateway international airport, Oahu is home to the capital of Honolulu, where high rises, shops and hotels fill the popular Waikiki neighborhood. But just a few miles away on the North Shore and Windward Coast, small towns still retain some of the local charm and culture that too often gets washed away in tourist hubs.
If you're wondering what there is to do here besides beach days and luaus, this three-day Oahu itinerary covers most of the coastline of the island, with ideas for road trips and where to hike, snorkel or just stop and enjoy the scenery.
Use it to flesh out a short stay on Oahu, or mix it with some relaxing days on the sand in between adventures. We've noted where some of the prettiest, most popular or quietest beaches are along the way.
Since it's not possible to drive a full circle around the island – the road ends at Kaena Point on the west and north sides – this Oahu itinerary is broken down into three areas. You could easily stay in Honolulu and make day trips to explore different sections if you want to have a home base. Driving routes are based on departing from and returning to Honolulu or Waikiki, but can be adapted if you're staying somewhere else on Oahu.
This post contains affiliate links, which means that if you click on or buy something, we might receive a commission for telling you about it. As an Amazon Associate we earn from qualifying purchases.
Though it's the most touristy and crowded area of Oahu, there are tons of attractions and activities in Honolulu and Waikiki, including perhaps the most popular hike on the island (with views that are worth it) and of course, Pearl Harbor. If you only have three days, you can squeeze both of these into one, but if you're staying for longer or don't enjoy hiking, consider devoting more of your day to the various museums and historic sites around Pearl Harbor.
Start your day with a hike at Diamond Head State Monument, a volcanic crater with an old military defense post at the top. The trail to the summit is only 1.6 miles round trip, but with an elevation gain of 560 feet, you'll be doing a bit of moderately strenuous uphill walking and stair climbing. The payoff is a panoramic view of downtown Honolulu and several miles of the surrounding shoreline that often shows up on postcards, plus glimpses of a picture-perfect lighthouse.
There's a small charge to park at the monument (right in the middle of the crater) or a per person fee for walk-ups. The monument is open 6 a.m. to 6 p.m., but go first thing in the morning to avoid the crowds, especially during the busy season. The park has restrooms, picnic areas and concessions at the base.
Lunchtime: Restaurant 604 is convenient to the Pearl Harbor Historic Sites and has waterfront dining with a menu of American and island favorites (like loco moco and kalua pork).
No trip to Oahu would be complete without visiting the World War II Valor in the Pacific National Monument, home to the Pearl Harbor Visitor Center and the USS Arizona Memorial, which sits over the sunken battleship. You'll want to set aside at least 2 to 3 hours here to tour the museums and join the free 75-minute USS Arizona program, which involves an informational video and a boat ride out to the monument. The visitor center and USS Arizona Memorial are free, but the memorial requires a timed ticket, available online 60 days ahead of time (for a small convenience fee). If you haven't reserved a spot before your visit, walk-in tickets are available the day of, but you'll need to arrive early to secure one. The visitor center is open 7 a.m. to 5 p.m.
If you have extra time, you can also visit the Battleship Missouri and Pacific Aviation Museum on Ford Island and the USS Bowfin Submarine Museum and Park, collectively known as the Pearl Harbor Historic Sites. Admission can be purchased at the visitor center.
Book your Passport to Pearl Harbor tickets ahead of time for access to all the Pearl Harbor Historic Sites.
Though it's the most crowded spot on the island, Waikiki Beach boasts a picture-perfect sunset you must see at least once. The west-facing swath of sand has uninterrupted views of the Pacific, with palm trees lining the shores and boats bobbing in the waves. The area is bustling with activity, from surf lessons to live music, so it'll be easy to kill some time if you arrive early. The sun sets at around 7:15 p.m. at the summer solstice and around 5:55 p.m. at the winter solstice.
Extra time? Other popular attractions in the Honolulu area include the Ala Moana Center (a large, open-air shopping mall), Waikiki Aquarium, Honolulu Zoo, Japanese Cultural Center of Hawaii, Punchbowl Crater (site of the National Memorial Cemetery of the Pacific) and an assortment of golf courses.
Today's Oahu road trip traces the North Shore and Windward Coast, where you'll find far fewer crowds than in the Honolulu area. In one day, you can drive this 100-mile path with some beach hopping, but you might have to cherry-pick between the longer activities like the hike, Waimea Valley and the Polynesian Cultural Center if you don't want to feel rushed.
Depart early from Honolulu for your drive north through the farmlands and mountain views of the center of the island, to stretches of empty beachfront and the northwest terminus of the highway at Kaena Point. If you want to do some hiking, the Kaena Point Trail is a 3.5-mile, out-and-back path from the end of the road to the remote western point of the island inaccessible to vehicles.
There is no fee for Kaena Point State Park here, but also no facilities or water along the trail. The park is open during daylight hours. After Kaena Point, the trail continues south along the western coast of the island toward Keawaula Beach, but if you hike this way, you'll be 50 road miles away from where you left your car, so you'll have to hike the whole trail in reverse.
From Kaena, backtrack towards Haleiwa, where you'll meet up with the Kamehameha Highway for a clockwise route around the island for the rest of the day. Historic Haleiwa is a great lunch stop (or brunch if you skipped the hike), with lots of food trucks and locally owned art galleries, surf shops and boutiques.
Coffee break: Haleiwa's Coffee Gallery makes a mean "Mocha Freeze," a combo of espresso, vanilla ice cream and chocolate sauce that will fuel you up for the rest of your North Shore road trip.
The next major point of interest, just 4 miles east, is Waimea Bay, where you'll find a large and popular sandy beach with a big rock people like to jump off of. Across the road, the Waimea Valley preservation has botanical gardens, a waterfall you can swim in and cultural tours of the religious sites. There's an entrance fee for this attraction.
Between Waimea Bay and the northern tip of the island, you find endless stretches of sandy beaches known for their big surf, most notably the Banzai Pipeline, where annual competitions are held. If you have some time, stop at Sunset Beach along here to watch the surfers doing their thing, especially in winter when the swells are large and the pros come out.
Next up is Turtle Bay Resort, which you can drive by if you're short on time. The huge hotel complex on a calm bay has public beach access and parking, as well as restaurants, a spa and a golf course.
Find oceanview rooms, cottages and villas at Turtle Bay Resort.
Dinner stop: The North Shore is known for its shrimp trucks, and you'll find a few options around the prawn and shrimp farms of Kahuku. We tried Fumi's Kahuku Shrimp and the fruit smoothie shop next to it and give both a thumbs up.
You can pop by the Polynesian Cultural Center for the evening on your way toward the Windward Coast. While the paid attractions and demonstrations around the Island Villages here close by early evening, the Hukilau Marketplace and its food kiosks, shops and Polynesian Football Hall of Fame are open late. There's also a luau and evening show, but you'll have to reserve tickets ahead of time and arrive early for the dinner seatings.
Search for tickets to the Polynesian Cultural Center Luau.
The return path will take you along the Windward Coast, past plenty of quiet beaches and a few intriguing sites you'll visit in the daylight tomorrow. Around Kaneohe, you can hop on either the Likelike or Pali Highway to cut through mountain tunnels on the way back to the Honolulu area.
Heading east from Waikiki, you'll drive along the southeast shoreline and the Windward Coast today, taking advantage of white-sand beaches, snorkeling spots and an array of both water and land activities available at parks and preserves. The suggested path is about 65 road miles with a little bit of backtracking to cover sites you drove by but probably missed the day before.
Wake up bright and early for your first visit, the Hanauma Bay Nature Preserve, a popular snorkeling spot full of coral and all sorts of colorful marine life. Many tourists book the shuttle service here from the Waikiki area, and parking (for a small fee) is extremely limited, so arriving early is essential if you plan to drive. The park opens at 6 a.m., and the admission fee is typically waived until 7 a.m., as is the otherwise mandatory video at the education center, which tells you not to touch or walk on the coral, amongst other things. Since preservation is essential to the wellbeing of the resident species, the bay is closed on Tuesdays so the fish can eat in peace. Be prepared to pay extra for things like lockers, a shuttle to and from the beach (walking is free), and snorkel gear (you can bring your own).
Shop for snorkel gear on Amazon.com.
If you want to do some hiking on this coast, the Makapuu Point Lighthouse Trail is a 2-mile, round-trip hike along the Kaiwi State Scenic Shoreline that takes roughly 2 hours to walk. The route follows an old access road towards a red-roofed lighthouse (the same one you can catch glimpses of on the Diamond Head hike from day 1). From the elevated trail you'll be able to see the offshore islands and, if you're lucky, humpback whales during migration season (November-May).
Picnic lunch: With so many pretty beach parks on your drive, why not make a picnic lunch to enjoy on the sand? There are a couple of grocery stores in Kailua and Kaneohe where you can grab supplies.
The beaches you'll encounter along the Windward Coast are ideal for swimming and kayaking, thanks to calm waters. Lanikai Beach offers soft, white sand and fantastic views of the Mokulua Islands State Seabird Sanctuary, a couple of offshore islets full of Hawaiian birdlife. Neighboring Kailua Bay has a long stretch of sand with mountain views and beachside parks with facilities. Just a few miles up the road, the large Kaneohe Bay has a huge sandbar and a triangular islet known as Chinaman's Hat that you can paddle out to. He'eia State Park and Kualoa Regional Park are both on the bay.
One of the most popular destinations on the Windward Coast, Kualoa Ranch is the place to go if you're into active adventures or movie history. The 4,000-acre nature reserve and cattle ranch is a popular filming site for movies and tv shows, thanks to its scenic and diverse landscape that includes wide valleys, mountain cliffs and sandy beaches. All sorts of tours are offered here, from ziplining and ATV rides to guided excursions tracing filming sites for "Jurassic Park," "Godzilla" and more. Experiences need to be booked ahead of time and most are offered in the morning to mid-afternoon, so you might have to head here straight from Hanauma Bay if your reservation is early. The two locales are only 30 miles apart, though.
Book a Kualoa Ranch Movie Tour or a Kualoa Ranch Zipline Tour.
When you finish up on the Windward Coast, head south back towards Kaneohe, where you'll take the Pali Highway through the Ko'olau range. If it's still light out, you can pull over at the Nu'uanu Pali Lookout for a panoramic view of the coastline you've been driving.
Extra time? Byodo-In Temple near Kaneohe Bay is a nice restful stop with picturesque grounds featuring a replica Japanese temple. If you skip Kualoa Ranch, you might have time to drive a little further north to Ahupua'a 'O Kahana State Park, where you'll find hiking trails in a landscape stretching from sea to mountains. The Polynesian Cultural Center from day 2 is also just 12 miles north of Kualoa Ranch, so if you skipped it before, you can incorporate it here.
Search for Oahu hotels and resorts on Booking.com.
Can't get enough of Hawaii? Check out this Maui itinerary for 5 days on the island and discover all the ways you'll fall in love with Kauai.


text:
Subscribe to Jenifer M. Bologna's Posts
Jenifer Bologna is a principal in the White Plains, New York, office of Jackson Lewis P.C.
Contact: Read more about Jenifer M. Bologna
By Jenifer M. Bologna & Patricia Anderson Pryor on January 18, 2023
On Friday, January 13, 2022, a New York State Supreme Court Judge for Onondaga County struck down the New York State Department of Health regulation mandating certain healthcare professionals be "fully vaccinated" against COVID-19, declaring the regulation to be "null, void, and of no effect." (Medical Professionals for Informed Consent, et. al. v. Bassett, et…
Fusion Medical Animation, Unsplash
New York Department of Health Revises the COVID-19 Return-to-Work Protocols for Healthcare Personnel, Consistent with CDC Recommendations
By Jenifer M. Bologna, Henry S. Shapiro & Tania J. Mistretta on December 19, 2022
On November 30, 2022, the New York State Department of Health (NYSDOH) updated its Advisory on Return-to-Work Protocols for Healthcare Personnel with SARS-CoV-2 Infection or Exposure to Sars-CoV-2. This new guidance supersedes previous New York guidance to be consistent with recommendations published by the Centers for Disease Control and Prevention (CDC).
For recommendations on…
New York Bars Discipline for Legally Protected Employee Absences
By Jenifer M. Bologna, Henry S. Shapiro & Tania J. Mistretta on December 2, 2022
On November 21, 2022, New York Governor Kathy Hochul signed a law clarifying that it is unlawful for an employer to penalize an employee for any absence protected under federal, state or local law. (S.1958/A.8092). The law goes into effect on February 19, 2023.
This new law amends Section 215 of…
CDC'S COVID-19 Guidance Changes… Again
By Jenifer M. Bologna, Katharine C. Weber & Patricia Anderson Pryor on August 11, 2022
Since March 2020, COVID-19 rules have been confusing at best. On August 11, 2022, in an effort to streamline the guidance and reflect the current state of the pandemic, the CDC once again issued updated guidance. The new guidance focuses on individual responsibility and is designed to help the public better understand how to…
What Employers Need to Know About Monkeypox
By Katharine C. Weber, Melanie L. Paul, Patricia Anderson Pryor, Jenifer M. Bologna & Tania J. Mistretta on August 5, 2022
Posted in Health Management, Monkeypox
Monkeypox (MPV) is the latest virus to catch wide attention. But it is important for employers to keep in mind that MPV is not COVID-19. Nevertheless, there are steps employers can take. Read the full article here.
U.S. Supreme Court Puts OSHA COVID-19 Vaccine-Or-Test ETS Back on Ice
By Courtney M. Malveaux, Melanie L. Paul, Patricia Anderson Pryor, Jenifer M. Bologna, Sheri L. Giger, Katharine C. Weber, Joseph J. Lynett & Kristina T. Brooks on January 13, 2022
Posted in COVID-19, OSHA ETS
In a 6-3 ruling, the U.S. Supreme Court has granted a temporary stay of the Occupational Safety and Health Administration's (OSHA) Emergency Temporary Standard (ETS).
The Court described the standard as a "blunt instrument" demanding most employers to require two-thirds of the American workforce to receive COVID-19 vaccinations or otherwise undergo weekly testing. The justices…
CDC's New Shorter Quarantine Period is Welcome News for Those Who Got COVID for Christmas
By Patricia Anderson Pryor, Jenifer M. Bologna, Sheri L. Giger & Tara K. Burke on December 27, 2021
The CDC announced today that it is updating its quarantine and isolation guidance. For people with COVID-19, the isolation period was reduced from ten days to five days as long as the individual has no symptoms or their symptoms are resolving after five days. Importantly, the revised isolation guidance does not recommend an individual…
Sixth Circuit Dissolves Stay of OSHA COVID-19 Vaccine ETS
By Courtney M. Malveaux, Melanie L. Paul, Patricia Anderson Pryor, Jenifer M. Bologna, Catherine A. Cano, Sheri L. Giger & Katharine C. Weber on December 20, 2021
The U.S. Court of Appeals for the Sixth Circuit has lifted the Fifth Circuit's stay of the Occupational Safety and Health Administration's (OSHA) Emergency Temporary Standard (ETS) on COVID-19 vaccination and testing for employers with at least 100 employees. In re: MCP No. 165, Occupational Safety & Health Admin. Rule on COVID-19 Vaccination and Testing, …

redpajama_set_name: RedPajamaCommonCrawl


Tesseract Salt – Eases one into the 4th dimension, into Soul awareness. The Sacred sounds of the Tesseract Salt Crystal Singing Bowls carry the energies of tesseract salt of all Earth's ancient oceans. As we are spiritual beings of water, salt, crystalline and DNA bodies, the sacred geometry of a 4-dimensional tesseract cube within a cube replenishes itself and assists our ascension pathway into the higher spiritual dimensional energies.


You're never too young to achieve your dreams. Nitesh Yadav, a coder with a social cause, stands testament.
Nitesh with an inspiring story is now taking steps to compile and publish his debut book.
It's amazing how a little self-belief and determination can go a long, long way. Like they say, when you have your eye on the prize, everything around it is immediately out of focus. This story, if nothing else, will definitely inspire your soul.
He first came in contact with a smartphone at the age of 13 and was intrigued by all that it could do. What immediately caught his attention was the applications on it. "I come from a village background; to spot a smartphone is a rarity. I was in class eight when I first saw a man in our village with a phone and learnt of something known as an Android app developer."
Having seen the benefits of a smartphone he was now bent on doing something for his locality and village. He wanted to be an app developer. Although he lacked the resources, he pressed forward, asking his father to support his dreams and buy him a smartphone. "When I told my father about an Android app, he was clueless. He still supported me and bought me a small smartphone after a lot of pleading."
A number of other difficulties and challenges followed. To code an app requires skill and training. Nitesh's family could not afford to send him to coding classes, and that was when Nitesh made his way to what he calls his "Google Gurudev." I read an article about an app developer who had created a number of educational apps. When I finally figured out how to use the smartphone, I made my way to Google Gurudev who has taught all me that I know about coding apps and creating websites."
His first app, 'Mera Kanharka', took him four months to make and was launched on Google Play. This app helps connect his village to digital India, giving them access to the outside world.
Rising above being made fun of by friends and village members, lacking the information required to make an app, and having a hard time coping with the Android language, his is a success story unlike any other.
There's more-he takes Skype classes for students at his village and others on various social media platforms regularly, teaching them about web developing at no cost. He is also currently working on his software development company named Techsicon that aims to promote any business as a brand to the global level.
In the process of establishing an NGO too, Nitesh says, "My aim is to digitise every single village in India and train students in web development and robotics so they're able to design apps themselves and give back to the community. Moreover, I do not want them to go through the hardships I have."
With invitations from the likes of Google, NASSCOM, and Programming Hub would it be wrong to call this young talent the man of the hour?
His story, from being demoralised to emerging gloriously through it all, is motivational and get-up-and-achieve-your-dreams worthy.


text:
Principles of Isometric Projection
What is Isometric Projection?
Iso (in Greek) means equal; metric means to measure. Hence Isometric projection means a system of projection of equality of measure.
Orthographic Projection of a cube: The principle involved in drawing an isometric projection can be explained best by drawing the Orthographic Projection of a Cube, resting on HP on one of its corners with a solid diagonal (solid diagonal is an imaginary line joining one of the corners at the top and the diametrically opposite corner at the bottom) perpendicular to HP.
Isometric Projection: The diagram shows the top view of the cube, obtained from the Orthographic projection. It gives a fair idea of a pictorial form of the cube and its Isometric Projection.
All the edges and faces of the cube are equally inclined to the plane of projection. Hence the square faces are seen as similar and equal rhombuses.
The three edges/sides of the cube AB, AD and AE are meeting at the point A. These edges are mutually perpendicular to each other in the solid.
Above three edges from 90º in the Orthographic Projection and are now seen to include 120º with each other in Isometric Projection.
Edges AB and AD make 30º with the horizontal. Edge AE is vertical.
The three edges AB, AD and AE meeting at A forming a right angle are also equally inclined to VP and therefore they are equally foreshortened.
All the other edges like CB and BF are parallel to any of the lines AB, AD, AE and are also equally foreshortened. These edges being equally foreshortened leads us to the problem of selecting an Isometric scale.
BD and AC represent equal diagonals of the cube but are equally shortened in Isometric view. Diagonal DA of the top face of the cube is parallel to the VP. Hence it retains its true length in Isometric. However, the diagonal AC is shortened.
www.techglads.com


I am 21 years old and am currently studying 3rd year Visual Communication and Design and the University of the Highlands and Islands. I have previously attended the Digital Media course at Forth Valley College in Stirling.
My ambition within the course is to explore my skills and abilities and to push myself further, past my comfort zone (and laziness).
My goals for the year ahead are to create content that will enhance my work portfolio, improve my skills using Adobe CC and generally boost my knowledge of the industry.


text:
New Mexico firefighter hurt in firework explosion dies
A New Mexico firefighter who was seriously injured earlier this month in a fireworks explosion has died.
New Mexico firefighter hurt in firework explosion dies A New Mexico firefighter who was seriously injured earlier this month in a fireworks explosion has died. Check out this story on lcsun-news.com: https://www.lcsun-news.com/story/news/local/new-mexico/2019/07/22/new-mexico-roswell-firefighter-dies-firework-explosion/1795272001/
Associated Press Published 2:16 p.m. MT July 22, 2019
A New Mexico firefighter who was seriously injured earlier this month in a fireworks explosion has died. (Photo: Courier-Post file photo)
ROSWELL, N.M. – A New Mexico firefighter who was seriously injured last month in a fireworks explosion has died.
Roswell Fire Chief Devin Graham announced that Roswell firefighter Jeff Stroble died Sunday in Lubbock, Texas, where he was hospitalized for injuries suffered in a June 5 explosion. He was 46.
Robert "Hoby" Bonham of the Roswell Fire Department also was injured in the blast. He was sent home from the hospital in late June.
Las Cruces:City to discuss crime and mental health at work session
The pair were critically injured while moving fireworks material at a New Mexico storage area for a planned show.
Twelve firefighters in all were hurt. Most sustained minor injuries.
The city officials in Roswell destroyed the remainder of the fireworks for safety reasons.
More news:187 pledge allegiance to the United States at Las Cruces naturalization ceremony
Read or Share this story: https://www.lcsun-news.com/story/news/local/new-mexico/2019/07/22/new-mexico-roswell-firefighter-dies-firework-explosion/1795272001/


text:
Mishel and Steve's Wedding Album
One major red flag emerges on an otherwise stunning wedding day
By Peter Conte|9:56am Feb 10, 2020
Mishel Karen
Steve Burley
Mishel And Steve
1 of 22Attribution: Nine
Mishel, a 48-year-old teacher, and Steve, a 51-year-old barber shop owner, enjoyed a stunning wedding on Married At First Sight 2020, but it wasn't without complications.
Scroll through to see professional photos from their big day.
Barber shop owner Steve is ready for marriage.
He says he's been in love before but sadly the relationship didn't work out. He was also diagnosed with cancer, which unfortunately put a huge strain on the relationship.
The experts described Mishel as "48 yet young at heart" and "a little bit quirky."
The mother of two was previously married for almost 13 years before she was sadly cheated on by her partner.
Steve revealed ahead of the wedding that he was "looking to make hay while the sun shines" and that was was hoping the experts would match him with someone he'd be able to fall in love with.
"When I look down the aisle hopefully I see my future and never have to look again," said Mishel.
She then met Steve and the two instantly exchanged compliments.
Steve's Vows:
"Mishel, I will be there for you though great times and not-so-great times. I will care for you, be loyal to you, I will make you laugh and hopefully never make you cry. And perhaps as time moves forward I will fall in love you.
"Today is the day that brought us both together. It's a special day, not only for both of us, but for all our family and friends. I do hope your family and friends approve of me. If not, I'll do my utmost to change their opinions.
"[To Mishel's guests] Your daughter, friend or sister is in very safe hands."
Mishel's Vows:
"Today as I stand here, I pledge to you to be your faithful friend and partner. I promise to be thoughtful and kind, and never leave you behind. It kind of rhymed, sorry.
"I will respect you and trust you, and cherish your uniqueness. And I hope that you learn to adore my unique qualities.
"I enter this commitment with you knowing that the magic of love is yet to come. I will try my best, and I hope my best will be enough for you to fall in love with me.
"Whatever lies ahead, good or bad, we'll face together."
"The way he kissed me it was smack bang on the lips. It was very nice," said Mishel, adding, "I think I might kiss him again."
9 of 22Attribution: Getty
Mishel and Steve looked stunning during their professional photo shoot, surrounded by huge flower arrangements as they looked longingly into each others' eyes.
10 of 22Attribution: Nine
And of course the pair shared a few kisses too. Clearly Mishel was following up on her post-wedding thoughts.
It's all smiles for Mishel and Steve on their sunny wedding day.
Clearly there's chemistry between the newlywed couple!
The pair then went on to enjoy their reception at Shadowfax Winery.
The pair hit it off at the beginning of the reception, with Mishel claiming she "couldn't have asked for more of a gentleman."
Married At First Sight 2020.
However a huge red flag arose during the afternoon.
When Mishel's daughter revealed to Steve that she's been cheated on in the past, Steve in turn admitted that he has been a cheater in the past, albeit "19 years ago."
Word got back to Mishel, who reacted by saying "once a cheater always a cheater."
Instantly she had concerns over Steve, and eventually Steve caught wind and sat Mishel down to explain the situation.
"What I did 17 years ago made me a low life and I've lived with it ever since... I made a stupid error. What happened 17 years ago was 17 years ago," said Steve.
Mishel opened up to Steve about being cheated on in the past, and Steve reassured Mishel that he would never cheat on her.
"He seems quite genuine. I feel like he honestly cares how I feel," revealed Mishel.
"It was just so reassuring... Maybe I did overreach," concluded Mishel.
Both Mishel and Steve delivered speeches on their big day, with Mishel's clearly leaving the guests in stitches.
The pair look every part the perfect couple as they cut their stunning four-tier cake together.
And of course, no wedding is complete without a slow dance.
We can't wait to see what's next for Mishel and Steve.
-- no more galleries --


text:
// This file is part of Indico.
// Copyright (C) 2002 - 2020 CERN
//
// Indico is free software; you can redistribute it and/or
// modify it under the terms of the MIT License; see the
// LICENSE file for more details.

function userListNothing(data, func) {
  func(true, data);
}

function singleUserNothing(user, func) {
  func(true);
}

function create_favorite_button(user_id) {
  var integerRegex = /^[0-9]+$/;
  if (!integerRegex.test(user_id)) {
    // external user
    return $('');
  }
  var active = !!Indico.User.favorite_users[user_id],
    span = $('')
      .attr({
        'title': active
          ? $T('Remove from your list of favorite users')
          : $T('Add to your list of favorite users'),
        'data-id': user_id,
      })
      .on('click', function(e) {
        e.stopPropagation();
        $(document).trigger(
          'favorites.' + ($(this).hasClass('active') ? 'remove-user' : 'add-user'),
          user_id
        );
      });

  if (active) {
    span.addClass('active');
  }

  return span;
}

$(function() {
  $(document)
    .on('favorites.remove-user', function(e, user_id) {
      $.ajax({
        url: build_url(Indico.Urls.FavoriteUserRemove, {fav_user_id: user_id}),
        method: 'DELETE',
        error: handleAjaxError,
      }).done(function() {
        $('.favorite-user[data-id=' + user_id + ']').removeClass('active');
        delete Indico.User.favorite_users[user_id];
      });
    })
    .on('favorites.add-user', function(e, user_id) {
      $.ajax({
        url: Indico.Urls.FavoriteUserAdd,
        method: 'POST',
        dataType: 'json',
        data: {user_id: [user_id]},
        error: handleAjaxError,
      }).done(function(result) {
        $('.favorite-user[data-id=' + user_id + ']').addClass('active');
        Indico.User.favorite_users[user_id] = result.users[0];
      });
    });
});

/**
 * @param {String} style The class name of the
 element inside this FoundPeopleList
 *                       If left to null, it will be "user-list"
 *
 * @param {Function} selectionObserver A function that will be called when the selection varies. The function will be called without arguments
 *                                     (it can use public methods of FoundPeopleList to get information).
 *
 * @param {Boolean} onlyOne If true, only 1 item can be selected at a time.
 */
type(
  'FoundPeopleList',
  ['SelectableListWidget'],
  {
    _drawItem: function(pair) {
      var self = this;
      var peopleData = pair.get();

      if (peopleData.get('isGroup') || peopleData.get('_type') === 'group') {
        return Html.div('info', Html.span('name', peopleData.get('name')));
      } else {
        var userName = Html.span(
          'name',
          peopleData.get('firstName'),
          ' ',
          peopleData.get('familyName')
        );
        var userEmail = Html.span(
          {id: self.id + '_' + pair.key + '_email', className: 'email'},
          Util.truncate(peopleData.get('email'), 40)
        );
        var affiliation = Html.$(
          $('').text(peopleData.get('affiliation'))
        );
        var html = Html.div('info', userName, userEmail, affiliation);

        var $actionButtons = $('
', {class: 'actions'});
        if (
          this.showToggleFavouriteButtons &&
          IndicoGlobalVars.isUserAuthenticated &&
          peopleData.get('_type') == 'Avatar'
        ) {
          var favouritizeButtonDiv = create_favorite_button(peopleData.get('id')).get(0);
          $actionButtons.append(favouritizeButtonDiv);
        }
        if (peopleData.get('_type') == 'EventPerson') {
          var userId = peopleData.get('user_id');
          if (userId) {
            $actionButtons.append(create_favorite_button(userId));
          }
          $('', {
            class: 'event-person',
            title: $T('This person exists in the event'),
          }).appendTo($actionButtons);
        }

        return [html, new Html.$($actionButtons)];
      }
    },
  },

  /**
   * Constructor for FoundPeopleList
   */
  function(style, onlyOne, selectionObserver, showToggleFavouriteButtons, favouriteButtonObserver) {
    if (!exists(style)) {
      style = 'user-list';
    }

    this.onlyOne = any(onlyOne, false);
    this.showToggleFavouriteButtons = any(showToggleFavouriteButtons, true);
    this.favouriteButtonObserver = any(favouriteButtonObserver, null);

    /*this.lastTargetListItem = null;*/

    this.SelectableListWidget(selectionObserver, this.onlyOne, style, 'selected', 'unselected'); //, this.__mouseoverObserver);
  }
);

/**
 * Base class for UserSearchPanel and GroupSearchPanel
 * @param {Boolean} onlyOne If true, only 1 item can be selected at a time.
 * @param {function} selectionObserver Function that will be called when the selected users xor groups change.
 *                                     Will be passed to the inner FoundPeopleList.
 */
type(
  'SimpleSearchPanel',
  ['IWidget'],
  {
    /**
     * Simulates a click in the search button
     */
    _searchAction: function() {
      Dom.Event.dispatch(this.searchButton.dom, 'click');
    },

    /**
     * Form to fill in group search data
     */
    _createSearchForm: function() {
      // To be overloaded by implementing classes
    },

    /**
     * Function to be executed when the search button is pressed
     */
    _search: function() {
      // To be overloaded by implementing classes
    },

    /**
     * Returns the part of the form corresponding to search including external authenticators
     * Should be called by the implementing classes' _createSearchForm method.
     */
    _createAuthenticatorSearch: function() {
      var self = this;
      if (empty(Indico.Settings.ExtAuthenticators) || !this.allowExternal) {
        return null;
      } else {
        var searchExternalCB = Html.checkbox({});
        $B(searchExternalCB, self.criteria.accessor('search-ext'));
        return [[$T('Users with no Indico account'), searchExternalCB]];
      }
    },

    /**
     * Returns the list of selected users xor groups for this panel
     */
    getSelectedList: function() {
      return this.foundPeopleList.getSelectedList();
    },

    /**
     * Clears the selection
     */
    clearSelection: function() {
      this.foundPeopleList.clearSelection();
    },

    /**
     * Returns the Panel's DOM
     */
    draw: function() {
      var self = this;

      this.searchForm = this._createSearchForm();

      this.searchButton = Html.input('button', {}, $T('Search'));
      this.searchButtonDiv = Html.div('searchUsersButtonDiv', this.searchButton);
      this.searchButton.observeClick(function() {
        self._search();
      });

      this.foundPeopleListDiv = Html.div('user-search-results', this.foundPeopleList.draw());

      this.container = Html.div(
        {},
        this.searchForm.get(),
        this.searchButtonDiv,
        this.foundPeopleListDiv,
        this.extraDiv
      );

      if (this.submissionRights) {
        var checkbox = Html.checkbox({}, true);
        $B(this.grantRights.accessor('submission'), checkbox);
        var submissionDiv = Html.div(
          {style: {marginTop: pixels(5), marginLeft: pixels(-4)}},
          checkbox,
          $T('Grant all selected users submission rights')
        );
        this.container.append(submissionDiv);
      }

      return this.IWidget.prototype.draw.call(this, this.container);
    },
  },

  /**
   * Constructor for SimpleSearchPanel
   */
  function(
    onlyOne,
    selectionObserver,
    showToggleFavouriteButtons,
    favouriteButtonObserver,
    submissionRights,
    grantRights,
    extraDiv,
    allowExternal
  ) {
    this.IWidget();
    this.onlyOne = any(onlyOne, false);

    this.criteria = new WatchObject();

    this.foundPeopleList = new FoundPeopleList(
      null,
      this.onlyOne,
      selectionObserver,
      showToggleFavouriteButtons,
      favouriteButtonObserver
    );
    this.foundPeopleList.setMessage($T('You can use the form above to search for users...'));
    this.extraDiv = any(extraDiv, Html.div({}));

    this.searchForm = null;
    this.searchButton = null;
    this.searchButtonDiv = null;
    this.foundPeopleListDiv = null;
    this.container = null;

    this.submissionRights = submissionRights;
    this.grantRights = grantRights;
    this.allowExternal = allowExternal;
  }
);

/**
 * Panel to search for users.
 * @param {Boolean} onlyOne If true, only 1 item can be selected at a time.
 * @param {function} selectionObserver Function that will be called when the selected users xor groups change.
 *                                     Will be passed to the inner FoundPeopleList.
 */
type(
  'UserSearchPanel',
  ['SimpleSearchPanel'],
  {
    /**
     * Form to fill in user search data
     */
    _createSearchForm: function() {
      var self = this,
        do_search = function() {
          self._searchAction();
          return false;
        };

      this.fields = {
        surName: new EnterObserverTextBox(
          'text',
          {style: {width: '100%'}, id: 'userSearchFocusField'},
          do_search
        ),
        name: new EnterObserverTextBox('text', {style: {width: '100%'}}, do_search),
        email: new EnterObserverTextBox('text', {style: {width: '100%'}}, do_search),
        organisation: new EnterObserverTextBox('text', {style: {width: '100%'}}, do_search),
        exactMatch: Html.checkbox({}),
      };

      var fieldList = [
        [$T('Family name'), this.fields.surName.draw()],
        [$T('First name'), this.fields.name.draw()],
        [$T('E-mail'), this.fields.email.draw()],
        [$T('Organisation'), this.fields.organisation.draw()],
        [$T('Exact Match'), this.fields.exactMatch],
      ];

      var authenticatorSearch = this._createAuthenticatorSearch();
      if (exists(authenticatorSearch)) {
        fieldList = concat(fieldList, authenticatorSearch);
      }

      return IndicoUtil.createFormFromMap(fieldList, true);
    },

    /**
     * Function that is called when the Search button is pressed.
     */
    _search: function() {
      var self = this;

      self.searchButton.dom.disabled = true;
      this.foundPeopleList.setMessage(
        Html.div({style: {paddingTop: '20px'}}, progressIndicator(false, true))
      );

      _.each(this.fields, function(widget, key) {
        self.criteria.set(key, widget.get());
      });

      indicoRequest('search.users', self.criteria, function(result, error) {
        if (!error) {
          self.foundPeopleList.clearList();
          if (result.length === 0) {
            self.foundPeopleList.setMessage($T('No results for this search...'));
          } else {
            each(result, function(entry) {
              if (entry._type === 'Avatar') {
                self.foundPeopleList.set('existingAv' + entry.id, $O(entry));
              } else if (entry._type === 'EventPerson') {
                self.foundPeopleList.set('existingEventPerson' + entry.id, $O(entry));
              }
            });
          }
          self.searchButton.dom.disabled = false;
        } else {
          self.foundPeopleList.clearList();
          showErrorDialog(error);
        }
      });
    },
    /**
     * Returns the panel's DOM
     */
    draw: function() {
      return this.SimpleSearchPanel.prototype.draw.call(this);
    },
  },

  /**
   * Constructor for UserSearchPanel
   */
  function(
    onlyOne,
    selectionObserver,
    conferenceId,
    showToggleFavouriteButtons,
    favouriteButtonObserver,
    submissionRights,
    grantRights,
    extraDiv,
    allowExternal
  ) {
    this.SimpleSearchPanel(
      onlyOne,
      selectionObserver,
      showToggleFavouriteButtons,
      favouriteButtonObserver,
      submissionRights,
      grantRights,
      extraDiv,
      allowExternal
    );
    if (exists(conferenceId)) {
      this.criteria.set('conferenceId', conferenceId);
    }
    this.fields = {};
  }
);

/**
 * Panel to search for groups.
 * @param {Boolean} onlyOne If true, only 1 item can be selected at a time.
 * @param {function} selectionObserver Function that will be called when the selected users xor groups change.
 *                                     Will be passed to the inner FoundPeopleList.
 */
type(
  'GroupSearchPanel',
  ['SimpleSearchPanel'],
  {
    /**
     * Form to fill in group search data
     */
    _createSearchForm: function() {
      var self = this;

      var groupName = new EnterObserverTextBox('text', {style: {width: '100%'}}, function() {
        self._searchAction();
        return false;
      });
      var exactMatch = Html.checkbox({});

      $B(groupName, this.criteria.accessor('group'));
      $B(exactMatch, this.criteria.accessor('exactMatch'));
      this.criteria.set('exactMatch', true);

      var fieldList = [[$T('Group name'), groupName.draw()], [$T('Exact Match'), exactMatch]];
      return IndicoUtil.createFormFromMap(fieldList, true);
    },

    /**
     * Function that is called when the Search button is pressed.
     */
    _search: function() {
      var self = this;

      self.searchButton.dom.disabled = true;
      this.foundPeopleList.setMessage(
        Html.div({style: {paddingTop: '20px'}}, progressIndicator(false, true))
      );

      indicoRequest('search.groups', self.criteria, function(result, error) {
        if (!error) {
          self.foundPeopleList.clearList();
          if (result.length === 0) {
            self.foundPeopleList.setMessage($T('No results for this search...'));
          } else {
            each(result, function(group) {
              self.foundPeopleList.set(group.name, $O(group));
            });
          }
          self.searchButton.dom.disabled = false;
        } else {
          self.foundPeopleList.clearList();
          showErrorDialog(error);
        }
      });
    },

    /**
     * Returns the panel's DOM
     */
    draw: function() {
      return this.SimpleSearchPanel.prototype.draw.call(this);
    },
  },

  /**
   * Constructor for GroupSearchPanel
   */
  function(onlyOne, selectionObserver, conferenceId, showToggleFavouriteButtons) {
    this.SimpleSearchPanel(onlyOne, selectionObserver, conferenceId, showToggleFavouriteButtons);
  }
);

/**
 * Tabbed panel to search either for users or groups
 * @param {Boolean} onlyOne If true, only 1 item can be selected at a time.
 * @param {function} selectionObserver Function that will be called when the selected users xor groups change.
 *                                     Will be passed to the inner FoundPeopleList.
 */
type(
  'UserAndGroupsSearchPanel',
  ['IWidget'],
  {
    /**
     * Function that will be called when the selection changes in either of the panel
     * @param {String} panel 'users' or 'groups', to know which panel's selection changed
     * @param {WatchObject} selectedList a list of selected users.
     */
    __selectionObserver: function(panel, selectedList) {
      if (this.onlyOne) {
        if (panel === 'users') {
          this.groupPanel.clearSelection();
        } else {
          this.userPanel.clearSelection();
        }
        this.parentSelectionObserver(selectedList);
      } else {
        var totalSelection = $O();
        totalSelection.update(this.userPanel.getSelectedList().getAll());
        totalSelection.update(this.groupPanel.getSelectedList().getAll());
        this.parentSelectionObserver(totalSelection);
      }
    },

    /**
     * Returns the list of selected users / groups in both panels
     * @return {WatchObject} The list of selected users. Key = user / group id, value = user data
     */
    getSelectedList: function() {
      var totalSelection = $O();

      totalSelection.update(this.userPanel.getSelectedList().getAll());
      totalSelection.update(this.groupPanel.getSelectedList().getAll());

      return totalSelection;
    },

    /**
     * Clears the selection in both panels
     */
    clearSelection: function() {
      this.userPanel.clearSelection();
      this.groupPanel.clearSelection();
    },

    /**
     * Returns the panel's DOM
     */
    draw: function() {
      this.tabWidget = new JTabWidget([
        [$T('Users'), this.userPanel.draw()],
        [$T('Groups'), this.groupPanel.draw()],
      ]);
      return this.IWidget.prototype.draw.call(this, this.tabWidget.draw());
    },
  },

  /**
   * Constructor for UserAndGroupsSearchPanel
   */
  function(
    onlyOne,
    selectionObserver,
    conferenceId,
    showToggleFavouriteButtons,
    favouriteButtonObserver,
    submissionRights,
    grantRights,
    extraDiv,
    allowExternal
  ) {
    this.IWidget();
    this.onlyOne = any(onlyOne, false);
    this.parentSelectionObserver = selectionObserver;

    var self = this;

    this.userPanel = new UserSearchPanel(
      this.onlyOne,
      function(selectedList) {
        self.__selectionObserver('users', selectedList);
      },
      conferenceId,
      showToggleFavouriteButtons,
      favouriteButtonObserver,
      submissionRights,
      grantRights,
      extraDiv,
      allowExternal
    );
    this.groupPanel = new GroupSearchPanel(
      this.onlyOne,
      function(selectedList) {
        self.__selectionObserver('groups', selectedList);
      },
      showToggleFavouriteButtons
    );

    this.tabWidget = null;
  }
);

/**
 * Panel with a list of suggested users.
 * @param {Boolean} onlyOne If true, only 1 item can be selected at a time.
 * @param {Boolean} includeFavourites If True, favourites will appear in the list of suggested users of the ChooseUsersPopup.
 * @param {WatchObject} suggestedUsers A list of users that will be used as list of suggested users.
 *                                     This argument has to be an array or WatchList where the keys are the user ids and the values are fossils/dictionaries with the user data.
 *                                     If null, there will be no suggestedUsers panel.
 * @param {function} selectionObserver Function that will be called when the selected users xor groups change.
 *                                     Will be passed to the inner FoundPeopleList.
 */
type(
  'SuggestedUsersPanel',
  ['IWidget'],
  {
    getSelectedList: function() {
      return this.suggestedUserList.getSelectedList();
    },

    clearSelection: function() {
      this.suggestedUserList.clearSelection();
    },

    draw: function() {
      var self = this;

      this.titleDiv = Html.div('suggestedUsersTitle', Html.span({}, $T('Suggested users')));

      if (this.suggestedUserList.isEmpty()) {
        var message = Html.span(
          {},
          $T('There are no suggested users for you at the moment. Why not add some '),
          Html.a({href: Indico.Urls.Favorites}, $T('favourites')),
          '?'
        );
        this.suggestedUserList.setMessage(message);
      }

      this.suggestedUserListDiv = Html.div('suggested-users', this.suggestedUserList.draw());

      return this.IWidget.prototype.draw.call(
        this,
        Widget.block([this.titleDiv, this.suggestedUserListDiv])
      );
    },
  },

  /**
   * Constructor for SuggestedUsersPanel
   */
  function(
    onlyOne,
    includeFavourites,
    suggestedUsers,
    selectionObserver,
    showToggleFavouriteButtons
  ) {
    this.IWidget();

    this.onlyOne = any(onlyOne, false);

    includeFavourites = any(includeFavourites, true);

    this.suggestedUserList = new FoundPeopleList(
      null,
      this.onlyOne,
      selectionObserver,
      showToggleFavouriteButtons
    );

    var self = this;

    if (exists(suggestedUsers)) {
      each(suggestedUsers, function(user) {
        if (any(user._type, null) === 'Avatar') {
          self.suggestedUserList.set('existingAv' + user.id, $O(user));
        } else {
          self.suggestedUserList.set(user.id, $O(user));
        }
      });
    }

    if (includeFavourites) {
      var favorites = _.values(Indico.User.favorite_users).sort(function(a, b) {
        var name1 = '{0} {1}'.format(a.firstName, a.familyName);
        var name2 = '{0} {1}'.format(b.firstName, b.familyName);
        return name1 == name2 ? 0 : name1 < name2 ? -1 : 1;
      });
      _.each(favorites, function(user) {
        if (!self.suggestedUserList.get('existingAv' + user.id)) {
          self.suggestedUserList.set('existingAv' + user.id, $O(user));
        }
      });
    }

    this.titleDiv = null;
    this.suggestedUserListDiv = null;
  }
);

/**
 * Creates a popup to add users in different ways (search in DB, choose from favourites, add a new one...).
 *
 * @param {String} title The dialog title
 *
 * @param {Boolean} allowSearch If this is true, a search user panel will appear.
 * @param {Integer} conferenceId If allowSearch is true, and if this is different from null, authors from that conference will be included in the search results.
 * @param {Boolean} enableGroups If allowSearch is true, and if this is true, groups will be available for search and add.
 *
 * @param {Boolean} includeFavourites If True, favourites will appear in the list of suggested users of the ChooseUsersPopup.
 * @param {WatchObject} suggestedUsers An array or WatchList of users that will be used as list of suggested users. The users
 *                                     should be fossils / dictionaries with the user data.
 *                                     If null, there will be no suggestedUsers panel.
 *
 * @param {Boolean} onlyOne If true, only 1 user will be able to be chosen in the dialog..
 *
 * @param {Function} process A function that will be called when new users (from new data, or from the search dialog, or from the suggested list) is added to the list.
 *                           The function will be passed a list of WatchObjects representing the users, even when onlyOne is true.
 */
type(
  'ChooseUsersPopup',
  ['ExclusivePopupWithButtons', 'PreLoadHandler'],
  {
    _preload: [],

    __buildSearchPanel: function(container) {
      var self = this;
      if (this.enableGroups) {
        this.searchPanel = new UserAndGroupsSearchPanel(
          this.onlyOne,
          function(selectedList) {
            self.__selectionObserver('searchUsers', selectedList);
          },
          this.conferenceId,
          this.showToggleFavouriteButtons,
          function(avatar, action) {
            self.__searchPanelFavouriteButtonObserver(avatar, action);
          },
          this.submissionRights,
          this.grantRights,
          self.extraDiv,
          self.allowExternal
        );
      } else {
        this.searchPanel = new UserSearchPanel(
          this.onlyOne,
          function(selectedList) {
            self.__selectionObserver('searchUsers', selectedList);
          },
          this.conferenceId,
          this.showToggleFavouriteButtons,
          function(avatar, action) {
            self.__searchPanelFavouriteButtonObserver(avatar, action);
          },
          this.submissionRights,
          this.grantRights,
          self.extraDiv,
          self.allowExternal
        );
      }
      var returnedDom = this.searchPanel.draw();
      container.append(returnedDom);
    },

    __buildSuggestedUsersPanel: function(container) {
      var self = this;

      this.suggestedUsersPanel = new SuggestedUsersPanel(
        this.onlyOne,
        this.includeFavourites,
        this.suggestedUsers,
        function(selectedList) {
          self.__selectionObserver('suggestedUsers', selectedList);
        },
        this.showToggleFavouriteButtons
      );

      container.append(this.suggestedUsersPanel.draw());
    },

    __selectionObserver: function(panel, selectedList) {
      if (this.onlyOne) {
        if (panel === 'searchUsers') {
          this.suggestedUsersPanel.clearSelection();
        } else {
          this.searchPanel.clearSelection();
        }
        if (selectedList.isEmpty()) {
          this._toggleSaveButton(false);
        } else {
          this._toggleSaveButton(true);
        }
      } else {
        if (!selectedList.isEmpty()) {
          this._toggleSaveButton(true);
        } else {
          var twoPanels =
            this.allowSearch && (this.includeFavourites || exists(this.suggestedUsers));
          if (twoPanels) {
            var otherSelectedList =
              panel === 'searchUsers'
                ? this.suggestedUsersPanel.getSelectedList()
                : this.searchPanel.getSelectedList();
            if (otherSelectedList.isEmpty()) {
              this._toggleSaveButton(false);
            } else {
              this._toggleSaveButton(true);
            }
          } else {
            this._toggleSaveButton(false);
          }
        }
      }
    },

    __save: function() {
      var self = this;
      var totalSelected = $O({});
      if (this.allowSearch) {
        totalSelected.update(this.searchPanel.getSelectedList().getAll());
      }
      if (this.includeFavourites || exists(this.suggestedUsers)) {
        totalSelected.update(this.suggestedUsersPanel.getSelectedList().getAll());
      }
      var returnedList = new List();
      each(totalSelected, function(selectedItem) {
        if (self.submissionRights) {
          selectedItem.set('isSubmitter', self.grantRights.get('submission'));
        }
        returnedList.append(selectedItem.getAll());
      });
      this.chooseProcess(returnedList.allItems());
    },

    __searchPanelFavouriteButtonObserver: function(avatar, addedOrRemoved) {
      if (addedOrRemoved) {
        this.suggestedUsersPanel.suggestedUserList.set('existingAv' + avatar.id, $O(avatar));
      }
    },

    _toggleSaveButton: function(enabled) {
      this.saveButton.disabledButtonWithTooltip(enabled ? 'enable' : 'disable');
    },

    /**
     * Returns the dialog's DOM
     */
    draw: function() {
      var self = this;

      this.saveButton = self.buttons.eq(0);
      this.saveButton.disabledButtonWithTooltip({
        tooltip: $T('Please select at least one item'),
        disabled: true,
      });

      var mainContent = Html.tr();
      if (self.allowSearch) {
        this.cellSearch = Html.td('searchUsersGroupsPanel');
        self.__buildSearchPanel(this.cellSearch);
        mainContent.append(this.cellSearch);
      }

      if (this.includeFavourites || exists(this.suggestedUsers)) {
        this.cellSuggested = Html.td('suggestedUsersPanel');
        self.__buildSuggestedUsersPanel(this.cellSuggested);
        mainContent.append(this.cellSuggested);
      }

      mainContent = Html.table(
        {cellpadding: 0, cellPadding: 0, cellspacing: 0, cellSpacing: 0},
        Html.tbody({}, mainContent)
      );
      return this.ExclusivePopupWithButtons.prototype.draw.call(this, mainContent, {
        padding: '0px',
      });
    },

    _getButtons: function() {
      var self = this;
      return [
        [
          self.onlyOne ? $T('Choose') : $T('Add'),
          function() {
            self.__save();
            self.close();
          },
        ],
        [
          $T('Cancel'),
          function() {
            self.close();
          },
        ],
      ];
    },

    postDraw: function() {
      // We have to do this first and not after the super call or it won't work in IE7
      if (this.allowSearch && this.enableGroups) {
        var tabContainer = this.searchPanel.tabWidget.canvas;
        tabContainer.height(tabContainer.height());
      }
      if (this.includeFavourites || exists(this.suggestedUsers)) {
        this.suggestedUsersPanel.suggestedUserListDiv.setStyle(
          'height',
          pixels(
            this.cellSuggested.dom.offsetHeight -
              this.suggestedUsersPanel.titleDiv.dom.offsetHeight -
              10
          )
        );
      }
      this.ExclusivePopupWithButtons.prototype.postDraw.call(this);
      if (this.allowSearch) $E('userSearchFocusField').dom.focus();
    },
  },
  /**
   * Constructor for ChooseUsersPopup
   */
  function(
    title,
    allowSearch,
    conferenceId,
    enableGroups,
    includeFavourites,
    suggestedUsers,
    onlyOne,
    showToggleFavouriteButtons,
    submissionRights,
    chooseProcess,
    extraDiv,
    allowExternal
  ) {
    var self = this;

    this.allowSearch = allowSearch;
    this.conferenceId = conferenceId;
    this.enableGroups = enableGroups;

    this.includeFavourites = any(includeFavourites, true);
    this.suggestedUsers = suggestedUsers;

    this.onlyOne = any(onlyOne, false);
    this.showToggleFavouriteButtons = any(showToggleFavouriteButtons, true);
    this.chooseProcess = chooseProcess;
    this.extraDiv = extraDiv;

    // Other attributes that will be set in other methods, listed here for reference
    this.saveButton = null;
    this.suggestedUsersPanel = null;
    this.searchPanel = null;
    this.buttonDiv = null;
    this.cellSearch = null;
    this.cellSuggested = null;

    this.submissionRights = submissionRights;
    this.grantRights = new WatchObject();

    // We build the dialog.
    this.PreLoadHandler(self._preload, function() {
      self.open();
    });
    this.ExclusivePopupWithButtons(title, positive);
    this.allowExternal = _.isBoolean(allowExternal) ? allowExternal : true;
  }
);

/**
 * Creates a data creation / edit pop-up dialog.
 * @param {String} title The title of the popup.
 * @param {Object} userData A WatchObject that has to have the following keys/attributes:
 *                          id, title, familyName, firstName, affiliation, email, address, telephone, submission.
 *                          Its information will be displayed as initial values in the dialog.
 * @param {Function} action A callback function that will be called if the user presses ok. The function will be passed
 *                          a WatchObject with the new values.
 */
type(
  'UserDataPopup',
  ['ExclusivePopupWithButtons'],
  {
    draw: function() {
      var userData = this.userData;
      var self = this;
      self.parameterManager = new IndicoUtil.parameterManager();

      var grantSubmission = [];
      var grantManagement = [];
      var grantCoordination = [];
      var warning = [];
      if (this.grantSubmission) {
        grantSubmission = [
          $T('Grant submission rights'),
          $B(Html.checkbox({id: 'submissionCheckbox'}), userData.accessor('submission')),
        ];
        warning = [
          Html.span(
            {},
            Html.span({style: {fontWeight: 'bold'}}, $T('Note:')),
            $T(' If this person does not already have an Indico account, '),
            Html.br(),
            $T('he or she will be sent an email asking to register as a user.'),
            Html.br(),
            $T(' After the registration the user will automatically be given'),
            Html.br(),
            $T(' submission rights.')
          ),
        ];
      }
      if (this.grantManagement) {
        grantManagement = [
          $T('Give management rights'),
          $B(Html.checkbox({}), userData.accessor('manager')),
        ];
        warning = [
          Html.span(
            {},
            Html.span({style: {fontWeight: 'bold'}}, $T('Note:')),
            $T(' If this person does not already have an Indico account, '),
            Html.br(),
            $T('he or she will be sent an email asking to create an account.'),
            Html.br(),
            $T(' After the account creation the user will automatically be'),
            Html.br(),
            $T(' given management rights.')
          ),
        ];
      }

      if (this.grantCoordination) {
        grantCoordination = [
          $T('Give coordination rights'),
          $B(Html.checkbox({}), userData.accessor('coordinator')),
        ];
        warning = [
          Html.span(
            {},
            Html.span({style: {fontWeight: 'bold'}}, $T('Note:')),
            $T(' If this person does not already have an Indico account, '),
            Html.br(),
            $T('he or she will be sent an email asking to create an account.'),
            Html.br(),
            $T(' After the account creation the user will automatically be'),
            Html.br(),
            $T(' given coordination rights.')
          ),
        ];
      }
      if (this.grantManagement && this.grantCoordination) {
        warning = [
          Html.span(
            {},
            Html.span({style: {fontWeight: 'bold'}}, $T('Note:')),
            $T(' If this person does not already have an Indico account, '),
            Html.br(),
            $T('he or she will be sent an email asking to create an account.'),
            Html.br(),
            $T(' After the account creation the user will automatically be'),
            Html.br(),
            $T(' given the rights.')
          ),
        ];
      }

      var form = IndicoUtil.createFormFromMap([
        [
          $T('Title'),
          $B(
            Html.select(
              {},
              Html.option({}, ''),
              Html.option({value: 'Mr'}, $T('Mr')),
              Html.option({value: 'Mrs'}, $T('Mrs')),
              Html.option({value: 'Ms'}, $T('Ms')),
              Html.option({value: 'Dr'}, $T('Dr')),
              Html.option({value: 'Prof.'}, $T('Prof.'))
            ),
            userData.accessor('title')
          ),
        ],
        [
          $T('Family Name'),
          $B(
            self.parameterManager.add(Html.edit({style: {width: '300px'}}), 'text', false),
            userData.accessor('familyName')
          ),
        ],
        [
          $T('First Name'),
          $B(Html.edit({style: {width: '300px'}}), userData.accessor('firstName')),
        ],
        [
          $T('Affiliation'),
          $B(Html.edit({style: {width: '300px'}}), userData.accessor('affiliation')),
        ],
        [
          $T('Email'),
          $B(
            self.parameterManager.add(
              Html.edit({id: 'email', style: {width: '300px'}}),
              'email',
              this.allowEmptyEmail
            ),
            userData.accessor('email')
          ),
        ],
        [$T('Address'), $B(Html.textarea({style: {width: '300px'}}), userData.accessor('address'))],
        [$T('Telephone'), $B(Html.edit({style: {width: '300px'}}), userData.accessor('phone'))],
        grantSubmission,
        grantManagement,
        grantCoordination,
        warning,
      ]);

      return this.ExclusivePopupWithButtons.prototype.draw.call(this, form);
    },

    _getButtons: function() {
      var self = this;
      return [
        [
          $T('Save'),
          function() {
            if ($('#submissionCheckbox').is(':checked') && $('#email').val() == 0) {
              var popup = new WarningPopup(
                $T('Warning'),
                $T(
                  'It is not possible to grant submission rights to a participant without an email address. Please set an email address.'
                )
              );
              popup.open();
              return;
            }
            self.userData.set(
              'name',
              '{0} {1}'.format(self.userData.get('firstName'), self.userData.get('familyName'))
            );
            if (self.parameterManager.check() && self.checkPerson(self.userData)) {
              self.action(self.userData);
              if (self.autoClose) {
                self.close();
              }
            }
          },
        ],
        [
          $T('Cancel'),
          function() {
            self.close();
          },
        ],
      ];
    },
  },
  function(
    title,
    userData,
    action,
    grantSubmission,
    grantManagement,
    grantCoordination,
    allowEmptyEmail,
    autoClose,
    checkPerson
  ) {
    this.userData = userData;
    this.action = action;
    this.grantSubmission = exists(grantSubmission) ? grantSubmission : false;
    this.grantManagement = exists(grantManagement) ? grantManagement : false;
    this.grantCoordination = exists(grantCoordination) ? grantCoordination : false;
    this.allowEmptyEmail = exists(allowEmptyEmail) ? allowEmptyEmail : true;
    this.ExclusivePopup(title, function() {
      return true;
    });
    this.autoClose = exists(autoClose) ? autoClose : true;
    this.checkPerson =
      checkPerson ||
      function() {
        return true;
      };
  }
);

$(function() {
  // This widget creates a clickable icon-shield that opens a qtip with the different rights that
  // can be granted to a user.
  $.widget('users.shield', {
    options: {
      userData: null,

      // rights
      submission: true,
    },

    _qtip: function(rights) {
      // qtip content

      var rightsTooltipText = $('

')
        .append(
          $('')
            .text($T('User privileges'))
            .css('fontSize', '13px')
        )
        .append($('

').css('marginTop', '8px'));
      $.each(rights, function(index, value) {
        rightsTooltipText.append(value.checkbox).append(
          $('')
            .text(value.text)
            .css('verticalAlign', 'middle')
        );
      });

      this.element.qtip({
        style: {
          classes: 'qtip-rounded qtip-shadow qtip-popup',
          tip: {
            corner: true,
            width: 15,
            height: 10,
          },
        },
        position: {
          my: 'top middle',
          at: 'bottom middle',
        },
        content: {
          text: rightsTooltipText,
        },
        show: {
          event: 'click',
          solo: true,
        },
        hide: {
          event: 'unfocus',
        },
      });
    },

    _create: function() {
      var self = this;

      this.element
        // add class and attrs
        .addClass('icon-shield user_list_icon_shield')
        .attr('ariaHidden', true)
        .attr('alt', $T('User rights'))
        .attr('title', $T('User rights'))
        .attr('data-id', 'author_' + this.options.userData.get('email'))
        //css
        .css('color', this.options.userData.get('isSubmitter') ? '#cc4646' : '#aaa');

      // Create privelege checkboxes and events
      var privelegeCheckboxes = [];

      if (this.options.submission) {
        var submissionCheckbox = $("<input type='checkbox'>")
          .prop('checked', !!this.options.userData.get('isSubmitter'))
          .css('marginRight', '8px')
          .css('verticalAlign', 'middle');

        submissionCheckbox.click(function() {
          $('.icon-shield[data-id="author_' + self.options.userData.get('email') + '"]').trigger(
            'participantProtChange',
            [{isSubmitter: submissionCheckbox.is(':checked')}]
          );
        });

        privelegeCheckboxes.push({checkbox: submissionCheckbox, text: $T('Submission rights')});
      }

      // Listen to protection changes
      this.element.on('participantProtChange', function(event, args) {
        if (self.options.submission && 'isSubmitter' in args) {
          self.element.css('color', args.isSubmitter ? '#cc4646' : '#aaa');
          self.options.userData.set('isSubmitter', args.isSubmitter);
          submissionCheckbox.prop('checked', args.isSubmitter);
        }
      });

      this._qtip(privelegeCheckboxes);
    },
  });
});

/**
 * Creates a list of users. Each user can be edited or removed.
 * It inherits from ListWidget who in turn inherits from WatchObject, so the usual WatchObject methods (get, set)
 * can be used on it. For example 'set' can be used to initialize the list.
 * This means that the users are stored with their id's as keys.
 * @param {String} style The class of the ul that will contain the users.
 * @param {Boolean} allowSetRights. If true, each user will have an edit button to change their rights.
 * @param {Boolean} allowEdit. If true, each user will have an edit button to change their data.
 * @param {Function} editProcess. A function that will be called when a user is edited. The function will
 *                                be passed the new data as a WatchObject.
 * @param {Boolean} showToggleFavouriteButtons. false by default. If true, favouritize buttons will not be shown.
 */
type(
  'UserListWidget',
  ['ListWidget'],
  {
    _iteratingElement: function(attrs, item) {
      var className;
      var pairProperties = attrs.pair.get();
      if (pairProperties.get('isGroup')) {
        className = 'item-group';
      } else if (pairProperties.get('_type') === 'IPNetworkGroup') {
        className = 'icon-lan';
      } else if (pairProperties.get('_type') === 'Email') {
        className = 'icon-mail';
      } else {
        className = 'icon-user';
      }
      attrs['className'] = className;
      delete attrs['pair'];
      return this.ListWidget.prototype._iteratingElement.call(this, attrs, item);
    },
    _drawItem: function(user) {
      var self = this;
      var userData = user.get();

      var edit_button = $('').click(function() {
        var editPopup = new UserDataPopup('Change user data', userData.clone(), function(newData) {
          //  editProcess will be passed a WatchObject representing the user.
          self.editProcess(userData, function(result) {
            if (result) {
              userData.update(newData.getAll());
              if (!startsWith('' + userData.get('id'), 'newUser')) {
                userData.set('id', 'edited' + userData.get('id'));
              }
              self.userListField.inform();
            }
          });
        });
        editPopup.open();
      });

      var remove_button = $('').click(function() {
        var currentUserId = $('body').data('user-id');
        var userId = userData.get('id');
        var confirmed;

        function setResult(result) {
          if (result) {
            self.set(user.key, null);
            self.userListField.inform();
          }
        }

        if (currentUserId === userId) {
          confirmed = confirmPrompt(
            $T.gettext('Are you sure you want to remove yourself from the list?'),
            $T.gettext('Confirm action')
          );
        } else {
          confirmed = $.Deferred().resolve();
        }

        confirmed.then(function() {
          self.removeProcess(userData, setResult);
        });
      });

      var buttonDiv = Html.div('actions', new Html(remove_button.get(0)));

      if (userData.get('isGroup') || userData.get('_fossil') === 'group') {
        var groupName = $B(Html.span('name'), userData.accessor('name'));
        return [Html.span('info', groupName), buttonDiv];
      } else {
        if (
          IndicoGlobalVars.isUserAuthenticated & this.showToggleFavouriteButtons &&
          userData.get('_type') === 'Avatar'
        ) {
          buttonDiv.append(new Html(create_favorite_button(userData.get('id')).get(0)));
        }
        if (this.allowSetRights) {
          buttonDiv.append(
            $('')
              .shield({userData: userData})
              .get(0)
          );
        }
        if (this.allowEdit) {
          buttonDiv.append(new Html(edit_button.get(0)));
        }
        buttonDiv.append(new Html(remove_button.get(0)));

        var userName;
        if (userData.get('_type') == 'Email') {
          userName = Html.span('info', $B(Html.span('name'), userData.accessor('email')));
        } else {
          userName = Html.span('info', $B(Html.span('name'), userData.accessor('name')));
        }

        if (userData.get('_type') !== 'Email') {
          userName.append(Html.span('email', userData.get('email')));
          userName.append(Html.span('affiliation', userData.get('affiliation')));
        }
        return [userName, buttonDiv];
      }
    },
  },

  function(
    style,
    allowSetRights,
    allowEdit,
    editProcess,
    removeProcess,
    showToggleFavouriteButtons,
    userListField
  ) {
    this.style = any(style, 'user-list');
    this.allowSetRights = allowSetRights;
    this.allowEdit = allowEdit;
    this.editProcess = any(editProcess, singleUserNothing);
    this.removeProcess = any(removeProcess, singleUserNothing);
    this.showToggleFavouriteButtons = any(showToggleFavouriteButtons, true);
    this.userListField = userListField;
    this.ListWidget(style);
  }
);

/**
 * Creates a form field with a list of users.
 * Users can be added from an initial list of users, from a 'new user' dialog, or from a "choose user"
 * dialog which will enable both to search users/groups and propose a list of suggested users (favourites and others).
 * The 'id' attribute of the users in the list will depend from their origin.
 * - If it was added from the search dialog, the id will be 'existingAvXX' (where XX corresponds to the avatar id).
 * - It it was added from the 'new user' dialog, the id will be 'newUserXX', where XX is auto-increment starting from 0.
 * - If it was added from the list of suggested users, it will retain the id of the user that was put in the list.
 * - It the user was edited, and the edited user corresponded to an Avatar, the id will be 'editedXX' where XX is the id of the Avatar used initially.
 *
 */
type(
  'UserListField',
  ['IWidget'],
  {
    clear: function() {
      this.userList.clearList();
    },

    privilegesOn: function() {
      return (
        $('#grant-manager').prop('checked') || $('#presenter-grant-submission').prop('checked')
      );
    },

    setUpParameters: function() {
      /* set up basic components : div containers for message , a checkbox list ,
           a list of messages and a warning counter.*/

      this.warning_no_indico = $T(
        'Please note that you have added a user that does not exist in Indico.\
                                    Non existing users will be asked via email to create an account so\
                                    that they will be able to use the privileges below.'
      );
      this.warning_no_email = $T(
        'Please note that you have added a user without an email address. Users without \
                                    email will not be able to use the privileges below.'
      );
      this.messageDiv = $('
', {
        css: {
          height: '58px',
          maxWidth: '420px',
          textAlign: 'left',
          width: 'auto',
          overflow: 'auto',
          marginLeft: '5px',
        },
      }).addClass('warningMessage');
      this.messageContainer = $('
', {css: {display: 'inline-block', position: 'absolute'}});
      this.containerDiv = $('
');
      this.warning_flag = false;
    },

    appendMessage: function(message) {
      this.messageDiv.html(message);
      this.messageContainer.append(this.messageDiv);
      this.containerDiv.append(this.messageContainer);
      this.warning_flag = true;
    },

    clearMessages: function() {
      this.messageContainer.html('');
      this.containerDiv.append(this.messageContainer);
      this.warning_flag = false;
    },

    checkList: function(list) {
      var self = this;
      self.clearMessages();
      each(list, function(val, key) {
        self.check(self.userList.get(key));
      });
    },

    check: function(user) {
      var self = this;

      if (keys(this.privileges).length > 0) {
        if (this.privilegesOn()) {
          if (!user.get('email')) {
            this.appendMessage(this.warning_no_email);
          } else {
            indicoRequest('search.users', {email: user.get('email')}, function(result, error) {
              if (!error) {
                if (result.length == 0) {
                  self.appendMessage(self.warning_no_indico);
                }
              }
            });
          }
        }
      }
    },

    inform: function() {
      if (keys(this.privileges).length > 0) {
        if (!this.userList.isEmpty()) {
          this.checkList(this.userList.getAll());
        } else {
          this.clearMessages();
        }
      }
    },

    draw: function() {
      var self = this;
      var buttonDiv = Html.div({style: {marginTop: pixels(10)}});

      if (this.allowSearch || this.includeFavourites || exists(this.suggestedUsers)) {
        var chooseUserButton = Html.input(
          'button',
          {style: {marginRight: pixels(5)}, className: 'i-button hide-if-locked', type: 'button'},
          this.enableGroups ? $T('Add User / Group') : $T('Add user')
        );
        var chooseIpNetwork = Html.a(
          {
            'href': '#',
            'style': {marginRight: pixels(5)},
            'className': 'i-button arrow js-dropdown',
            'data-toggle': 'dropdown',
          },
          $T('Add IP Network')
        );
        var ipNetworksContainer = Html.span({className: 'group'});
        var ipNetworksList = Html.ul({className: 'i-dropdown'});

        _.each(this.ipNetworks, function(network) {
          var li = Html.li();
          li.append(Html.a({href: '#'}, network.name));
          li.observeClick(function() {
            self.newProcess([network], function(result) {
              if (result && !self.userList.get(network.identifier)) {
                var entries = self.userList.getAll();
                entries[network.identifier] = $O(network);
                updatePrincipalsList(entries);
              }
            });
          });
          ipNetworksList.append(li);
        });

        ipNetworksContainer.append(chooseIpNetwork);
        ipNetworksContainer.append(ipNetworksList);
        var title;
        if (this.includeFavourites || exists(this.suggestedUsers)) {
          title = this.enableGroups ? $T('Add Users and Groups') : $T('Add Users');
        } else {
          title = this.enableGroups ? $T('Search Users and Groups') : $T('Search Users');
        }

        function updatePrincipalsList(entries) {
          var sortedKeys = _.sortBy(_.keys(entries), function(key) {
            var principal = entries[key].getAll();
            var weight =
              principal._type == 'Avatar'
                ? 0
                : principal._type == 'Email'
                ? 1
                : principal.isGroup
                ? 2
                : 3;
            var name = principal._type !== 'Email' ? principal.name : principal.email;
            return [weight, name.toLowerCase()];
          });

          self.userList.clearList();
          _.each(sortedKeys, function(key) {
            self.userList.set(key, $O(entries[key].getAll()));
          });
        }

        var peopleAddedHandler = function(peopleList) {
          // newProcess will be passed a list of WatchObjects representing the users.
          self.newProcess(peopleList, function(value, results) {
            if (value) {
              each(results, function(person) {
                var key;
                if (
                  person.isGroup ||
                  person._fossil === 'group' ||
                  person._type == 'IPNetworkGroup'
                ) {
                  key = person.identifier;
                } else if (person._type === 'Avatar') {
                  key = 'existingAv' + person.id;
                } else if (person._type === 'EventPerson') {
                  key = 'existingEventPerson' + person.id;
                } else {
                  key = person.id;
                }
                if (!self.userList.get(key)) {
                  var entries = self.userList.getAll();
                  entries[key] = $O(person);
                  updatePrincipalsList(entries);
                  $('.icon-shield[data-id="author_' + person.email + '"]').trigger(
                    'participantProtChange',
                    [{isSubmitter: person.isSubmitter}]
                  );
                }
              });
            }
          });
        };

        chooseUserButton.observeClick(function() {
          var chooseUsersPopup = new ChooseUsersPopup(
            title,
            self.allowSearch,
            self.conferenceId,
            self.enableGroups,
            self.includeFavourites,
            self.suggestedUsers,
            false,
            self.showToggleFavouriteButtons,
            self.allowSetRights,
            peopleAddedHandler,
            null,
            self.allowExternal
          );
          chooseUsersPopup.execute();
        });

        buttonDiv.append(chooseUserButton);
        if (this.enableIpNetworks) {
          buttonDiv.append(ipNetworksContainer);
        }
      }

      if (this.allowNew) {
        var addNewUserButton = Html.input(
          'button',
          {className: 'i-button', style: {marginRight: pixels(5)}},
          $T('Add New')
        );
        addNewUserButton.observeClick(function() {
          var newUserId = 'newUser' + self.newUserCounter++;
          var newUser = $O({id: newUserId});
          var newUserPopup = new UserDataPopup(
            $T('New user'),
            newUser,
            function(newData) {
              newUser.set('isSubmitter', newUser.get('submission'));
              self.newProcess([newUser], function(result) {
                if (result) {
                  self.userList.set(newUserId, newUser);
                  self.check(newUser);
                  $('.icon-shield[data-id="author_' + newUser.get('email') + '"]').trigger(
                    'participantProtChange',
                    [{isSubmitter: newUser.get('isSubmitter') || false}]
                  );
                }
              });
            },
            self.allowSetRights
          );
          newUserPopup.open();
        });
        buttonDiv.append(addNewUserButton);
      }

      // User privileges (submission privilege, etc.)
      var privilegesDiv = $('').css('marginTop', '10px');
      var keysList = keys(this.privileges);
      if (keysList.length > 0) {
        privilegesDiv.append($('').html($T('Grant all these users with privileges: ')));
      }
      var comma = ', ';
      for (var i = 0; i < keysList.length; i++) {
        if (i + 1 == keysList.length) {
          comma = '';
        }
        var key = keysList[i];
        var value = this.privileges[key];
        var checkbox = $('<input>', {type: 'checkbox', id: key, name: key}).change(function() {
          if (!self.privilegesOn()) {
            self.clearMessages();
            return;
          } else {
            if (!self.warning_flag) {
              self.inform();
            }
          }
        });
        $B(this.selectedPrivileges.accessor(key), checkbox);
        privilegesDiv.append(
          $('')
            .append(checkbox)
            .append(value[0] + comma)
        );
      }

      var containsUserListDiv = $('
').css('display', 'inline-block');
      containsUserListDiv.append(
        $('
')
          .addClass(this.userDivStyle)
          .html(this.userList.draw().dom)
      );
      this.containerDiv.append(containsUserListDiv);
      return Widget.block([this.containerDiv.get(0), privilegesDiv.get(0), buttonDiv]);
    },
  },
  /*
   * @param {String} userDivStyle A CSS class for the div that will sourround the user list.
   * @param {String} userListStyle A CSS class for the user list. It will be passed to the inner UserListWidget.
   *
   * @param {list} initialUsers A list of (fossilized) avatars that will appear initially.
   * @param {Boolean} includeFavourites If True, favourites will appear in the list of suggested users of the ChooseUsersPopup.
   * @param {list} suggestedUsers A list of users that will be offered as options to be added.
   * @param {Boolean} allowSearch If True, the "Choose user" dialog will propose to search.
   * @param {Boolean} enableGroups If True, the "Choose user" dialog will propose to search groups.
   * @param {string} conferenceId for author list search
   * @param {list} privileges dictionary with the privileges that we can set for the users. There is a key and a tuple as vale: (label, default value for checked). E.g. {"grant-manager": ["event modification", false]}
   * @param {Boolean} allowNew If True, a 'New User' button will be present.
   * @param {Boolean} allowSetRights. If True, rights for each user will be able to be edited.
   * @param {Boolean} allowEdit If True, users in the list will be able to be edited.
   * @param {Boolean} showToggleFavouriteButtons. false by default. If true, favouritize buttons will not be shown.
   * @param {Function} newProcess A function that will be called when new users (from new data, or from the search dialog, or from the suggested list) is added to the list.
   * @param {Function} editProcess A function that will be called when a user is edited.
   * @param {Function} removeProcess A function that will be called when a user is removed.
   */
  function(
    userDivStyle,
    userListStyle,
    initialUsers,
    includeFavourites,
    suggestedUsers,
    allowSearch,
    enableGroups,
    conferenceId,
    privileges,
    allowNew,
    allowSetRights,
    allowEdit,
    showToggleFavouriteButtons,
    newProcess,
    editProcess,
    removeProcess,
    allowExternal,
    enableIpNetworks,
    ipNetworks
  ) {
    var self = this;
    this.userList = new UserListWidget(
      userListStyle,
      allowSetRights,
      allowEdit,
      editProcess,
      removeProcess,
      showToggleFavouriteButtons,
      this
    );
    self.newUserCounter = 0;
    this.userDivStyle = any(userDivStyle, 'user-list');
    this.setUpParameters();

    if (exists(initialUsers)) {
      each(initialUsers, function(user) {
        if (any(user._type, null) === 'Avatar') {
          self.userList.set('existingAv' + user.id, $O(user));
        } else if (user._type === 'EventPerson') {
          self.userList.set('existingEventPerson' + user.id, $O(user));
        } else if (
          ~[
            'LDAPGroupWrapper',
            'LocalGroupWrapper',
            'MultipassGroup',
            'LocalGroup',
            'IPNetworkGroup',
          ].indexOf(user._type)
        ) {
          self.userList.set(user.identifier, $O(user));
        } else {
          self.userList.set(user.id, $O(user));
        }
      });
    }

    this.includeFavourites = any(includeFavourites, true);
    if (exists(suggestedUsers)) {
      if (suggestedUsers.WatchList) {
        this.suggestedUsers = suggestedUsers;
      } else {
        this.suggestedUsers = new WatchList();
        each(suggestedUsers, function(user) {
          self.suggestedUsers.append(user);
        });
      }
    } else {
      this.suggestedUsers = null;
    }

    this.allowSearch = any(allowSearch, true);
    this.enableGroups = any(enableGroups, false);
    this.enableIpNetworks = any(enableIpNetworks, false);
    this.ipNetworks = any(ipNetworks, []);
    this.conferenceId = any(conferenceId, null);
    this.privileges = any(privileges, {});
    this.selectedPrivileges = new WatchObject();
    this.allowSetRights = allowSetRights;

    this.allowNew = any(allowNew, true);
    this.showToggleFavouriteButtons = any(showToggleFavouriteButtons, true);
    this.newProcess = any(newProcess, userListNothing);
    this.allowExternal = _.isBoolean(allowExternal) ? allowExternal : true;
  }
);

redpajama_set_name: RedPajamaGithub


text:
QuirkLogic's Quilla is a massive 42-inch E Ink whiteboard
The device automatically save your doodles in the cloud.
D. Cooper|01.04.17
@danielwcooper
D. Cooper
In this article: CES2017, E-Ink, EInk, EInkPearl, gadgetry, gadgets, gear, Quilla, QuirkLogic, QuirkLogicQuilla
Canada's QuirkLogic wants to see an end to people taking pictures of their whiteboard notes with a smartphone. That's why it's launching the Quilla, a 42-inch smart whiteboard that's built around the world's largest electronic-paper display. Paired with QuirkLogic's connected-writer system, anything you draw on the display with the stylus will be instantly stored for retrieval later.
The product is the result of a three-year partnership between QuirkLogic and E Ink, the company that's been pushing electronic paper displays for years. The same technology will also be available from E Ink itself as a stand-alone display that can be used by the signage industry where low-power screens are useful.
The Quilla weighs 22 pounds and has a built-in battery that enables it to keep working for 16 hours on a charge. If you're sufficiently baller, you can even daisy-chain displays together to increase your workspace rather than simply moving to another page.
There's no word on pricing, although you can expect it to be significantly more expensive than, you know, just buying a whiteboard and writing things down afterward.
Click here to catch up on the latest news from CES 2017.


Daniel Buld (ex-Paradox, Hatred) is the new member of Septagon!
Daniel Buld (ex-Paradox, Hatred) is the new member of German speed/thrash metal band Septagon!
Some of you guys probably already know that Gen decided to leave Septagon. There was no bad blood, no musical or personal differences, just a time problem. We're still best buddies and want to thank Gen for all the effort he put into our baby and of course for the friendship that lasts! It really wasn't an easy task to find a drummer who is able to step in Gen's footsteps, but we're now really happy to announce that Daniel Buld (ex-Paradox, Hatred) is the new member in Septagon. He is not only a great drummer, but also a nice and funny guy and as Mr. Becker mentioned, "he is even long-haired". Can't wait to share the stage with him! Please welcome Daniel!
Not only a new Job was incoming…. I also have a new band! I am very happy to be a part of mighty Septagon!


After 26 years, Roger Fowler finally has hope.
The retired General Electric worker, who believes his cancer was caused by the two decades he spent working among toxic chemicals in the company's Peterborough plant, received a call late last week saying his WSIB claim is being reviewed.
But the roller-coaster ride he's been on since the 1990s – having his claim turned down, being rejected again on appeal, then informed he would get a review, only to be told no because of the previous rejection – means the 71-year-old is still wary.
Fowler, a colorectal cancer survivor who worked at the plant for 22 years, is among hundreds of former employees between 1945 and 2000 who say their cancer or serious illness was a result of exposure to thousands of chemicals at the site.
Their compensation claims were earlier dismissed by the Workplace Safety and Insurance Board.
But in recent months, new research has found they were exposed to the toxic substances at unsafe levels – and about 40 of those chemicals have been linked to cancer – so the board decided to reconsider some 250 cases that had previously been denied.
The WSIB has since looked at 16 of those cases, and so far 10 of them have been overturned, with six denials upheld. The rest of the 250 claimants have been told to expect letters in the mail this week as the reviews get underway.
"It's taken a while to get to this point, but I think we're doing it the right way because we are now going to see an orderly progression," Labour Minister Kevin Flynn said.
In the past, he added, "there were no claims being dealt with … when you look at how far this dates back, now that the WSIB is starting to move through the process in an orderly way … if they can keep this pace" all should be dealt with by the end of the year or early spring.
Last week, Fowler was part of a group who came to Queen's Park to say the Ministry of Labour was dragging its feet and failing to make good on promises, such as $2 million in funding for a specialized team based in Peterborough to help workers with their WSIB claims.
In 2016, the Star chronicled the "lethal legacy" of the Peterborough plant and, earlier this year, a study by the Unifor union found an "epidemic" of workplace illnesses for those who worked at the plant up until 2000, when it was decontaminated.
The WSIB says that since 1993, about 80 per cent of some 2,400 GE claims were allowed, though critics have said the number of approved cancer claims is just one-quarter. Given the new findings, the WSIB is now looking at more than 250 rejected claims and urging workers who haven't yet put in a claim to come forward.
The WSIB says it has received more than 60 calls in the past month, and 30 new claims are now being processed.
The additional cases that have come forward may be reviewed under a different, expedited process, Flynn has said, and advocates have been urging automatic approval.
Last Thursday during question period, New Democrat MPP Cindy Forster (Welland) asked why the workers and their families haven't yet received compensation. She noted that while the WSIB has been rejecting claims, it has continued to give businesses a cut in premiums.
She said the government needs to act much faster.
"What they shouldn't do is keep stringing them along," she added.


Search for the best flight prices from East Midlands Airport to Tunisia. Compare prices, book today and save money with East Midlands Airport.
Although not as well-known as its fellow North African nation of Egypt, Tunisia is a hidden gem with a rich history, iconic architecture and much more to offer visiting tourists. The northeast region is most popular with visitors and it's easy to see why – along the northeast coast there's a series of spectacular resort cities including Hammamet, Sousse and Monastir. From any of these fantastic destinations, you can enjoy views across the Mediterranean Sea and exotic white-sand beaches.
The climate is desirable the whole year round as Tunisia experiences the best of both the Mediterranean and African weather – so you can expect warm days with blue skies and cool, star-lit nights. There's plenty to keep the family entertained as well, including ancient Roman ruins, sprawling markets and even a few iconic locations from your favourite films. If you're on the lookout for a holiday unlike any other Tunisia is the one for you.
Tunisia's northeast coastline boasts every type of accommodation, catering to almost any budget. If you want the very best experience, you should look out for resorts with private Mediterranean beaches offering a range of luxury services, like La Badire in Hammamet or the JAZ Tour Khalef Thalasso & Spa in Sousse. Thrifty travellers can grab a bargain too by looking for less popular resorts which provide great service at a much lower price.
The labyrinthine markets of Tunisia have plenty to offer visitors hoping to indulge in retail therapy. If you're looking to pick up a bargain, you can find just about anything at any of the Medinas (or markets) found in most Tunisian cities. Alternatively, there's plenty of places to enjoy a more western shopping experience including the Soula Center in Sousse and the Costa Market in Hammamet. No matter where you go shopping, be prepared to barter and haggle as it's a common practice throughout the country.
Although you might be unfamiliar with Tunisian cuisine, there's a wealth of foods to try during your visit to the country. One of the country's most popular dishes is Brik, a type of thin pasty with a variety of fillings and often served at lunch. Couscous is a must as Tunisian couscous is famous for its blend of unique flavours. However, if you have more of a sweet tooth you should also try khobz mella which is a prized sweet bread that takes several hours to make.
We currently offer regular flights to and from Enfidha-Hammamet International Airport which are operated by TUI. These flights take around 2 hours 25 minutes. From Enfidha-Hammamet International Airport you can easily explore all the northeast coast of Tunisia, with the option to also head further into the countryside if you wish.
While originally a festival dedicated to celebrating the history of Carthage, the International Festival Of Carthage has become a more traditional music festival – attracting well-known acts like Shaggy and One Republic in recent years. If you're planning a visit in late July it's worth checking the main acts as you may find some of your favourite musicians playing for far less than they would in the UK.
Similar to Black Friday, the Summer Discounts are a period from August 1st to September 15th when stores slash prices with the hope of selling their wares to tourists and visitors shopping shortly after the Eid holiday. If you're a bit of a shopaholic this is a perfect opportunity to go wild.
Although not on the scale of the Coliseum in Rome, the El Jem Amphitheatre is one of the few remaining amphitheatres that has stayed relatively intact since the Roman era. The remnants are exciting to explore as there are thousands of small details left from the amphitheatre's ancient occupants. Film buffs will also notice that the El Jem Amphitheatre has appeared in several movies including Monty Python's Life of Brian.
One of the largest museums in Africa, the Bardo National Museum has an incredible collection of artefacts from all over the Mediterranean. Security has been tightened greatly in response to the 2015 terrorist attack, making the museum one of the safest attractions in Tunisia. This is a fantastic day out for families and history buffs alike.