[gnome-initial-setup/wip/pwithnall/misc-fixes: 47/70] site: add page to track site-specific details for deployments




commit 32ec9d456c0eb1f6d9048ba86624dff17dbb0b95
Author: Travis Reitter <travis reitter endlessm com>
Date:   Tue Apr 17 13:05:44 2018 -0700

    site: add page to track site-specific details for deployments
    
    These details will be included in metrics messages.
    
    The ID field is intentionally editable; this is a requirement for
    Conafe.
    
    See README.md file for more details.
    
    (Rebase 3.38: Drop mention of demo support in the prepare() vfunc.)
    
    https://phabricator.endlessm.com/T18842

 gnome-initial-setup/gnome-initial-setup.c          |   2 +
 gnome-initial-setup/pages/meson.build              |   1 +
 gnome-initial-setup/pages/site/README.md           |  14 +
 .../pages/site/deployment-sites.json.example       |  34 ++
 gnome-initial-setup/pages/site/eos-write-location  |  16 +
 gnome-initial-setup/pages/site/gis-site-page.c     | 469 +++++++++++++++++++++
 gnome-initial-setup/pages/site/gis-site-page.h     |  62 +++
 gnome-initial-setup/pages/site/gis-site-page.ui    | 279 ++++++++++++
 .../pages/site/gis-site-search-entry.c             | 452 ++++++++++++++++++++
 .../pages/site/gis-site-search-entry.h             |  44 ++
 gnome-initial-setup/pages/site/gis-site.c          | 289 +++++++++++++
 gnome-initial-setup/pages/site/gis-site.h          |  47 +++
 gnome-initial-setup/pages/site/meson.build         |  20 +
 gnome-initial-setup/pages/site/site.gresource.xml  |   6 +
 po/POTFILES.in                                     |   2 +
 15 files changed, 1737 insertions(+)
---
diff --git a/gnome-initial-setup/gnome-initial-setup.c b/gnome-initial-setup/gnome-initial-setup.c
index b4f4008f..bfe721ea 100644
--- a/gnome-initial-setup/gnome-initial-setup.c
+++ b/gnome-initial-setup/gnome-initial-setup.c
@@ -46,6 +46,7 @@
 #include "pages/account/gis-account-pages.h"
 #include "pages/parental-controls/gis-parental-controls-page.h"
 #include "pages/password/gis-password-page.h"
+#include "pages/site/gis-site-page.h"
 #include "pages/summary/gis-summary-page.h"
 
 #define VENDOR_PAGES_GROUP "pages"
@@ -85,6 +86,7 @@ static PageData page_table[] = {
   PAGE (parental_controls, TRUE),
   PAGE (parent_password, TRUE),
 #endif
+  PAGE (site, TRUE),
   PAGE (summary,  FALSE),
   { NULL },
 };
diff --git a/gnome-initial-setup/pages/meson.build b/gnome-initial-setup/pages/meson.build
index 0898d2ac..0f916488 100644
--- a/gnome-initial-setup/pages/meson.build
+++ b/gnome-initial-setup/pages/meson.build
@@ -10,6 +10,7 @@ pages = [
    'privacy',
    'goa',
    'password',
+   'site',
    'summary',
    'welcome',
 ]
diff --git a/gnome-initial-setup/pages/site/README.md b/gnome-initial-setup/pages/site/README.md
new file mode 100644
index 00000000..acf3e6e1
--- /dev/null
+++ b/gnome-initial-setup/pages/site/README.md
@@ -0,0 +1,14 @@
+Summary
+=======
+This page allows the user to search for a site among the ones in the global JSON
+file or enter their own.
+
+The resulting details will be stored globally for the metrics daemon to include
+in reports for analyzing specific deployments of computers.
+
+See `eos-write-location` and `gis-site-page.c` for details on the locations of
+these files on the system.
+
+See the `deployment-sites.json.example` file for the syntax. Note that this page
+will not be displayed in the first boot experience if a file matching the
+expected path does not exist or fails to be parsed correctly.
diff --git a/gnome-initial-setup/pages/site/deployment-sites.json.example 
b/gnome-initial-setup/pages/site/deployment-sites.json.example
new file mode 100644
index 00000000..4c7d0a9e
--- /dev/null
+++ b/gnome-initial-setup/pages/site/deployment-sites.json.example
@@ -0,0 +1,34 @@
+[
+    {
+        "id": "00112233",
+        "facility": "Example facility A",
+        "locality": "Bogotá",
+        "region": "DC",
+        "country": "Colombia"
+    },
+    {
+        "id": "other_facility",
+        "facility": "Other Facility",
+        "street": "123 Easy Street",
+        "locality": "San Jose",
+        "region": "CA",
+        "country": "USA"
+    },
+    {
+        "id": "18a9e72bc",
+        "facility": "Yet another Facility",
+        "locality": "San José",
+        "region": "San José",
+        "country": "Costa Rica"
+    },
+    {
+        "facility": "Facility with no ID or country",
+        "locality": "Manaus",
+        "region": "Amazonas"
+    },
+    {
+        "facility": "Only facility, street address, locality",
+        "street": "456 Main Street",
+        "locality": "Boston"
+    }
+]
diff --git a/gnome-initial-setup/pages/site/eos-write-location 
b/gnome-initial-setup/pages/site/eos-write-location
new file mode 100755
index 00000000..70975a57
--- /dev/null
+++ b/gnome-initial-setup/pages/site/eos-write-location
@@ -0,0 +1,16 @@
+#!/bin/bash -e
+
+LOCATION_FILENAME=/etc/metrics/location.conf
+LOCATION_DIRNAME=$(dirname $LOCATION_FILENAME)
+
+src_location_filename=$1
+
+if [ "$#" -ne 1 ]; then
+    echo "usage: $0 SRC_FILENAME" >&2
+    exit 1
+ fi
+
+mkdir -p $LOCATION_DIRNAME
+mv $src_location_filename $LOCATION_FILENAME
+chmod 644 $LOCATION_FILENAME
+chown metrics:metrics $LOCATION_FILENAME
diff --git a/gnome-initial-setup/pages/site/gis-site-page.c b/gnome-initial-setup/pages/site/gis-site-page.c
new file mode 100644
index 00000000..2d0ec228
--- /dev/null
+++ b/gnome-initial-setup/pages/site/gis-site-page.c
@@ -0,0 +1,469 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+/*
+ * Copyright (C) 2012 Red Hat
+ * Copyright (C) 2018 Endless Mobile, Inc.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * Written by:
+ *     Jasper St. Pierre <jstpierre mecheye net>
+ *     Travis Reitter <travis reitter endlessm com>
+ */
+
+/* Site page {{{1 */
+
+#define PAGE_ID GIS_SITE_PAGE_ID
+
+#include "config.h"
+#include "gis-site.h"
+#include "gis-site-page.h"
+#include "gis-site-search-entry.h"
+#include "site-resources.h"
+
+#include <glib/gi18n.h>
+#include <glib/gstdio.h>
+#include <gio/gio.h>
+
+#include <json-glib/json-glib.h>
+
+#include <errno.h>
+#include <stdlib.h>
+#include <string.h>
+
+#define LOCATION_FILENAME_TEMPL "gnome-initial-setup-location-XXXXXX.conf"
+
+struct _GisSitePagePrivate
+{
+  GtkWidget *search_entry;
+  GtkWidget *manual_check;
+  GtkWidget *id_entry;
+  GtkWidget *facility_entry;
+  GtkWidget *street_entry;
+  GtkWidget *locality_entry;
+  GtkWidget *region_entry;
+  GtkWidget *country_entry;
+};
+typedef struct _GisSitePagePrivate GisSitePagePrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (GisSitePage, gis_site_page, GIS_TYPE_PAGE)
+
+static void
+set_site (GisSitePage  *page,
+          GisSite      *site)
+{
+  GisSitePagePrivate *priv = gis_site_page_get_instance_private (page);
+  const gchar *id = NULL;
+  const gchar *facility = NULL;
+  const gchar *street = NULL;
+  const gchar *locality = NULL;
+  const gchar *region = NULL;
+  const gchar *country = NULL;
+
+  if (site)
+    {
+      id = gis_site_get_id (site);
+      facility = gis_site_get_facility (site);
+      street = gis_site_get_street (site);
+      locality = gis_site_get_locality (site);
+      region = gis_site_get_region (site);
+      country = gis_site_get_country (site);
+    }
+
+  gtk_entry_set_text (GTK_ENTRY (priv->id_entry), id ? id : "");
+  gtk_entry_set_text (GTK_ENTRY (priv->facility_entry),
+                      facility ? facility : "");
+  gtk_entry_set_text (GTK_ENTRY (priv->street_entry), street ? street : "");
+  gtk_entry_set_text (GTK_ENTRY (priv->locality_entry),
+                      locality ? locality : "");
+  gtk_entry_set_text (GTK_ENTRY (priv->region_entry), region ? region : "");
+  gtk_entry_set_text (GTK_ENTRY (priv->country_entry), country ? country : "");
+}
+
+static gboolean
+page_validate (GisSitePage *page)
+{
+  GisSitePagePrivate *priv = gis_site_page_get_instance_private (page);
+
+  if (gtk_entry_get_text_length (GTK_ENTRY (priv->id_entry)) ||
+      gtk_entry_get_text_length (GTK_ENTRY (priv->facility_entry)) ||
+      gtk_entry_get_text_length (GTK_ENTRY (priv->street_entry)) ||
+      gtk_entry_get_text_length (GTK_ENTRY (priv->locality_entry)) ||
+      gtk_entry_get_text_length (GTK_ENTRY (priv->region_entry)) ||
+      gtk_entry_get_text_length (GTK_ENTRY (priv->country_entry)))
+    {
+      return TRUE;
+    }
+
+  return FALSE;
+}
+
+static void
+update_page_validation (GObject *object, GParamSpec *param, GisSitePage *page)
+{
+  gis_page_set_complete (GIS_PAGE (page), page_validate (page));
+}
+
+static GisSite*
+get_site_from_widgets (GisSitePage *page)
+{
+  GisSitePagePrivate *priv = gis_site_page_get_instance_private (page);
+  const gchar *id = gtk_entry_get_text (GTK_ENTRY (priv->id_entry));
+  const gchar *facility = gtk_entry_get_text (GTK_ENTRY (priv->facility_entry));
+  const gchar *street = gtk_entry_get_text (GTK_ENTRY (priv->street_entry));
+  const gchar *locality = gtk_entry_get_text (GTK_ENTRY (priv->locality_entry));
+  const gchar *region = gtk_entry_get_text (GTK_ENTRY (priv->region_entry));
+  const gchar *country = gtk_entry_get_text (GTK_ENTRY (priv->country_entry));
+
+  return gis_site_new (id, facility, street, locality, region, country);
+}
+
+static void
+ini_string_append_safe (GString     *string,
+                        const gchar *property,
+                        const gchar *value)
+{
+  g_string_append_printf (string, "%s = %s\n", property, value ? value : "");
+}
+
+static gchar*
+ini_file_content_from_site (GisSite *site)
+{
+  GString *builder = g_string_new ("[Label]\n");
+
+  /* Note the field names aren't 1:1 between GisSite and the file format */
+  ini_string_append_safe (builder, "id", gis_site_get_id (site));
+  ini_string_append_safe (builder, "facility", gis_site_get_facility (site));
+  ini_string_append_safe (builder, "street", gis_site_get_street (site));
+  ini_string_append_safe (builder, "city", gis_site_get_locality (site));
+  ini_string_append_safe (builder, "state", gis_site_get_region (site));
+  ini_string_append_safe (builder, "country", gis_site_get_country (site));
+
+  return g_string_free (builder, FALSE);
+}
+
+static gboolean
+gis_site_page_save_data (GisPage  *gis_page,
+                         GError  **error)
+{
+  GisSitePage *page = GIS_SITE_PAGE (gis_page);
+  g_autofree gchar *location_file_content = NULL;
+  g_autofree gchar *filename_created = NULL;
+  g_autoptr(GisSite) site = NULL;
+  int fd;
+
+  if (gis_page->driver == NULL)
+    return TRUE;
+
+  site = get_site_from_widgets (page);
+  location_file_content = ini_file_content_from_site (site);
+
+  fd = g_file_open_tmp (LOCATION_FILENAME_TEMPL, &filename_created, error);
+  if (fd < 0)
+    return FALSE;
+
+  /* immediately close the new file so we don't have multiple FDs open for
+   * the file at the same time
+   */
+  if (!g_close (fd, error))
+    return FALSE;
+
+  if (!g_file_set_contents (filename_created, location_file_content, -1,
+                            error))
+    return FALSE;
+
+  if (!gis_pkexec (LIBEXECDIR "/eos-write-location", filename_created, NULL,
+                   error))
+    return FALSE;
+
+  return TRUE;
+}
+
+static void
+entry_site_changed (GObject *object, GParamSpec *param, GisSitePage *page)
+{
+  GisSiteSearchEntry *entry = GIS_SITE_SEARCH_ENTRY (object);
+  GisSite *site;
+
+  site = gis_site_search_entry_get_site (entry);
+  set_site (page, site);
+}
+
+static void
+manual_check_toggled (GtkToggleButton *manual_check, GisSitePage *page)
+{
+  GisSitePagePrivate *priv = gis_site_page_get_instance_private (page);
+  gboolean active = gtk_toggle_button_get_active (manual_check);
+
+  /* clear the search entry and field GtkEntrys */
+  gtk_entry_set_text (GTK_ENTRY (priv->search_entry), "");
+
+  /* make the search active and all the "field" GtkEntrys inactive or vice versa
+   */
+  gtk_widget_set_can_focus (priv->search_entry, !active);
+  gtk_widget_set_can_focus (priv->id_entry, active);
+  gtk_widget_set_can_focus (priv->facility_entry, active);
+  gtk_widget_set_can_focus (priv->street_entry, active);
+  gtk_widget_set_can_focus (priv->locality_entry, active);
+  gtk_widget_set_can_focus (priv->region_entry, active);
+  gtk_widget_set_can_focus (priv->country_entry, active);
+
+  gtk_widget_set_sensitive (priv->search_entry, !active);
+  gtk_widget_set_sensitive (priv->id_entry, active);
+  gtk_widget_set_sensitive (priv->facility_entry, active);
+  gtk_widget_set_sensitive (priv->street_entry, active);
+  gtk_widget_set_sensitive (priv->locality_entry, active);
+  gtk_widget_set_sensitive (priv->region_entry, active);
+  gtk_widget_set_sensitive (priv->country_entry, active);
+
+  /* focus the first sensible widget for entry */
+  if (active)
+    gtk_widget_grab_focus (priv->id_entry);
+  else
+    gtk_widget_grab_focus (priv->search_entry);
+}
+
+static void
+gis_site_page_shown (GisPage *gis_page)
+{
+  GisSitePage *page = GIS_SITE_PAGE (gis_page);
+  GisSitePagePrivate *priv = gis_site_page_get_instance_private (page);
+
+  gtk_widget_grab_focus (priv->search_entry);
+}
+
+/**
+ * Parses the sites file which is a JSON file in the format:
+ *
+ * [SITE_1, SITE_2, SITE_3, ...]
+ *
+ * where each SITE is a JSON object containing one or more of the following
+ * members:
+ * * id: (string)       - a pre-existing ID (if any) the site admins use for it
+ * * facility: (string) - a name for the site/building
+ * * street: (string)   - the street address for the site (eg, "123 Main St.")
+ * * locality: (string) - city or equivalent
+ * * region: (string)   - state (US), department (Colombia), or equivalent
+ * * country: (string)  - country
+ */
+static GPtrArray*
+parse_sites_file (const gchar *filename)
+{
+  gboolean sites_usable = TRUE;
+  GPtrArray *sites = NULL;
+  JsonParser *parser;
+  JsonNode *root;
+  JsonReader *reader = NULL;
+  GError *error;
+
+  parser = json_parser_new ();
+
+  error = NULL;
+  json_parser_load_from_file (parser, filename, &error);
+  if (error)
+    {
+      if (!g_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_NOENT))
+        g_warning ("Unable to parse ‘%s’: %s", filename, error->message);
+      g_error_free (error);
+      goto out;
+    }
+
+  root = json_parser_get_root (parser);
+  if (!root)
+    goto out;
+
+  reader = json_reader_new (root);
+  if (!json_reader_is_array (reader))
+    goto out;
+
+  sites = g_ptr_array_new_full (0, g_object_unref);
+  int count = json_reader_count_elements (reader);
+  for (int i = 0; count > 0 && i < count; i++)
+    {
+      json_reader_read_element (reader, i);
+      if (json_reader_is_object (reader))
+        {
+          GisSite *site;
+          const gchar *id = NULL;
+          const gchar *facility = NULL;
+          const gchar *street = NULL;
+          const gchar *locality = NULL;
+          const gchar *region = NULL;
+          const gchar *country = NULL;
+
+          json_reader_read_member (reader, "id");
+          id = json_reader_get_string_value (reader);
+          json_reader_end_member (reader);
+
+          json_reader_read_member (reader, "facility");
+          facility = json_reader_get_string_value (reader);
+          json_reader_end_member (reader);
+
+          json_reader_read_member (reader, "street");
+          street = json_reader_get_string_value (reader);
+          json_reader_end_member (reader);
+
+          json_reader_read_member (reader, "locality");
+          locality = json_reader_get_string_value (reader);
+          json_reader_end_member (reader);
+
+          json_reader_read_member (reader, "region");
+          region = json_reader_get_string_value (reader);
+          json_reader_end_member (reader);
+
+          json_reader_read_member (reader, "country");
+          country = json_reader_get_string_value (reader);
+          json_reader_end_member (reader);
+
+          site = gis_site_new (id, facility, street, locality, region, country);
+          g_ptr_array_add (sites, site);
+        }
+      else
+        {
+          sites_usable = FALSE;
+          goto out;
+        }
+      json_reader_end_element (reader);
+    }
+
+out:
+  g_clear_object (&reader);
+  g_clear_object (&parser);
+  if (!sites_usable)
+    g_clear_pointer (&sites, g_ptr_array_unref);
+
+  return sites;
+}
+
+static void
+gis_site_page_constructed (GObject *object)
+{
+  GisSitePage *page = GIS_SITE_PAGE (object);
+  GisSitePagePrivate *priv = gis_site_page_get_instance_private (page);
+  gboolean visible = TRUE;
+
+  G_OBJECT_CLASS (gis_site_page_parent_class)->constructed (object);
+
+  if (!g_file_test (GIS_SITE_PAGE_SITES_FILE, G_FILE_TEST_EXISTS))
+    {
+      visible = FALSE;
+      goto out;
+    }
+
+  /* make all the fields insensitive to start */
+  manual_check_toggled (GTK_TOGGLE_BUTTON (priv->manual_check), page);
+
+  /* let the ID field show its current value (if populated based on a matched
+   * pre-defined site) but don't let the user set it when editing manually
+   */
+  gtk_widget_set_can_focus (priv->id_entry, FALSE);
+  gtk_widget_set_sensitive (priv->id_entry, FALSE);
+
+  g_signal_connect (priv->search_entry,
+                    "notify::" GIS_SITE_SEARCH_ENTRY_PROP_SITE,
+                    G_CALLBACK (entry_site_changed), page);
+
+  g_signal_connect (priv->manual_check, "toggled",
+                    G_CALLBACK (manual_check_toggled), page);
+
+  g_signal_connect (priv->id_entry,
+                    "notify::text-length",
+                    G_CALLBACK (update_page_validation), page);
+
+  g_signal_connect (priv->facility_entry,
+                    "notify::text-length",
+                    G_CALLBACK (update_page_validation), page);
+
+  g_signal_connect (priv->street_entry,
+                    "notify::text-length",
+                    G_CALLBACK (update_page_validation), page);
+
+  g_signal_connect (priv->locality_entry,
+                    "notify::text-length",
+                    G_CALLBACK (update_page_validation), page);
+
+  g_signal_connect (priv->region_entry,
+                    "notify::text-length",
+                    G_CALLBACK (update_page_validation), page);
+
+  g_signal_connect (priv->country_entry,
+                    "notify::text-length",
+                    G_CALLBACK (update_page_validation), page);
+
+out:
+  gtk_widget_set_visible (GTK_WIDGET (page), visible);
+}
+
+static void
+gis_site_page_class_init (GisSitePageClass *klass)
+{
+  GisPageClass *page_class = GIS_PAGE_CLASS (klass);
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  gtk_widget_class_set_template_from_resource (widget_class,
+                                               "/org/gnome/initial-setup/gis-site-page.ui");
+
+  gtk_widget_class_bind_template_child_private (widget_class, GisSitePage,
+                                                search_entry);
+  gtk_widget_class_bind_template_child_private (widget_class, GisSitePage,
+                                                manual_check);
+  gtk_widget_class_bind_template_child_private (widget_class, GisSitePage,
+                                                id_entry);
+  gtk_widget_class_bind_template_child_private (widget_class, GisSitePage,
+                                                facility_entry);
+  gtk_widget_class_bind_template_child_private (widget_class, GisSitePage,
+                                                street_entry);
+  gtk_widget_class_bind_template_child_private (widget_class, GisSitePage,
+                                                locality_entry);
+  gtk_widget_class_bind_template_child_private (widget_class, GisSitePage,
+                                                region_entry);
+  gtk_widget_class_bind_template_child_private (widget_class, GisSitePage,
+                                                country_entry);
+
+  page_class->page_id = PAGE_ID;
+  page_class->save_data = gis_site_page_save_data;
+  page_class->shown = gis_site_page_shown;
+  object_class->constructed = gis_site_page_constructed;
+}
+
+static void
+gis_site_page_init (GisSitePage *page)
+{
+  GisSitePagePrivate *priv = gis_site_page_get_instance_private (page);
+  GPtrArray *sites_array;
+
+  g_resources_register (site_get_resource ());
+
+  g_type_ensure (GIS_TYPE_SITE_SEARCH_ENTRY);
+
+  gtk_widget_init_template (GTK_WIDGET (page));
+
+  sites_array = parse_sites_file (GIS_SITE_PAGE_SITES_FILE);
+
+  g_object_set (priv->search_entry, GIS_SITE_SEARCH_ENTRY_PROP_SITES_AVAILABLE,
+                sites_array, NULL);
+
+  g_clear_pointer (&sites_array, g_ptr_array_unref);
+}
+
+GisPage *
+gis_prepare_site_page (GisDriver *driver)
+{
+  if (gis_driver_is_live_session (driver))
+    return NULL;
+
+  return g_object_new (GIS_TYPE_SITE_PAGE,
+                       "driver", driver,
+                       NULL);
+}
diff --git a/gnome-initial-setup/pages/site/gis-site-page.h b/gnome-initial-setup/pages/site/gis-site-page.h
new file mode 100644
index 00000000..df4c2232
--- /dev/null
+++ b/gnome-initial-setup/pages/site/gis-site-page.h
@@ -0,0 +1,62 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+/*
+ * Copyright (C) 2012 Red Hat
+ * Copyright (C) 2018 Endless Mobile, Inc.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * Written by:
+ *     Jasper St. Pierre <jstpierre mecheye net>
+ *     Travis Reitter <travis reitter endlessm com>
+ */
+
+#ifndef __GIS_SITE_PAGE_H__
+#define __GIS_SITE_PAGE_H__
+
+#include <glib-object.h>
+
+#include "gnome-initial-setup.h"
+
+#define GIS_SITE_PAGE_ID "site"
+#define GIS_SITE_PAGE_SITES_FILE "/var/lib/eos-image-defaults/deployment-sites.json"
+
+G_BEGIN_DECLS
+
+#define GIS_TYPE_SITE_PAGE               (gis_site_page_get_type ())
+#define GIS_SITE_PAGE(obj)                           (G_TYPE_CHECK_INSTANCE_CAST ((obj), GIS_TYPE_SITE_PAGE, 
GisSitePage))
+#define GIS_SITE_PAGE_CLASS(klass)                   (G_TYPE_CHECK_CLASS_CAST ((klass),  GIS_TYPE_SITE_PAGE, 
GisSitePageClass))
+#define GIS_IS_SITE_PAGE(obj)         (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GIS_TYPE_SITE_PAGE))
+#define GIS_IS_SITE_PAGE_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass),  GIS_TYPE_SITE_PAGE))
+#define GIS_SITE_PAGE_GET_CLASS(obj)                 (G_TYPE_INSTANCE_GET_CLASS ((obj),  GIS_TYPE_SITE_PAGE, 
GisSitePageClass))
+
+typedef struct _GisSitePage        GisSitePage;
+typedef struct _GisSitePageClass   GisSitePageClass;
+
+struct _GisSitePage
+{
+  GisPage parent;
+};
+
+struct _GisSitePageClass
+{
+  GisPageClass parent_class;
+};
+
+GType gis_site_page_get_type (void);
+
+GisPage *gis_prepare_site_page (GisDriver *driver);
+
+G_END_DECLS
+
+#endif /* __GIS_SITE_PAGE_H__ */
diff --git a/gnome-initial-setup/pages/site/gis-site-page.ui b/gnome-initial-setup/pages/site/gis-site-page.ui
new file mode 100644
index 00000000..7675b158
--- /dev/null
+++ b/gnome-initial-setup/pages/site/gis-site-page.ui
@@ -0,0 +1,279 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <!-- interface-requires gtk+ 3.0 -->
+  <template class="GisSitePage" parent="GisPage">
+    <child>
+      <object class="GtkBox" id="box">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="orientation">vertical</property>
+        <property name="halign">center</property>
+        <property name="valign">fill</property>
+        <child>
+          <object class="GtkImage" id="image">
+            <property name="visible" bind-source="GisSitePage" bind-property="small-screen" 
bind-flags="invert-boolean|sync-create"/>
+            <property name="can_focus">False</property>
+            <property name="margin_top">24</property>
+            <property name="pixel_size">96</property>
+            <property name="icon_name">poi-building</property>
+            <style>
+              <class name="dim-label" />
+            </style>
+          </object>
+        </child>
+        <child>
+          <object class="GtkLabel" id="title">
+            <property name="visible" bind-source="GisSitePage" bind-property="small-screen" 
bind-flags="invert-boolean|sync-create"/>
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="margin_top">12</property>
+            <property name="halign">center</property>
+            <property name="valign">start</property>
+            <property name="vexpand">True</property>
+            <property name="label" translatable="yes">Site</property>
+            <attributes>
+              <attribute name="weight" value="bold"/>
+              <attribute name="scale" value="1.8"/>
+            </attributes>
+          </object>
+        </child>
+        <child>
+          <object class="GtkBox" id="page_box">
+            <property name="visible">True</property>
+            <property name="margin_top">18</property>
+            <property name="margin_bottom">18</property>
+            <property name="valign">center</property>
+            <property name="orientation">vertical</property>
+            <property name="spacing">14</property>
+            <child>
+              <object class="GtkLabel">
+                <property name="visible">True</property>
+                <property name="xalign">0</property>
+                <property name="halign">center</property>
+                <property name="wrap">True</property>
+                <property name="label" translatable="yes">At which site is this computer located? Search for 
your site here. You may enter manually if there is no match.</property>
+              </object>
+            </child>
+
+            <child>
+              <object class="GtkBox">
+                <property name="visible">True</property>
+                <property name="margin_top">10</property>
+                <property name="halign">center</property>
+                <property name="orientation">horizontal</property>
+                <property name="spacing">14</property>
+
+                <child>
+                  <object class="GisSiteSearchEntry" id="search_entry">
+                    <property name="visible">True</property>
+                    <property name="halign">center</property>
+                    <property name="max-width-chars">55</property>
+                  </object>
+                </child>
+
+                <child>
+                  <object class="GtkCheckButton" id="manual_check">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="label" translatable="yes">enter manually</property>
+                    <property name="halign">end</property>
+                  </object>
+                  <packing>
+                    <property name="expand">False</property>
+                  </packing>
+                </child>
+              </object>
+            </child>
+
+            <child>
+              <object class="GtkGrid" id="fields_grid">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="column_spacing">12</property>
+                <property name="row_spacing">6</property>
+                <property name="margin_top">20</property>
+                <property name="halign">center</property>
+
+                <child>
+                  <object class="GtkLabel" id="id_label">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="halign">start</property>
+                    <property name="label" translatable="yes">ID</property>
+                  </object>
+                  <packing>
+                    <property name="left_attach">0</property>
+                    <property name="top_attach">0</property>
+                    <property name="width">1</property>
+                    <property name="height">1</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkEntry" id="id_entry">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="visibility">True</property>
+                    <property name="width_chars">24</property>
+                    <property name="margin_end">30</property>
+                  </object>
+                  <packing>
+                    <property name="left_attach">1</property>
+                    <property name="top_attach">0</property>
+                    <property name="width">1</property>
+                    <property name="height">1</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkLabel" id="facility_label">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="halign">start</property>
+                    <property name="label" translatable="yes">Facility</property>
+                  </object>
+                  <packing>
+                    <property name="left_attach">3</property>
+                    <property name="top_attach">0</property>
+                    <property name="width">1</property>
+                    <property name="height">1</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkEntry" id="facility_entry">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="visibility">True</property>
+                    <property name="width_chars">24</property>
+                  </object>
+                  <packing>
+                    <property name="left_attach">4</property>
+                    <property name="top_attach">0</property>
+                    <property name="width">1</property>
+                    <property name="height">1</property>
+                  </packing>
+                </child>
+
+                <child>
+                  <object class="GtkLabel" id="street_label">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="halign">start</property>
+                    <property name="label" translatable="yes">Street address</property>
+                  </object>
+                  <packing>
+                    <property name="left_attach">0</property>
+                    <property name="top_attach">1</property>
+                    <property name="width">1</property>
+                    <property name="height">1</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkEntry" id="street_entry">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="visibility">True</property>
+                    <property name="width_chars">24</property>
+                    <property name="margin_end">30</property>
+                  </object>
+                  <packing>
+                    <property name="left_attach">1</property>
+                    <property name="top_attach">1</property>
+                    <property name="width">1</property>
+                    <property name="height">1</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkLabel" id="locality_label">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="halign">start</property>
+                    <property name="label" translatable="yes">City</property>
+                  </object>
+                  <packing>
+                    <property name="left_attach">3</property>
+                    <property name="top_attach">1</property>
+                    <property name="width">1</property>
+                    <property name="height">1</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkEntry" id="locality_entry">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="visibility">True</property>
+                    <property name="width_chars">24</property>
+                  </object>
+                  <packing>
+                    <property name="left_attach">4</property>
+                    <property name="top_attach">1</property>
+                    <property name="width">1</property>
+                    <property name="height">1</property>
+                  </packing>
+                </child>
+
+                <child>
+                  <object class="GtkLabel" id="region_label">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="halign">start</property>
+                    <property name="label" translatable="yes">State</property>
+                  </object>
+                  <packing>
+                    <property name="left_attach">0</property>
+                    <property name="top_attach">2</property>
+                    <property name="width">1</property>
+                    <property name="height">1</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkEntry" id="region_entry">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="visibility">True</property>
+                    <property name="width_chars">24</property>
+                    <property name="margin_end">30</property>
+                  </object>
+                  <packing>
+                    <property name="left_attach">1</property>
+                    <property name="top_attach">2</property>
+                    <property name="width">1</property>
+                    <property name="height">1</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkLabel" id="country_label">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="halign">start</property>
+                    <property name="label" translatable="yes">Country</property>
+                  </object>
+                  <packing>
+                    <property name="left_attach">3</property>
+                    <property name="top_attach">2</property>
+                    <property name="width">1</property>
+                    <property name="height">1</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkEntry" id="country_entry">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="visibility">True</property>
+                    <property name="width_chars">24</property>
+                  </object>
+                  <packing>
+                    <property name="left_attach">4</property>
+                    <property name="top_attach">2</property>
+                    <property name="width">1</property>
+                    <property name="height">1</property>
+                  </packing>
+                </child>
+
+              </object>
+            </child>
+
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/gnome-initial-setup/pages/site/gis-site-search-entry.c 
b/gnome-initial-setup/pages/site/gis-site-search-entry.c
new file mode 100644
index 00000000..f16f2686
--- /dev/null
+++ b/gnome-initial-setup/pages/site/gis-site-search-entry.c
@@ -0,0 +1,452 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+
+/* Copyright (C) 2008 Red Hat, Inc.
+ * Copyright (C) 2018 Endless Mobile, Inc.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * Written by:
+ *     Travis Reitter <travis reitter endlessm com>
+ */
+
+#include <string.h>
+#include <gio/gio.h>
+#include <glib/gi18n.h>
+#include <gtk/gtk.h>
+
+#include "gis-site-search-entry.h"
+
+/**
+ * SECTION:GisSiteSearchEntry
+ * @Title: GisSiteSearchEntry
+ *
+ * A subclass of #GtkSearchEntry that provides autocompletion on
+ * #GisSite<!-- -->s
+ */
+
+typedef struct {
+  GisSite          *site;
+  GPtrArray        *sites;
+} GisSiteSearchEntryPrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (GisSiteSearchEntry, gis_site_search_entry,
+                            GTK_TYPE_SEARCH_ENTRY)
+
+enum {
+  PROP_0,
+
+  PROP_SITES_AVAILABLE,
+  PROP_SITE,
+
+  LAST_PROP
+};
+
+static void set_property (GObject *object, guint prop_id,
+                          const GValue *value, GParamSpec *pspec);
+static void get_property (GObject *object, guint prop_id,
+                          GValue *value, GParamSpec *pspec);
+
+static void set_site_internal (GisSiteSearchEntry *entry,
+                               GtkTreeIter        *iter,
+                               GisSite            *site);
+
+static void fill_store (GisSite *site, GtkListStore *store);
+
+enum STORE_COL
+{
+  GIS_SITE_SEARCH_ENTRY_COL_DISPLAY_NAME = 0,
+  GIS_SITE_SEARCH_ENTRY_COL_SITE,
+  GIS_SITE_SEARCH_ENTRY_COL_LOCAL_COMPARE_NAME,
+};
+
+static gboolean matcher (GtkEntryCompletion *completion, const char *key,
+                         GtkTreeIter *iter, gpointer user_data);
+static gboolean match_selected (GtkEntryCompletion *completion,
+                                GtkTreeModel       *model,
+                                GtkTreeIter        *iter,
+                                gpointer            entry);
+static void     entry_changed (GisSiteSearchEntry *entry);
+
+static void
+gis_site_search_entry_init (GisSiteSearchEntry *entry)
+{
+  GtkEntryCompletion *completion;
+
+  completion = gtk_entry_completion_new ();
+
+  gtk_entry_completion_set_popup_set_width (completion, FALSE);
+  gtk_entry_completion_set_text_column (completion,
+                                        GIS_SITE_SEARCH_ENTRY_COL_DISPLAY_NAME);
+  gtk_entry_completion_set_match_func (completion, matcher, NULL, NULL);
+  gtk_entry_completion_set_inline_completion (completion, TRUE);
+
+  g_signal_connect (completion, "match-selected", G_CALLBACK (match_selected),
+                    entry);
+
+  gtk_entry_set_completion (GTK_ENTRY (entry), completion);
+  g_object_unref (completion);
+
+  g_signal_connect (entry, "changed",
+                    G_CALLBACK (entry_changed), NULL);
+}
+
+static void
+finalize (GObject *object)
+{
+  GisSiteSearchEntryPrivate *priv = gis_site_search_entry_get_instance_private (
+    GIS_SITE_SEARCH_ENTRY (object));
+
+  g_clear_object (&priv->site);
+  g_clear_pointer (&priv->sites, g_ptr_array_unref);
+
+  G_OBJECT_CLASS (gis_site_search_entry_parent_class)->finalize (object);
+}
+
+static void
+gis_site_search_entry_class_init (
+        GisSiteSearchEntryClass *site_search_entry_class)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (site_search_entry_class);
+
+  object_class->finalize = finalize;
+  object_class->set_property = set_property;
+  object_class->get_property = get_property;
+
+  /* properties */
+  g_object_class_install_property (
+      object_class, PROP_SITES_AVAILABLE,
+      g_param_spec_boxed (GIS_SITE_SEARCH_ENTRY_PROP_SITES_AVAILABLE,
+                          "Full site list",
+                          "The array of sites available",
+                          G_TYPE_PTR_ARRAY,
+                          G_PARAM_WRITABLE));
+
+  g_object_class_install_property (
+      object_class, PROP_SITE,
+      g_param_spec_object (GIS_SITE_SEARCH_ENTRY_PROP_SITE,
+                           "Site",
+                           "The selected site",
+                           GIS_TYPE_SITE,
+                           G_PARAM_READWRITE));
+}
+
+static void
+set_property_sites_available (GisSiteSearchEntry *self,
+                              const GValue       *value)
+{
+  GisSiteSearchEntryPrivate *priv = gis_site_search_entry_get_instance_private (
+    self);
+  GtkListStore *store = NULL;
+  GtkEntryCompletion *completion;
+
+  store = gtk_list_store_new (3, G_TYPE_STRING, GIS_TYPE_SITE, G_TYPE_STRING);
+
+  g_clear_pointer (&priv->sites, g_ptr_array_unref);
+  priv->sites = g_value_dup_boxed (value);
+  for (int i = 0; priv->sites && i < priv->sites->len; i++)
+    {
+      GisSite *site = g_ptr_array_index (priv->sites, i);
+      fill_store (site, store);
+    }
+
+  completion = gtk_entry_get_completion (GTK_ENTRY (self));
+  gtk_entry_completion_set_match_func (completion, matcher, NULL, NULL);
+  gtk_entry_completion_set_model (completion, GTK_TREE_MODEL (store));
+}
+
+static void
+set_property (GObject *object, guint prop_id,
+              const GValue *value, GParamSpec *pspec)
+{
+  switch (prop_id)
+    {
+      case PROP_SITES_AVAILABLE:
+        set_property_sites_available (GIS_SITE_SEARCH_ENTRY (object), value);
+        break;
+      case PROP_SITE:
+        gis_site_search_entry_set_site (GIS_SITE_SEARCH_ENTRY (object),
+                                        g_value_get_object (value));
+        break;
+      default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+        break;
+    }
+}
+
+static void
+get_property (GObject *object, guint prop_id,
+              GValue *value, GParamSpec *pspec)
+{
+  GisSiteSearchEntryPrivate *priv = gis_site_search_entry_get_instance_private (
+    GIS_SITE_SEARCH_ENTRY (object));
+
+  switch (prop_id)
+    {
+      case PROP_SITE:
+        g_value_set_boxed (value, priv->site);
+        break;
+      default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+        break;
+    }
+}
+
+static void
+entry_changed (GisSiteSearchEntry *entry)
+{
+  const gchar *text = gtk_entry_get_text (GTK_ENTRY (entry));
+
+  if (!text || text[0] == '\0')
+    set_site_internal (entry, NULL, NULL);
+}
+
+static void
+set_site_internal (GisSiteSearchEntry *entry,
+                   GtkTreeIter        *iter,
+                   GisSite            *site)
+{
+  GisSiteSearchEntryPrivate *priv =
+      gis_site_search_entry_get_instance_private (entry);
+
+  g_clear_object (&priv->site);
+
+  g_assert (iter == NULL || site == NULL);
+
+  if (iter)
+    {
+      char *name;
+      GtkEntryCompletion *completion = gtk_entry_get_completion (
+          GTK_ENTRY (entry));
+      GtkTreeModel *model = gtk_entry_completion_get_model (completion);
+
+      gtk_tree_model_get (model, iter,
+                          GIS_SITE_SEARCH_ENTRY_COL_DISPLAY_NAME, &name,
+                          GIS_SITE_SEARCH_ENTRY_COL_SITE, &priv->site,
+                          -1);
+      gtk_entry_set_text (GTK_ENTRY (entry), name);
+      g_free (name);
+    }
+  else if (site)
+    {
+      gchar *display_name = gis_site_get_display_name (site);
+      priv->site = g_object_ref (site);
+      gtk_entry_set_text (GTK_ENTRY (entry), display_name);
+
+      g_free (display_name);
+    }
+  else
+    {
+      gtk_entry_set_text (GTK_ENTRY (entry), "");
+    }
+
+  gtk_editable_set_position (GTK_EDITABLE (entry), -1);
+  g_object_notify (G_OBJECT (entry), GIS_SITE_SEARCH_ENTRY_PROP_SITE);
+}
+
+/**
+ * gis_site_search_entry_set_site:
+ * @entry: a #GisSiteSearchEntry
+ * @site: (allow-none): a #GisSite in @entry, or %NULL to clear @entry
+ *
+ * Sets @entry's site to @site, and update the text of the entry accordingly.
+ **/
+void
+gis_site_search_entry_set_site (GisSiteSearchEntry *entry,
+                                GisSite            *site)
+{
+  g_return_if_fail (GIS_IS_SITE_SEARCH_ENTRY (entry));
+
+  set_site_internal (entry, NULL, site);
+}
+
+/**
+ * gis_site_search_entry_get_site:
+ * @entry: a #GisSiteSearchEntry
+ *
+ * Gets the site that was set by a previous call to
+ * gis_site_search_entry_set_site() or was selected by the user.
+ *
+ * Return value: (transfer none) (allow-none): the selected site or %NULL if no
+ * site is selected.
+ **/
+GisSite *
+gis_site_search_entry_get_site (GisSiteSearchEntry *entry)
+{
+  GisSiteSearchEntryPrivate *priv;
+
+  g_return_val_if_fail (GIS_IS_SITE_SEARCH_ENTRY (entry), NULL);
+
+  priv = gis_site_search_entry_get_instance_private (entry);
+
+  return priv->site;
+}
+
+static char *
+find_word (const char *full_name, const char *word, int word_len,
+           gboolean whole_word, gboolean is_first_word)
+{
+  char *p;
+
+  if (word == NULL || *word == '\0')
+    return NULL;
+
+  p = (char *)full_name - 1;
+  while ((p = strchr (p + 1, *word)))
+    {
+      if (strncmp (p, word, word_len) != 0)
+        continue;
+
+      if (p > (char *)full_name)
+        {
+          char *prev = g_utf8_prev_char (p);
+
+          /* Make sure p points to the start of a word */
+          if (g_unichar_isalpha (g_utf8_get_char (prev)))
+            continue;
+
+          /* If we're matching the first word of the key, it has to
+           * match the first word of the field.
+           * Eg, it either matches the start of the string
+           * (which we already know it doesn't at this point) or
+           * it is preceded by the string ", " or "(" (which isn't actually
+           * a perfect test.)
+           */
+          if (is_first_word)
+            {
+              if (prev == (char *)full_name ||
+                  ((prev - 1 <= full_name && strncmp (prev - 1, ", ", 2) != 0)
+                    && *prev != '('))
+                continue;
+            }
+        }
+
+      if (whole_word && g_unichar_isalpha (g_utf8_get_char (p + word_len)))
+        continue;
+
+      return p;
+    }
+
+  return NULL;
+}
+
+static gboolean
+match_compare_name (const char *key, const char *name)
+{
+  gboolean is_first_word = TRUE;
+  size_t len;
+
+  /* Ignore whitespace before the string */
+  key += strspn (key, " ");
+
+  /* All but the last word in KEY must match a full word from NAME,
+   * in order (but possibly skipping some words from NAME).
+   */
+  len = strcspn (key, " ");
+  while (key[len])
+    {
+      name = find_word (name, key, len, TRUE, is_first_word);
+      if (!name)
+        return FALSE;
+
+      key += len;
+      while (*key && !g_unichar_isalnum (g_utf8_get_char (key)))
+        key = g_utf8_next_char (key);
+      while (*name && !g_unichar_isalnum (g_utf8_get_char (name)))
+        name = g_utf8_next_char (name);
+
+      len = strcspn (key, " ");
+      is_first_word = FALSE;
+    }
+
+  /* The last word in KEY must match a prefix of a following word in NAME */
+  if (len == 0)
+    return TRUE;
+
+  /* if we get here, key[len] == 0 */
+  g_assert (len == strlen (key));
+
+  return find_word (name, key, len, FALSE, is_first_word) != NULL;
+}
+
+static gboolean
+matcher (GtkEntryCompletion *completion, const char *key,
+         GtkTreeIter *iter, gpointer user_data)
+{
+  char *compare_name;
+  gboolean match;
+
+  gtk_tree_model_get (gtk_entry_completion_get_model (completion), iter,
+                      GIS_SITE_SEARCH_ENTRY_COL_LOCAL_COMPARE_NAME, &compare_name,
+                      -1);
+
+  match = match_compare_name (key, compare_name);
+
+  g_free (compare_name);
+
+  return match;
+}
+
+static gboolean
+match_selected (GtkEntryCompletion *completion,
+                GtkTreeModel       *model,
+                GtkTreeIter        *iter,
+                gpointer            entry)
+{
+  set_site_internal (entry, iter, NULL);
+
+  return TRUE;
+}
+
+static void
+fill_store (GisSite *site, GtkListStore *store)
+{
+  char *display_name;
+  char *normalized;
+  char *compare_name;
+
+  display_name = gis_site_get_display_name (site);
+  normalized = g_utf8_normalize (display_name, -1, G_NORMALIZE_ALL);
+  compare_name = g_utf8_casefold (normalized, -1);
+
+  gtk_list_store_insert_with_values (store, NULL, -1,
+                                     GIS_SITE_SEARCH_ENTRY_COL_DISPLAY_NAME, display_name,
+                                     GIS_SITE_SEARCH_ENTRY_COL_SITE, site,
+                                     GIS_SITE_SEARCH_ENTRY_COL_LOCAL_COMPARE_NAME, compare_name,
+                                     -1);
+
+  g_free (normalized);
+  g_free (compare_name);
+  g_free (display_name);
+}
+
+/**
+ * gis_site_search_entry_new:
+ * @sites: a #GPtrArray of #GisSite objects to serve as the data source for this
+ * search widget. This array and its content must not be modified after this
+ * function is called.
+ *
+ * Creates a new #GisSiteSearchEntry.
+ *
+ * To be nofified when the user makes a selection, add a signal handler for
+ * "notify::site".
+ *
+ * Return value: the new #GisSiteSearchEntry
+ **/
+GtkWidget *
+gis_site_search_entry_new (GPtrArray *sites)
+{
+  return g_object_new (GIS_TYPE_SITE_SEARCH_ENTRY,
+                       GIS_SITE_SEARCH_ENTRY_PROP_SITES_AVAILABLE, sites,
+                       NULL);
+}
diff --git a/gnome-initial-setup/pages/site/gis-site-search-entry.h 
b/gnome-initial-setup/pages/site/gis-site-search-entry.h
new file mode 100644
index 00000000..02e84912
--- /dev/null
+++ b/gnome-initial-setup/pages/site/gis-site-search-entry.h
@@ -0,0 +1,44 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+/* location-entry.h - Location-selecting text entry
+ *
+ * Copyright 2008, Red Hat, Inc.
+ * Copyright (C) 2018 Endless Mobile, Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, see
+ * <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <glib-object.h>
+#include <gtk/gtk.h>
+
+#include "gis-site.h"
+
+#define GIS_SITE_SEARCH_ENTRY_PROP_SITES_AVAILABLE "sites-available"
+#define GIS_SITE_SEARCH_ENTRY_PROP_SITE "site"
+
+#define GIS_TYPE_SITE_SEARCH_ENTRY (gis_site_search_entry_get_type())
+G_DECLARE_DERIVABLE_TYPE (GisSiteSearchEntry, gis_site_search_entry, GIS, SITE_SEARCH_ENTRY, GtkSearchEntry)
+
+struct _GisSiteSearchEntryClass {
+    GtkSearchEntryClass parent_class;
+};
+
+GtkWidget*        gis_site_search_entry_new          (GPtrArray *sites);
+
+void              gis_site_search_entry_set_site (GisSiteSearchEntry *entry,
+                                                  GisSite            *site);
+
+GisSite*          gis_site_search_entry_get_site (GisSiteSearchEntry *entry);
diff --git a/gnome-initial-setup/pages/site/gis-site.c b/gnome-initial-setup/pages/site/gis-site.c
new file mode 100644
index 00000000..556bb0fc
--- /dev/null
+++ b/gnome-initial-setup/pages/site/gis-site.c
@@ -0,0 +1,289 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+/*
+ * Copyright (C) 2018 Endless Mobile, Inc.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * Written by:
+ *     Travis Reitter <travis reitter endlessm com>
+ */
+
+#include "gis-site.h"
+
+/**
+ * GisSite:
+ *
+ * All the details about a given deployment site needed for the purposes of
+ * grouping computers at a given site for the purposes of metrics.
+ *
+ * #GisSite is an opaque data structure and can only be accessed
+ * using the following functions.
+ */
+
+struct _GisSite
+{
+  GObject parent;
+};
+
+struct _GisSitePrivate
+{
+  gchar             *id;
+
+  /* eg, name of a school */
+  gchar             *facility;
+
+  /* eg, "123 Main Street" */
+  gchar             *street;
+
+  /* city, township, or equivalent */
+  gchar             *locality;
+
+  /* municipal division between a county and country such as a state in the
+   * United States or department in Colombia
+   */
+  gchar             *region;
+
+  gchar             *country;
+};
+typedef struct _GisSitePrivate GisSitePrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (GisSite, gis_site, G_TYPE_OBJECT)
+
+static void
+gis_site_finalize (GObject *object)
+{
+  GisSite *site = GIS_SITE (object);
+  GisSitePrivate *priv = gis_site_get_instance_private (site);
+  g_free (priv->id);
+  g_free (priv->facility);
+  g_free (priv->street);
+  g_free (priv->locality);
+  g_free (priv->region);
+  g_free (priv->country);
+
+  G_OBJECT_CLASS (gis_site_parent_class)->finalize (object);
+}
+
+static void
+gis_site_init (GisSite *self)
+{
+}
+
+static void
+gis_site_class_init (GisSiteClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = gis_site_finalize;
+}
+
+/**
+ * gis_site_new:
+ * @id: (nullable): the ID, if any, the site's admins use for this site (such as
+ *                  a school ID).
+ * @facility: (nullable): name of the site (eg, the name of a school)
+ * @street: (nullable): street address (eg, "123 Main Street")
+ * @locality: (nullable): city, township, or equivalent
+ * @region: (nullable): municipal division between a county and country such as
+ *                      a state in the United States or department in Colombia
+ * @country: (nullable): country
+ *
+ * Creates a new #GisSite instance.
+ *
+ * All fields are nullable but at least the @id, @facility, or @locality should
+ * be set for this #GisSite to be useful.
+ *
+ * Returns: a new #GisSite instance
+ **/
+GisSite *
+gis_site_new (const gchar *id,
+              const gchar *facility,
+              const gchar *street,
+              const gchar *locality,
+              const gchar *region,
+              const gchar *country)
+{
+  GisSite *self;
+  GisSitePrivate *priv;
+
+  self = g_object_new (GIS_TYPE_SITE, NULL);
+
+  priv = gis_site_get_instance_private (self);
+  priv->id = g_strdup (id);
+  priv->facility = g_strdup (facility);
+  priv->street = g_strdup (street);
+  priv->locality = g_strdup (locality);
+  priv->region = g_strdup (region);
+  priv->country = g_strdup (country);
+
+  return self;
+}
+
+/**
+ * gis_site_get_id:
+ * @site: a #GisSite
+ *
+ * Gets the unique identifier for @site.
+ *
+ * Returns: (transfer none): the identifier or %NULL
+ **/
+const gchar *
+gis_site_get_id (GisSite *site)
+{
+  GisSitePrivate *priv = gis_site_get_instance_private (site);
+
+  g_return_val_if_fail (GIS_IS_SITE (site), NULL);
+
+  return priv->id;
+}
+
+/**
+ * gis_site_get_facility:
+ * @site: a #GisSite
+ *
+ * Gets the name of the facility (eg, a school) for @site.
+ *
+ * Returns: (transfer none): the facility or %NULL
+ **/
+const gchar *
+gis_site_get_facility (GisSite *site)
+{
+  GisSitePrivate *priv = gis_site_get_instance_private (site);
+
+  g_return_val_if_fail (GIS_IS_SITE (site), NULL);
+
+  return priv->facility;
+}
+
+/**
+ * gis_site_get_street:
+ * @site: a #GisSite
+ *
+ * Gets the street address (eg, 123 Main Street) for @site.
+ *
+ * Returns: (transfer none): the street address or %NULL
+ **/
+const gchar *
+gis_site_get_street (GisSite *site)
+{
+  GisSitePrivate *priv = gis_site_get_instance_private (site);
+
+  g_return_val_if_fail (GIS_IS_SITE (site), NULL);
+
+  return priv->street;
+}
+
+/**
+ * gis_site_get_locality:
+ * @site: a #GisSite
+ *
+ * Gets the locality (city or equivalent) for @site.
+ *
+ * Returns: (transfer none): the locality or %NULL
+ **/
+const gchar *
+gis_site_get_locality (GisSite *site)
+{
+  GisSitePrivate *priv = gis_site_get_instance_private (site);
+
+  g_return_val_if_fail (GIS_IS_SITE (site), NULL);
+
+  return priv->locality;
+}
+
+/**
+ * gis_site_get_region:
+ * @site: a #GisSite
+ *
+ * Gets the region (equivalent to a state in the US or department in Colombia)
+ * for @site.
+ *
+ * Returns: (transfer none): the region or %NULL
+ **/
+const gchar *
+gis_site_get_region (GisSite *site)
+{
+  GisSitePrivate *priv = gis_site_get_instance_private (site);
+
+  g_return_val_if_fail (GIS_IS_SITE (site), NULL);
+
+  return priv->region;
+}
+
+/**
+ * gis_site_get_country:
+ * @site: a #GisSite
+ *
+ * Gets the country for @site.
+ *
+ * Returns: (transfer none): the country or %NULL
+ **/
+const gchar *
+gis_site_get_country (GisSite *site)
+{
+  GisSitePrivate *priv = gis_site_get_instance_private (site);
+
+  g_return_val_if_fail (GIS_IS_SITE (site), NULL);
+
+  return priv->country;
+}
+
+static void
+string_append_safe (GString     *string,
+                    const gchar *field)
+{
+  if (field && field[0] != '\0')
+    g_string_append_printf (string, "%s%s", string->len > 0 ? ", " : "", field);
+}
+
+/**
+ * Get a string for this #GisSite that's suitable to display to a user.
+ *
+ * NOTE: this assumes the values provided as arguments to this #GisSite are
+ * already translated into the relevant local language (and that it's a single
+ * language).
+ *
+ * @site: a #GisSite
+ *
+ * Returns: (transfer full): a display name suitable for display (which must be
+ * freed) or %NULL if none could be created.
+ */
+gchar*
+gis_site_get_display_name (GisSite *site)
+{
+  gchar *retval;
+  const gchar *id = gis_site_get_id (site);
+  const gchar *facility = gis_site_get_facility (site);
+  const gchar *street = gis_site_get_street (site);
+  const gchar *locality = gis_site_get_locality (site);
+  const gchar *region = gis_site_get_region (site);
+  const gchar *country = gis_site_get_country (site);
+
+  GString *string = g_string_new ("");
+
+  string_append_safe (string, id);
+  string_append_safe (string, facility);
+  string_append_safe (string, street);
+  string_append_safe (string, locality);
+  string_append_safe (string, region);
+  string_append_safe (string, country);
+
+  retval = g_string_free (string, FALSE);
+
+  if (g_str_equal (retval, ""))
+    g_clear_pointer (&retval, g_free);
+
+  return retval;
+}
+
diff --git a/gnome-initial-setup/pages/site/gis-site.h b/gnome-initial-setup/pages/site/gis-site.h
new file mode 100644
index 00000000..721d4ab4
--- /dev/null
+++ b/gnome-initial-setup/pages/site/gis-site.h
@@ -0,0 +1,47 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+/*
+ * Copyright (C) 2018 Endless Mobile, Inc.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * Written by:
+ *     Travis Reitter <travis reitter endlessm com>
+ */
+
+#pragma once
+
+#include <glib.h>
+#include <glib-object.h>
+
+#define GIS_TYPE_SITE (gis_site_get_type())
+G_DECLARE_FINAL_TYPE (GisSite, gis_site, GIS, SITE, GObject)
+
+struct _GisSiteClass
+{
+  GObjectClass parent_class;
+};
+
+GisSite *     gis_site_new              (const gchar *id,
+                                         const gchar *facility,
+                                         const gchar *street,
+                                         const gchar *locality,
+                                         const gchar *region,
+                                         const gchar *country);
+const gchar * gis_site_get_id           (GisSite *site);
+const gchar * gis_site_get_facility     (GisSite *site);
+const gchar * gis_site_get_street       (GisSite *site);
+const gchar * gis_site_get_locality     (GisSite *site);
+const gchar * gis_site_get_region       (GisSite *site);
+const gchar * gis_site_get_country      (GisSite *site);
+gchar *       gis_site_get_display_name (GisSite *site);
diff --git a/gnome-initial-setup/pages/site/meson.build b/gnome-initial-setup/pages/site/meson.build
new file mode 100644
index 00000000..395604b2
--- /dev/null
+++ b/gnome-initial-setup/pages/site/meson.build
@@ -0,0 +1,20 @@
+sources += gnome.compile_resources(
+       'site-resources',
+       files('site.gresource.xml'),
+       c_name: 'site'
+)
+
+sources += files(
+       'gis-site-page.c',
+       'gis-site-page.h',
+       'gis-site.c',
+       'gis-site.h',
+       'gis-site-search-entry.c',
+       'gis-site-search-entry.h',
+)
+
+install_data(
+       'eos-write-location',
+       install_dir: libexec_dir,
+       install_mode: 'rwxr-xr-x'
+)
diff --git a/gnome-initial-setup/pages/site/site.gresource.xml 
b/gnome-initial-setup/pages/site/site.gresource.xml
new file mode 100644
index 00000000..960600a8
--- /dev/null
+++ b/gnome-initial-setup/pages/site/site.gresource.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+  <gresource prefix="/org/gnome/initial-setup">
+    <file preprocess="xml-stripblanks" alias="gis-site-page.ui">gis-site-page.ui</file>
+  </gresource>
+</gresources>
diff --git a/po/POTFILES.in b/po/POTFILES.in
index e96f9b40..c9caf787 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -43,6 +43,8 @@ gnome-initial-setup/pages/password/gis-password-page.ui
 gnome-initial-setup/pages/password/pw-utils.c
 gnome-initial-setup/pages/privacy/gis-privacy-page.c
 gnome-initial-setup/pages/privacy/gis-privacy-page.ui
+gnome-initial-setup/pages/site/gis-site-page.c
+gnome-initial-setup/pages/site/gis-site-page.ui
 gnome-initial-setup/pages/summary/gis-summary-page.c
 gnome-initial-setup/pages/summary/gis-summary-page.ui
 gnome-initial-setup/pages/timezone/gis-timezone-page.c


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]